Skip to content

Commit

Permalink
Add input tests (mouse and keyboard)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dinnerbone committed Mar 28, 2024
1 parent 9a9fb5b commit 5112b3b
Show file tree
Hide file tree
Showing 8 changed files with 278 additions and 87 deletions.
155 changes: 155 additions & 0 deletions app/src/androidTest/java/rs/ruffle/InputEvents.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package rs.ruffle

import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Point
import android.graphics.Rect
import android.net.Uri
import android.view.KeyEvent
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import java.io.File
import kotlin.math.min
import kotlin.math.roundToInt
import org.hamcrest.CoreMatchers
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

private const val BASIC_SAMPLE_PACKAGE = "rs.ruffle"
private const val LAUNCH_TIMEOUT = 5000L
private const val SWF_WIDTH = 550.0
private const val SWF_HEIGHT = 400.0

@RunWith(AndroidJUnit4::class)
class InputEvents {
private lateinit var device: UiDevice
private lateinit var traceOutput: File
private lateinit var swfFile: File

@Before
fun startMainActivityFromHomeScreen() {
// Initialize UiDevice instance
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())

// Start from the home screen
device.pressHome()

// Wait for launcher
val launcherPackage: String = device.launcherPackageName
ViewMatchers.assertThat(launcherPackage, CoreMatchers.notNullValue())
device.wait(
Until.hasObject(By.pkg(launcherPackage).depth(0)),
LAUNCH_TIMEOUT
)

// Launch the app
val context = ApplicationProvider.getApplicationContext<Context>()
traceOutput = File.createTempFile("trace", ".txt", context.cacheDir)
swfFile = File.createTempFile("movie", ".swf", context.cacheDir)
val resources = InstrumentationRegistry.getInstrumentation().context.resources
val inStream = resources.openRawResource(
rs.ruffle.test.R.raw.input_test
)
val bytes = inStream.readBytes()
swfFile.writeBytes(bytes)
val intent = context.packageManager.getLaunchIntentForPackage(
BASIC_SAMPLE_PACKAGE
)?.apply {
component = ComponentName("rs.ruffle", "rs.ruffle.PlayerActivity")
data = Uri.fromFile(swfFile)
putExtra("traceOutput", traceOutput.absolutePath)
// Clear out any previous instances
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
context.startActivity(intent)

// Wait for the app to appear
device.wait(
Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)),
LAUNCH_TIMEOUT
)
}

@Test
fun clickEvents() {
device.waitForWindowUpdate(null, 1000)
ViewMatchers.assertThat(device, CoreMatchers.notNullValue())

val player = device.findObject(By.desc("Ruffle Player"))

val red = screenToSwf(player.visibleBounds, Point(50, 50))
val blue = screenToSwf(player.visibleBounds, Point(500, 350))
device.click(red)
device.click(blue)
device.drag(red.x, red.y, blue.x, blue.y, 100)
device.waitForWindowUpdate(null, 500)

val trace = traceOutput.readLines()
ViewMatchers.assertThat(
trace,
CoreMatchers.equalTo(
listOf(
"Test started!",
"red received mouseDown",
"red received mouseUp",
"red received click",
"blue received mouseDown",
"blue received mouseUp",
"blue received click",
"red received mouseDown",
"blue received mouseUp"
)
)
)
}

@Test
fun keyEvents() {
device.waitForWindowUpdate(null, 1000)
ViewMatchers.assertThat(device, CoreMatchers.notNullValue())

device.pressKeyCode(KeyEvent.KEYCODE_A)
device.pressKeyCode(KeyEvent.KEYCODE_B)

device.waitForWindowUpdate(null, 500)

val trace = traceOutput.readLines()
ViewMatchers.assertThat(
trace,
CoreMatchers.equalTo(
listOf(
"Test started!",
"keyDown: keyCode = 65, charCode = 97",
"keyUp: keyCode = 65, charCode = 97",
"keyDown: keyCode = 66, charCode = 98",
"keyUp: keyCode = 66, charCode = 98"
)
)
)
}

private fun screenToSwf(playerBounds: Rect, point: Point): Point {
val stretchX = playerBounds.width() / SWF_WIDTH
val stretchY = playerBounds.height() / SWF_HEIGHT
val scaleFactor = min(stretchX, stretchY)
val swfScreenWidth = SWF_WIDTH * scaleFactor
val swfScreenHeight = SWF_HEIGHT * scaleFactor
val swfOffsetX = (playerBounds.width() - swfScreenWidth) / 2
val swfOffsetY = (playerBounds.height() - swfScreenHeight) / 2
return Point(
(playerBounds.left + swfOffsetX + point.x * scaleFactor).roundToInt(),
(playerBounds.top + swfOffsetY + point.y * scaleFactor).roundToInt()
)
}
}

private fun UiDevice.click(point: Point) {
this.click(point.x, point.y)
}
Binary file added app/src/androidTest/res/raw/input_test.swf
Binary file not shown.
35 changes: 35 additions & 0 deletions app/src/androidTest/res/raw/input_tests/Test.as
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package {
import flash.display.MovieClip;
import flash.events.MouseEvent;
import flash.display.DisplayObject;
import flash.events.KeyboardEvent;

public class Test extends MovieClip {
public function Test() {
trace("Test started!");

addKeyListeners("stage", stage);
addMouseListeners("red", red);
addMouseListeners("blue", blue);
}

function addMouseListeners(name: String, clip: DisplayObject) {
var listener = function(event: MouseEvent) {
trace(name + " received " + event.type);
};
clip.addEventListener(MouseEvent.MOUSE_DOWN, listener);
clip.addEventListener(MouseEvent.MOUSE_UP, listener);
//clip.addEventListener(MouseEvent.MOUSE_OVER, listener);
//clip.addEventListener(MouseEvent.MOUSE_OUT, listener);
clip.addEventListener(MouseEvent.CLICK, listener);
}

function addKeyListeners(name: String, clip: DisplayObject) {
var listener = function(event: KeyboardEvent) {
trace(event.type + ": keyCode = " + event.keyCode + ", charCode = " + event.charCode);
};
stage.addEventListener(KeyboardEvent.KEY_DOWN, listener);
stage.addEventListener(KeyboardEvent.KEY_UP, listener);
}
}
}
Binary file added app/src/androidTest/res/raw/input_tests/test.fla
Binary file not shown.
Binary file added app/src/androidTest/res/raw/input_tests/test.swf
Binary file not shown.
3 changes: 3 additions & 0 deletions app/src/main/java/rs/ruffle/PlayerActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ class PlayerActivity : GameActivity() {
layout.id = contentViewId
setContentView(layout)
mSurfaceView = InputEnabledSurfaceView(this)

mSurfaceView.contentDescription = "Ruffle Player"

val placeholder = findViewById<View>(R.id.placeholder)
val pars = placeholder.layoutParams as ConstraintLayout.LayoutParams
val parent = placeholder.parent as ViewGroup
Expand Down
156 changes: 78 additions & 78 deletions src/keycodes.rs
Original file line number Diff line number Diff line change
@@ -1,85 +1,85 @@
use android_activity::input::Keycode as Android;
use ruffle_core::events::KeyCode as Ruffle;

pub fn android_keycode_to_ruffle(android: Android) -> Option<Ruffle> {
pub fn android_keycode_to_ruffle(android: Android) -> Option<(Ruffle, Option<char>)> {
Some(match android {
Android::A => Ruffle::A,
Android::B => Ruffle::B,
Android::C => Ruffle::C,
Android::D => Ruffle::D,
Android::E => Ruffle::E,
Android::F => Ruffle::F,
Android::G => Ruffle::G,
Android::H => Ruffle::H,
Android::I => Ruffle::I,
Android::J => Ruffle::J,
Android::K => Ruffle::K,
Android::L => Ruffle::L,
Android::M => Ruffle::M,
Android::N => Ruffle::N,
Android::O => Ruffle::O,
Android::P => Ruffle::P,
Android::Q => Ruffle::Q,
Android::R => Ruffle::R,
Android::S => Ruffle::S,
Android::T => Ruffle::T,
Android::U => Ruffle::U,
Android::V => Ruffle::V,
Android::W => Ruffle::W,
Android::X => Ruffle::X,
Android::Y => Ruffle::Y,
Android::Z => Ruffle::Z,
Android::Comma => Ruffle::Comma,
Android::Period => Ruffle::Period,
Android::Tab => Ruffle::Tab,
Android::Space => Ruffle::Space,
Android::Enter => Ruffle::Return,
Android::Del => Ruffle::Delete,
Android::Grave => Ruffle::Grave,
Android::Minus => Ruffle::Minus,
Android::Equals => Ruffle::Equals,
Android::LeftBracket => Ruffle::LBracket,
Android::RightBracket => Ruffle::RBracket,
Android::Backslash => Ruffle::Backslash,
Android::Semicolon => Ruffle::Semicolon,
Android::Apostrophe => Ruffle::Apostrophe,
Android::Slash => Ruffle::Slash,
Android::Plus => Ruffle::Plus,
Android::PageUp => Ruffle::PgUp,
Android::PageDown => Ruffle::PgDown,
Android::Escape => Ruffle::Escape,
Android::ForwardDel => Ruffle::Delete,
Android::CapsLock => Ruffle::CapsLock,
Android::ScrollLock => Ruffle::ScrollLock,
Android::MoveHome => Ruffle::Home,
Android::MoveEnd => Ruffle::End,
Android::Insert => Ruffle::Insert,
Android::F1 => Ruffle::F1,
Android::F2 => Ruffle::F2,
Android::F3 => Ruffle::F3,
Android::F4 => Ruffle::F4,
Android::F5 => Ruffle::F5,
Android::F6 => Ruffle::F6,
Android::F7 => Ruffle::F7,
Android::F8 => Ruffle::F8,
Android::F9 => Ruffle::F9,
Android::F10 => Ruffle::F10,
Android::F11 => Ruffle::F11,
Android::F12 => Ruffle::F12,
Android::Numpad0 => Ruffle::Numpad0,
Android::Numpad1 => Ruffle::Numpad1,
Android::Numpad2 => Ruffle::Numpad2,
Android::Numpad3 => Ruffle::Numpad3,
Android::Numpad4 => Ruffle::Numpad4,
Android::Numpad5 => Ruffle::Numpad5,
Android::Numpad6 => Ruffle::Numpad6,
Android::Numpad7 => Ruffle::Numpad7,
Android::Numpad8 => Ruffle::Numpad8,
Android::Numpad9 => Ruffle::Numpad9,
Android::NumpadDivide => Ruffle::NumpadSlash,
Android::NumpadSubtract => Ruffle::NumpadMinus,
Android::NumpadDot => Ruffle::NumpadPeriod,
Android::NumpadEnter => Ruffle::NumpadEnter,
Android::A => (Ruffle::A, Some('a')),
Android::B => (Ruffle::B, Some('b')),
Android::C => (Ruffle::C, Some('c')),
Android::D => (Ruffle::D, Some('d')),
Android::E => (Ruffle::E, Some('e')),
Android::F => (Ruffle::F, Some('f')),
Android::G => (Ruffle::G, Some('g')),
Android::H => (Ruffle::H, Some('h')),
Android::I => (Ruffle::I, Some('i')),
Android::J => (Ruffle::J, Some('j')),
Android::K => (Ruffle::K, Some('k')),
Android::L => (Ruffle::L, Some('l')),
Android::M => (Ruffle::M, Some('m')),
Android::N => (Ruffle::N, Some('n')),
Android::O => (Ruffle::O, Some('o')),
Android::P => (Ruffle::P, Some('p')),
Android::Q => (Ruffle::Q, Some('q')),
Android::R => (Ruffle::R, Some('r')),
Android::S => (Ruffle::S, Some('s')),
Android::T => (Ruffle::T, Some('t')),
Android::U => (Ruffle::U, Some('u')),
Android::V => (Ruffle::V, Some('v')),
Android::W => (Ruffle::W, Some('w')),
Android::X => (Ruffle::X, Some('x')),
Android::Y => (Ruffle::Y, Some('y')),
Android::Z => (Ruffle::Z, Some('z')),
Android::Comma => (Ruffle::Comma, Some(',')),
Android::Period => (Ruffle::Period, Some('.')),
Android::Tab => (Ruffle::Tab, Some('\t')),
Android::Space => (Ruffle::Space, Some(' ')),
Android::Enter => (Ruffle::Return, Some(13 as char)),
Android::Del => (Ruffle::Backspace, Some(8 as char)),
Android::Grave => (Ruffle::Grave, Some('`')),
Android::Minus => (Ruffle::Minus, Some('-')),
Android::Equals => (Ruffle::Equals, Some('=')),
Android::LeftBracket => (Ruffle::LBracket, Some('(')),
Android::RightBracket => (Ruffle::RBracket, Some(')')),
Android::Backslash => (Ruffle::Backslash, Some('\\')),
Android::Semicolon => (Ruffle::Semicolon, Some(';')),
Android::Apostrophe => (Ruffle::Apostrophe, Some('\'')),
Android::Slash => (Ruffle::Slash, Some('/')),
Android::Plus => (Ruffle::Plus, Some('+')),
Android::PageUp => (Ruffle::PgUp, None),
Android::PageDown => (Ruffle::PgDown, None),
Android::Escape => (Ruffle::Escape, None),
Android::ForwardDel => (Ruffle::Delete, Some(127 as char)),
Android::CapsLock => (Ruffle::CapsLock, None),
Android::ScrollLock => (Ruffle::ScrollLock, None),
Android::MoveHome => (Ruffle::Home, None),
Android::MoveEnd => (Ruffle::End, None),
Android::Insert => (Ruffle::Insert, None),
Android::F1 => (Ruffle::F1, None),
Android::F2 => (Ruffle::F2, None),
Android::F3 => (Ruffle::F3, None),
Android::F4 => (Ruffle::F4, None),
Android::F5 => (Ruffle::F5, None),
Android::F6 => (Ruffle::F6, None),
Android::F7 => (Ruffle::F7, None),
Android::F8 => (Ruffle::F8, None),
Android::F9 => (Ruffle::F9, None),
Android::F10 => (Ruffle::F10, None),
Android::F11 => (Ruffle::F11, None),
Android::F12 => (Ruffle::F12, None),
Android::Numpad0 => (Ruffle::Numpad0, Some('0')),
Android::Numpad1 => (Ruffle::Numpad1, Some('1')),
Android::Numpad2 => (Ruffle::Numpad2, Some('2')),
Android::Numpad3 => (Ruffle::Numpad3, Some('3')),
Android::Numpad4 => (Ruffle::Numpad4, Some('4')),
Android::Numpad5 => (Ruffle::Numpad5, Some('5')),
Android::Numpad6 => (Ruffle::Numpad6, Some('6')),
Android::Numpad7 => (Ruffle::Numpad7, Some('7')),
Android::Numpad8 => (Ruffle::Numpad8, Some('8')),
Android::Numpad9 => (Ruffle::Numpad9, Some('9')),
Android::NumpadDivide => (Ruffle::NumpadSlash, Some('/')),
Android::NumpadSubtract => (Ruffle::NumpadMinus, Some('-')),
Android::NumpadDot => (Ruffle::NumpadPeriod, Some('.')),
Android::NumpadEnter => (Ruffle::NumpadEnter, Some(13 as char)),
_ => return None,
})
}
16 changes: 7 additions & 9 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,20 +325,18 @@ fn run(app: AndroidApp) {
}
InputEvent::KeyEvent(event) => {
if let Some(player) = playerbox.as_ref() {
let Some(key_code) =
let Some((key_code, key_char)) =
android_keycode_to_ruffle(event.key_code())
else {
return InputStatus::Unhandled;
};
let ruffle_event = match event.action() {
KeyAction::Down => PlayerEvent::KeyDown {
key_code,
key_char: None,
},
KeyAction::Up => PlayerEvent::KeyUp {
key_code,
key_char: None,
},
KeyAction::Down => {
PlayerEvent::KeyDown { key_code, key_char }
}
KeyAction::Up => {
PlayerEvent::KeyUp { key_code, key_char }
}
_ => return InputStatus::Unhandled,
};
player
Expand Down

0 comments on commit 5112b3b

Please sign in to comment.