From aeb4ff7230ca436a1ccf4d8da771885701c3b07b Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:58:16 -0700 Subject: [PATCH] test(mobile): cover held composer arrows and focus transfer --- .../HardwareKeyboardSubmitArrowTest.kt | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/mobile/packages/expo-hardware-keyboard/android/src/test/java/expo/modules/hardwarekeyboard/HardwareKeyboardSubmitArrowTest.kt b/mobile/packages/expo-hardware-keyboard/android/src/test/java/expo/modules/hardwarekeyboard/HardwareKeyboardSubmitArrowTest.kt index a86c798f6b7..153e7598302 100644 --- a/mobile/packages/expo-hardware-keyboard/android/src/test/java/expo/modules/hardwarekeyboard/HardwareKeyboardSubmitArrowTest.kt +++ b/mobile/packages/expo-hardware-keyboard/android/src/test/java/expo/modules/hardwarekeyboard/HardwareKeyboardSubmitArrowTest.kt @@ -2,6 +2,7 @@ package expo.modules.hardwarekeyboard import android.view.InputDevice import android.view.KeyEvent +import android.widget.Button import android.view.inputmethod.BaseInputConnection import com.facebook.react.bridge.NativeModule import com.facebook.react.uimanager.DisplayMetricsHolder @@ -109,4 +110,49 @@ class HardwareKeyboardSubmitArrowTest { fixture.capture.onWindowFocusChanged(false) assertFalse(fixture.key(action = KeyEvent.ACTION_UP)) } + + @Test fun heldMovementClaimsReleaseOnlyAfterAnUnhandledBoundaryRepeat() { + val fixture = Fixture() + fixture.nativeHandled = true + assertTrue(fixture.key()) + assertTrue(fixture.key(repeat = 1)) + fixture.nativeHandled = false + assertTrue(fixture.key(repeat = 2)) + assertTrue(fixture.key(repeat = 3)) + val delegated = fixture.delegated.size + assertTrue(fixture.key(action = KeyEvent.ACTION_UP)) + assertEquals(delegated, fixture.delegated.size) + assertEquals(listOf(0, 1, 2, 3), fixture.delegated.map { it.repeatCount }) + } + + @Test fun boundaryReleaseDoesNotLeakToNewlyFocusedControl() { + val fixture = Fixture() + val received = mutableListOf() + val button = object : Button(fixture.input.context) { + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + received.add(event) + return false + } + }.apply { isFocusableInTouchMode = true } + fixture.capture.addView(button) + button.layout(0, 0, 100, 100) + assertTrue(fixture.key()) + assertTrue(button.requestFocus()) + assertFalse(fixture.input.hasFocus()) + assertTrue(fixture.key(action = KeyEvent.ACTION_UP)) + assertTrue(received.isEmpty()) + assertFalse(fixture.key()) + assertFalse(fixture.key(action = KeyEvent.ACTION_UP)) + assertEquals(listOf(KeyEvent.ACTION_DOWN, KeyEvent.ACTION_UP), received.map { it.action }) + } + + @Test fun freshNativeMovementSupersedesLostBoundaryRelease() { + val fixture = Fixture() + assertTrue(fixture.key()) + fixture.nativeHandled = true + assertTrue(fixture.key()) + assertTrue(fixture.key(action = KeyEvent.ACTION_UP)) + assertEquals(listOf(KeyEvent.ACTION_DOWN, KeyEvent.ACTION_DOWN, KeyEvent.ACTION_UP), + fixture.delegated.map { it.action }) + } }