From e9c4aaa6497806722cd53e7dc3541abd332402df Mon Sep 17 00:00:00 2001 From: Igor Demin Date: Tue, 14 Feb 2023 13:50:13 +0100 Subject: [PATCH 1/2] Fix Korean text input Fixes https://github.com/JetBrains/compose-jb/issues/2600 1. We shouldn't skip input events with empty text, they will clear the current uncommitted text. In case of Korean, Swing will send a clear event of the previous char when we press Space, and send a separate "KEY_TYPED" event for the last character: - press q -> q uncommitted - press w -> commit q, w uncommitted - press Space -> discard committed text (event with empty text), send KEY_TYPED event with w and KEY_TYPED event with Space 2. Add tests for test input. I traced Swing events on each OS to mimic integration tests (It is difficult to write real input tests) --- .../compose/ui/awt/ComposeLayer.desktop.kt | 9 +- .../platform/DesktopPlatformInput.desktop.kt | 34 +- .../kotlin/androidx/compose/ui/TestUtils.kt | 48 +- .../ui/platform/DesktopInputComponentTest.kt | 4 +- .../androidx/compose/ui/window/TestUtils.kt | 21 +- .../ui/window/window/WindowTypeTest.kt | 688 ++++++++++++++++++ 6 files changed, 743 insertions(+), 61 deletions(-) create mode 100644 compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/window/WindowTypeTest.kt diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt index 8a78c1ef79ae3..11938c13a8c68 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt @@ -331,17 +331,14 @@ internal class ComposeLayer( _component.addInputMethodListener(object : InputMethodListener { override fun caretPositionChanged(event: InputMethodEvent?) { if (isDisposed) return - if (event != null) { - catchExceptions { - platform.textInputService.onInputEvent(event) - } - } + // Which OSes and which input method could produce such events? We need to have some + // specific cases in mind before implementing this } override fun inputMethodTextChanged(event: InputMethodEvent) { if (isDisposed) return catchExceptions { - platform.textInputService.onInputEvent(event) + platform.textInputService.inputMethodTextChanged(event) } } }) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt index 3451fac59f116..06d75adf83ee7 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt @@ -106,35 +106,17 @@ internal class PlatformInput(private val component: PlatformComponent) : } } - fun onInputEvent(event: InputMethodEvent) { + fun inputMethodTextChanged(event: InputMethodEvent) { if (!event.isConsumed) { - when (event.id) { - InputMethodEvent.INPUT_METHOD_TEXT_CHANGED -> { - replaceInputMethodText(event) - event.consume() - } - InputMethodEvent.CARET_POSITION_CHANGED -> { - inputMethodCaretPositionChanged(event) - event.consume() - } - } + replaceInputMethodText(event) + event.consume() } } - private fun inputMethodCaretPositionChanged( - @Suppress("UNUSED_PARAMETER") event: InputMethodEvent - ) { - // Which OSes and which input method could produce such events? We need to have some - // specific cases in mind before implementing this - } - private fun replaceInputMethodText(event: InputMethodEvent) { currentInput?.let { input -> - if (event.text == null) { - return - } - val committed = event.text.toStringUntil(event.committedCharacterCount) - val composing = event.text.toStringFrom(event.committedCharacterCount) + val committed = event.text?.toStringUntil(event.committedCharacterCount).orEmpty() + val composing = event.text?.toStringFrom(event.committedCharacterCount).orEmpty() val ops = mutableListOf() if (needToDeletePreviousChar && isMac && input.value.selection.min > 0 && composing.isEmpty()) { @@ -142,11 +124,7 @@ internal class PlatformInput(private val component: PlatformComponent) : ops.add(DeleteSurroundingTextInCodePointsCommand(1, 0)) } - // newCursorPosition == 1 leads to effectively ignoring of this parameter in EditCommands - // processing. the cursor will be set after the inserted text. - if (committed.isNotEmpty()) { - ops.add(CommitTextCommand(committed, 1)) - } + ops.add(CommitTextCommand(committed, 1)) if (composing.isNotEmpty()) { ops.add(SetComposingTextCommand(composing, 1)) } diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/TestUtils.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/TestUtils.kt index ceb6b649e3aee..a309df580eb0e 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/TestUtils.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/TestUtils.kt @@ -22,34 +22,21 @@ import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.Painter -import java.awt.AWTEvent import java.awt.Component import java.awt.Container -import java.awt.EventQueue import java.awt.Image -import java.awt.Toolkit import java.awt.Window -import java.awt.event.InvocationEvent +import java.awt.event.InputMethodEvent import java.awt.event.KeyEvent import java.awt.event.MouseEvent import java.awt.event.MouseWheelEvent +import java.awt.font.TextHitInfo import java.awt.image.BufferedImage import java.awt.image.MultiResolutionImage -import java.lang.reflect.InvocationTargetException -import java.lang.reflect.Method -import java.util.Objects -import java.util.concurrent.atomic.AtomicReference +import java.text.AttributedString import javax.swing.Icon import javax.swing.ImageIcon -import javax.swing.JFrame -import javax.swing.SwingUtilities -import kotlin.coroutines.resume -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.yield -import org.jetbrains.annotations.NonNls -import org.jetbrains.annotations.TestOnly -import org.jetbrains.skiko.MainUIDispatcher fun testImage(color: Color): Painter = run { val bitmap = ImageBitmap(100, 100) @@ -72,19 +59,40 @@ internal val isMacOs = os.startsWith("mac") fun Window.sendKeyEvent( code: Int, + char: Char = code.toChar(), + id: Int = KeyEvent.KEY_PRESSED, + location: Int = KeyEvent.KEY_LOCATION_STANDARD, modifiers: Int = 0 ): Boolean { val event = KeyEvent( // if we would just use `focusOwner` then it will be null if the window is minimized mostRecentFocusOwner, - KeyEvent.KEY_PRESSED, + id, 0, modifiers, code, - code.toChar(), - KeyEvent.KEY_LOCATION_STANDARD + char, + location + ) + mostRecentFocusOwner!!.dispatchEvent(event) + return event.isConsumed +} + +fun Window.sendInputEvent( + text: String?, + committedCharacterCount: Int, +): Boolean { + val event = InputMethodEvent( + // if we would just use `focusOwner` then it will be null if the window is minimized + mostRecentFocusOwner, + InputMethodEvent.INPUT_METHOD_TEXT_CHANGED, + 0, + text?.let(::AttributedString)?.iterator, + committedCharacterCount, + TextHitInfo.leading(0), + TextHitInfo.leading(0) ) - dispatchEvent(event) + mostRecentFocusOwner!!.dispatchEvent(event) return event.isConsumed } diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt index 7e48df385719b..b3c017fbdd4e0 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt @@ -56,7 +56,7 @@ class DesktopInputComponentTest { val familyEmoji = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66" - input.onInputEvent( + input.inputMethodTextChanged( InputMethodEvent( DummyComponent, InputMethodEvent.INPUT_METHOD_TEXT_CHANGED, @@ -104,7 +104,7 @@ class DesktopInputComponentTest { component.enabledInput!!.getSelectedText(null) input.charKeyPressed = false - input.onInputEvent( + input.inputMethodTextChanged( InputMethodEvent( DummyComponent, InputMethodEvent.INPUT_METHOD_TEXT_CHANGED, diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/TestUtils.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/TestUtils.kt index 00110f5653a67..273b0ba71b60e 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/TestUtils.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/TestUtils.kt @@ -36,7 +36,6 @@ import kotlinx.coroutines.yield import org.jetbrains.skiko.MainUIDispatcher import org.junit.Assume.assumeFalse -@OptIn(ExperimentalCoroutinesApi::class) internal fun runApplicationTest( /** * Use delay(500) additionally to `yield` in `await*` functions @@ -45,9 +44,15 @@ internal fun runApplicationTest( * (non-flaky). * * We have to use `useDelay` in some Linux Tests, because Linux can behave in - * non-deterministic way when we change position/size very fast (see the snippet below) + * non-deterministic way when we change position/size very fast (see the snippet below). */ useDelay: Boolean = false, + // TODO ui-test solved this issue by passing InfiniteAnimationPolicy to CoroutineContext. Do the same way here + /** + * Hint for `awaitIdle` that the content contains animations (ProgressBar, TextField cursor, etc). + * In this case, we use `delay` instead of waiting for state changes to end. + */ + hasAnimations: Boolean = false, timeoutMillis: Long = 30000, body: suspend WindowTestScope.() -> Unit ) { @@ -57,7 +62,7 @@ internal fun runApplicationTest( withTimeout(timeoutMillis) { val exceptionHandler = TestExceptionHandler() withExceptionHandler(exceptionHandler) { - val scope = WindowTestScope(this, useDelay, exceptionHandler) + val scope = WindowTestScope(this, useDelay, hasAnimations, exceptionHandler) scope.body() scope.exitTestApplication() } @@ -100,6 +105,7 @@ internal class TestExceptionHandler : Thread.UncaughtExceptionHandler { internal class WindowTestScope( private val scope: CoroutineScope, private val useDelay: Boolean, + private val hasAnimations: Boolean, private val exceptionHandler: TestExceptionHandler ) : CoroutineScope by CoroutineScope(scope.coroutineContext + Job()) { var isOpen by mutableStateOf(true) @@ -132,8 +138,13 @@ internal class WindowTestScope( awaitEDT() Snapshot.sendApplyNotifications() - for (recomposerInfo in Recomposer.runningRecomposers.value - initialRecomposers) { - recomposerInfo.state.takeWhile { it > Recomposer.State.Idle }.collect() + + if (hasAnimations) { + delay(500) + } else { + for (recomposerInfo in Recomposer.runningRecomposers.value - initialRecomposers) { + recomposerInfo.state.takeWhile { it > Recomposer.State.Idle }.collect() + } } exceptionHandler.throwIfCaught() diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/window/WindowTypeTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/window/WindowTypeTest.kt new file mode 100644 index 0000000000000..ca11c34783a5d --- /dev/null +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/window/WindowTypeTest.kt @@ -0,0 +1,688 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.window.window + +import androidx.compose.material.TextField +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.ComposeWindow +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.sendInputEvent +import androidx.compose.ui.sendKeyEvent +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowTestScope +import androidx.compose.ui.window.runApplicationTest +import com.google.common.truth.Truth.assertThat +import java.awt.event.KeyEvent.KEY_LOCATION_UNKNOWN +import java.awt.event.KeyEvent.KEY_PRESSED +import java.awt.event.KeyEvent.KEY_RELEASED +import java.awt.event.KeyEvent.KEY_TYPED +import org.junit.Test + +/** + * Tests for emulate input to the native window on various systems. + * + * Events were captured on each system via logging. + */ +class WindowTypeTest { + private var window: ComposeWindow? = null + private var text by mutableStateOf(TextFieldValue("")) + + @Test + fun `q, w, space, backspace 4x (English)`() = runTypeTest { + // q + window?.sendKeyEvent(81, 'q', KEY_PRESSED) + window?.sendKeyEvent(0, 'q', KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(81, 'q', KEY_RELEASED) + assert(text, "q", selection = TextRange(1), composition = null) + + // w + window?.sendKeyEvent(87, 'w', KEY_PRESSED) + window?.sendKeyEvent(0, 'w', KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(87, 'w', KEY_RELEASED) + assert(text, "qw", selection = TextRange(2), composition = null) + + // space + window?.sendKeyEvent(32, ' ', KEY_PRESSED) + window?.sendKeyEvent(0, ' ', KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(32, ' ', KEY_RELEASED) + assert(text, "qw ", selection = TextRange(3), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "qw", selection = TextRange(2), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "q", selection = TextRange(1), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + } + + @Test + fun `q, w, space, backspace 4x (Russian)`() = runTypeTest { + // q + window?.sendKeyEvent(81, 'й', KEY_PRESSED) + window?.sendKeyEvent(0, 'й', KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(81, 'й', KEY_RELEASED) + assert(text, "й", selection = TextRange(1), composition = null) + + // w + window?.sendKeyEvent(87, 'ц', KEY_PRESSED) + window?.sendKeyEvent(0, 'ц', KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(87, 'ц', KEY_RELEASED) + assert(text, "йц", selection = TextRange(2), composition = null) + + // space + window?.sendKeyEvent(32, ' ', KEY_PRESSED) + window?.sendKeyEvent(0, ' ', KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(32, ' ', KEY_RELEASED) + assert(text, "йц ", selection = TextRange(3), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "йц", selection = TextRange(2), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "й", selection = TextRange(1), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + } + + @Test + fun `f, g, space, backspace 4x (Arabic)`() = runTypeTest { + // q + window?.sendKeyEvent(70, 'ب', KEY_PRESSED) + window?.sendKeyEvent(0, 'ب', KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(70, 'ب', KEY_RELEASED) + assert(text, "ب", selection = TextRange(1), composition = null) + + // w + window?.sendKeyEvent(71, 'ل', KEY_PRESSED) + window?.sendKeyEvent(0, 'ل', KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(71, 'ل', KEY_RELEASED) + assert(text, "بل", selection = TextRange(2), composition = null) + + // space + window?.sendKeyEvent(32, ' ', KEY_PRESSED) + window?.sendKeyEvent(0, ' ', KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(32, ' ', KEY_RELEASED) + assert(text, "بل ", selection = TextRange(3), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "بل", selection = TextRange(2), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "ب", selection = TextRange(1), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + } + + @Test + fun `q, w, space, backspace 4x (Korean, Windows)`() = runTypeTest { + // q + window?.sendInputEvent("ㅂ", 0) + window?.sendKeyEvent(81, 'q', KEY_RELEASED) + assert(text, "ㅂ", selection = TextRange(1), composition = TextRange(0, 1)) + + // w + window?.sendInputEvent("ㅂ", 1) + window?.sendInputEvent("ㅈ", 0) + window?.sendKeyEvent(87, 'w', KEY_RELEASED) + assert(text, "ㅂㅈ", selection = TextRange(2), composition = TextRange(1, 2)) + + // space + window?.sendInputEvent(null, 0) + window?.sendKeyEvent(0, 'ㅈ', KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(32, ' ', KEY_PRESSED) + window?.sendKeyEvent(0, ' ', KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(32, ' ', KEY_RELEASED) + assert(text, "ㅂㅈ ", selection = TextRange(3), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "ㅂㅈ", selection = TextRange(2), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "ㅂ", selection = TextRange(1), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + } + + @Test + fun `q, w, backspace 3x (Korean, Windows)`() = runTypeTest { + // q + window?.sendInputEvent("ㅂ", 0) + window?.sendKeyEvent(81, 'q', KEY_RELEASED) + assert(text, "ㅂ", selection = TextRange(1), composition = TextRange(0, 1)) + + // w + window?.sendInputEvent("ㅂ", 1) + window?.sendInputEvent("ㅈ", 0) + window?.sendKeyEvent(87, 'w', KEY_RELEASED) + assert(text, "ㅂㅈ", selection = TextRange(2), composition = TextRange(1, 2)) + + // backspace + window?.sendInputEvent(null, 0) + window?.sendInputEvent(null, 0) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "ㅂ", selection = TextRange(1), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + } + + @Test + fun `f, g, space, backspace 3x (Korean, Windows)`() = runTypeTest { + // f + window?.sendInputEvent("ㄹ", 0) + window?.sendKeyEvent(81, 'f', KEY_RELEASED) + assert(text, "ㄹ", selection = TextRange(1), composition = TextRange(0, 1)) + + // g + window?.sendInputEvent("ㅀ", 0) + window?.sendKeyEvent(87, 'g', KEY_RELEASED) + assert(text, "ㅀ", selection = TextRange(1), composition = TextRange(0, 1)) + + // space + window?.sendInputEvent(null, 0) + window?.sendKeyEvent(0, 'ㅀ', KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(32, ' ', KEY_PRESSED) + window?.sendKeyEvent(0, ' ', KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(32, ' ', KEY_RELEASED) + assert(text, "ㅀ ", selection = TextRange(2), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "ㅀ", selection = TextRange(1), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + } + + @Test + fun `f, g, backspace 2x (Korean, Windows)`() = runTypeTest { + // f + window?.sendInputEvent("ㄹ", 0) + window?.sendKeyEvent(81, 'f', KEY_RELEASED) + assert(text, "ㄹ", selection = TextRange(1), composition = TextRange(0, 1)) + + // g + window?.sendInputEvent("ㅀ", 0) + window?.sendKeyEvent(87, 'g', KEY_RELEASED) + assert(text, "ㅀ", selection = TextRange(1), composition = TextRange(0, 1)) + + // backspace + window?.sendInputEvent("ㄹ", 0) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "ㄹ", selection = TextRange(1), composition = TextRange(0, 1)) + + // backspace + window?.sendInputEvent(null, 0) + window?.sendInputEvent(null, 0) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + } + + @Test + fun `q, w, space, backspace 4x (Korean, macOS)`() = runTypeTest { + // q + window?.sendInputEvent("ㅂ", 0) + window?.sendKeyEvent(81, 'ㅂ', KEY_RELEASED) + assert(text, "ㅂ", selection = TextRange(1), composition = TextRange(0, 1)) + + // w + window?.sendInputEvent("ㅂ", 0) + window?.sendInputEvent("ㅂ", 1) + window?.sendInputEvent("ㅈ", 0) + window?.sendKeyEvent(87, 'ㅈ', KEY_RELEASED) + assert(text, "ㅂㅈ", selection = TextRange(2), composition = TextRange(1, 2)) + + // space + window?.sendInputEvent("ㅈ ", 0) + window?.sendInputEvent("ㅈ ", 2) + window?.sendKeyEvent(32, ' ', KEY_RELEASED) + assert(text, "ㅂㅈ ", selection = TextRange(3), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "ㅂㅈ", selection = TextRange(2), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "ㅂ", selection = TextRange(1), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + } + + @Test + fun `q, w, backspace 3x (Korean, macOS)`() = runTypeTest { + // q + window?.sendInputEvent("ㅂ", 0) + window?.sendKeyEvent(81, 'ㅂ', KEY_RELEASED) + assert(text, "ㅂ", selection = TextRange(1), composition = TextRange(0, 1)) + + // w + window?.sendInputEvent("ㅂ", 0) + window?.sendInputEvent("ㅂ", 1) + window?.sendInputEvent("ㅈ", 0) + window?.sendKeyEvent(87, 'ㅈ', KEY_RELEASED) + assert(text, "ㅂㅈ", selection = TextRange(2), composition = TextRange(1, 2)) + + // backspace + window?.sendInputEvent("ㅈ", 0) + window?.sendInputEvent("ㅈ", 1) + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "ㅂ", selection = TextRange(1), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + } + + // f, g on macOs prints 2 separate symbols (comparing to Windows), so we test t + y + @Test + fun `t, y, space, backspace 3x (Korean, MacOS)`() = runTypeTest { + // t + window?.sendInputEvent("ㅅ", 0) + window?.sendKeyEvent(84, 'ㅅ', KEY_RELEASED) + assert(text, "ㅅ", selection = TextRange(1), composition = TextRange(0, 1)) + + // y + window?.sendInputEvent("쇼", 0) + window?.sendKeyEvent(89, 'ㅛ', KEY_RELEASED) + assert(text, "쇼", selection = TextRange(1), composition = TextRange(0, 1)) + + // space + window?.sendInputEvent("쇼 ", 0) + window?.sendInputEvent("쇼 ", 2) + window?.sendKeyEvent(32, ' ', KEY_RELEASED) + assert(text, "쇼 ", selection = TextRange(2), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "쇼", selection = TextRange(1), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + } + + @Test + fun `t, y, backspace 2x (Korean, MacOS)`() = runTypeTest { + // t + window?.sendInputEvent("ㅅ", 0) + window?.sendKeyEvent(84, 'ㅅ', KEY_RELEASED) + assert(text, "ㅅ", selection = TextRange(1), composition = TextRange(0, 1)) + + // y + window?.sendInputEvent("쇼", 0) + window?.sendKeyEvent(89, 'ㅛ', KEY_RELEASED) + assert(text, "쇼", selection = TextRange(1), composition = TextRange(0, 1)) + + // backspace + window?.sendInputEvent("ㅅ", 0) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "ㅅ", selection = TextRange(1), composition = TextRange(0, 1)) + + // backspace + window?.sendInputEvent("ㅅ", 0) + window?.sendInputEvent("ㅅ", 1) + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + } + + @Test + fun `q, w, space, backspace 4x (Korean, Linux)`() = runTypeTest { + // q + window?.sendInputEvent("ㅂ", 0) + window?.sendKeyEvent(0, 'ㅂ', KEY_RELEASED) + assert(text, "ㅂ", selection = TextRange(1), composition = TextRange(0, 1)) + + // w + window?.sendInputEvent(null, 0) + window?.sendInputEvent("ㅂ", 1) + window?.sendInputEvent("ㅈ", 0) + window?.sendKeyEvent(0, 'ㅈ', KEY_RELEASED) + assert(text, "ㅂㅈ", selection = TextRange(2), composition = TextRange(1, 2)) + + // space + window?.sendInputEvent(null, 0) + window?.sendInputEvent("ㅈ", 1) + window?.sendKeyEvent(32, ' ', KEY_PRESSED) + window?.sendKeyEvent(0, ' ', KEY_TYPED) + window?.sendKeyEvent(32, ' ', KEY_RELEASED) + assert(text, "ㅂㅈ ", selection = TextRange(3), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "ㅂㅈ", selection = TextRange(2), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "ㅂ", selection = TextRange(1), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + } + + @Test + fun `q, w, space, backspace 3x (Chinese, Windows)`() = runTypeTest { + // q + window?.sendInputEvent("q", 0) + window?.sendKeyEvent(81, 'q', KEY_RELEASED) + assert(text, "q", selection = TextRange(1), composition = TextRange(0, 1)) + + // w + window?.sendInputEvent("q'w", 0) + window?.sendKeyEvent(87, 'w', KEY_RELEASED) + assert(text, "q'w", selection = TextRange(3), composition = TextRange(0, 3)) + + // space + window?.sendInputEvent("請問", 2) + window?.sendInputEvent(null, 0) + window?.sendKeyEvent(32, ' ', KEY_RELEASED) + assert(text, "請問", selection = TextRange(2), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "請", selection = TextRange(1), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + } + + @Test + fun `q, w, backspace 3x (Chinese, Windows)`() = runTypeTest { + // q + window?.sendInputEvent("q", 0) + window?.sendKeyEvent(81, 'q', KEY_RELEASED) + assert(text, "q", selection = TextRange(1), composition = TextRange(0, 1)) + + // w + window?.sendInputEvent("q'w", 0) + window?.sendKeyEvent(87, 'w', KEY_RELEASED) + assert(text, "q'w", selection = TextRange(3), composition = TextRange(0, 3)) + + // backspace + window?.sendInputEvent("q", 0) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "q", selection = TextRange(1), composition = TextRange(0, 1)) + + // backspace + window?.sendInputEvent(null, 0) + window?.sendInputEvent(null, 0) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + } + + @Test + fun `q, w, space, backspace 3x (Chinese, macOS)`() = runTypeTest { + // q + window?.sendInputEvent("q", 0) + window?.sendKeyEvent(81, 'q', KEY_RELEASED) + assert(text, "q", selection = TextRange(1), composition = TextRange(0, 1)) + + // w + window?.sendInputEvent("q w", 0) + window?.sendKeyEvent(87, 'w', KEY_RELEASED) + assert(text, "q w", selection = TextRange(3), composition = TextRange(0, 3)) + + // space + window?.sendInputEvent("请问", 2) + window?.sendKeyEvent(32, ' ', KEY_RELEASED) + assert(text, "请问", selection = TextRange(2), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "请", selection = TextRange(1), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + } + + @Test + fun `q, w, backspace 3x (Chinese, macOS)`() = runTypeTest { + // q + window?.sendInputEvent("q", 0) + window?.sendKeyEvent(81, 'q', KEY_RELEASED) + assert(text, "q", selection = TextRange(1), composition = TextRange(0, 1)) + + // w + window?.sendInputEvent("q w", 0) + window?.sendKeyEvent(87, 'w', KEY_RELEASED) + assert(text, "q w", selection = TextRange(3), composition = TextRange(0, 3)) + + // backspace + window?.sendInputEvent("q", 0) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "q", selection = TextRange(1), composition = TextRange(0, 1)) + + // backspace + window?.sendInputEvent("", 0) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + + // backspace + window?.sendKeyEvent(8, Char(8), KEY_PRESSED) + window?.sendKeyEvent(0, Char(8), KEY_TYPED, KEY_LOCATION_UNKNOWN) + window?.sendKeyEvent(8, Char(8), KEY_RELEASED) + assert(text, "", selection = TextRange(0), composition = null) + } + + private fun runTypeTest(body: suspend WindowTestScope.() -> Unit) = runApplicationTest(hasAnimations = true) { + launchTestApplication { + Window(onCloseRequest = ::exitApplication) { + this@WindowTypeTest.window = this.window + val focusRequester = FocusRequester() + TextField(text, { text = it }, modifier = Modifier.focusRequester(focusRequester)) + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + } + } + + awaitIdle() + body() + exitApplication() + } + + private suspend fun WindowTestScope.assert( + actual: TextFieldValue, text: String, selection: TextRange, composition: TextRange? + ) { + awaitIdle() + assertThat(actual.text).isEqualTo(text) + assertThat(actual.selection).isEqualTo(selection) + assertThat(actual.composition).isEqualTo(composition) + } +} From bd4fcda02403d776a52c565f2fda2b89abfd7e8b Mon Sep 17 00:00:00 2001 From: Igor Demin Date: Thu, 23 Feb 2023 13:54:07 +0100 Subject: [PATCH 2/2] Update WindowTypeTest.kt --- .../kotlin/androidx/compose/ui/window/window/WindowTypeTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/window/WindowTypeTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/window/WindowTypeTest.kt index ca11c34783a5d..f6fa1c7fc07f6 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/window/WindowTypeTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/window/WindowTypeTest.kt @@ -43,6 +43,8 @@ import org.junit.Test * Tests for emulate input to the native window on various systems. * * Events were captured on each system via logging. + * All tests can run on all OS'es. + * The OS names in test names just represent an unique order of input events on these OS'es. */ class WindowTypeTest { private var window: ComposeWindow? = null