From b45efc2d15400a3f6ed4504a03b10324fc5eec8b Mon Sep 17 00:00:00 2001 From: Shagen Ogandzhanian Date: Tue, 27 Feb 2024 18:23:51 +0100 Subject: [PATCH 1/4] Introduce common SelectionTests --- .../selection/DesktopDefaultSelectionTests.kt | 31 ++ .../selection/DesktopSelectionMacosTests.kt | 31 ++ .../text/selection/SelectionTests.kt | 291 ------------------ .../text/selection/CommonSelectionTest.kt | 168 ++++++++++ .../text/selection/KeyboardActions.kt | 116 +++++++ .../WasmSelectionTests.kt | 39 +++ .../foundation/text/TextFieldKeyInput.js.kt | 7 +- .../ui/test/InputDispatcher.wasmMain.kt | 68 +++- 8 files changed, 453 insertions(+), 298 deletions(-) create mode 100644 compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/text/selection/DesktopDefaultSelectionTests.kt create mode 100644 compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/text/selection/DesktopSelectionMacosTests.kt delete mode 100644 compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/text/selection/SelectionTests.kt create mode 100644 compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/text/selection/CommonSelectionTest.kt create mode 100644 compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/text/selection/KeyboardActions.kt create mode 100644 compose/foundation/foundation/src/wasmJsTest/kotlin/androidx.compose.foundation.text.selection/WasmSelectionTests.kt diff --git a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/text/selection/DesktopDefaultSelectionTests.kt b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/text/selection/DesktopDefaultSelectionTests.kt new file mode 100644 index 0000000000000..8fae7f041e39e --- /dev/null +++ b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/text/selection/DesktopDefaultSelectionTests.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2023 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.foundation.text.selection + +import androidx.compose.foundation.text.KeyMapping +import androidx.compose.foundation.text.defaultSkikoKeyMapping +import androidx.compose.foundation.text.overriddenDefaultKeyMapping + +internal class DesktopDefaultSelectionTests : + CommonSelectionTests( + DefaultKeyboardActions, defaultSkikoKeyMapping + ) { + + override fun setPlatformDefaultKeyMapping(value: KeyMapping) { + overriddenDefaultKeyMapping = value + } +} diff --git a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/text/selection/DesktopSelectionMacosTests.kt b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/text/selection/DesktopSelectionMacosTests.kt new file mode 100644 index 0000000000000..bad2bdeb2f013 --- /dev/null +++ b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/text/selection/DesktopSelectionMacosTests.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2023 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.foundation.text.selection + +import androidx.compose.foundation.text.KeyMapping +import androidx.compose.foundation.text.createMacosDefaultKeyMapping +import androidx.compose.foundation.text.overriddenDefaultKeyMapping + +internal class DesktopSelectionMacosTests : + CommonSelectionTests( + MacosKeyboardActions, createMacosDefaultKeyMapping() + ) { + + override fun setPlatformDefaultKeyMapping(value: KeyMapping) { + overriddenDefaultKeyMapping = value + } +} diff --git a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/text/selection/SelectionTests.kt b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/text/selection/SelectionTests.kt deleted file mode 100644 index 947f8d71a1557..0000000000000 --- a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/text/selection/SelectionTests.kt +++ /dev/null @@ -1,291 +0,0 @@ -/* - * Copyright 2023 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.foundation.text.selection - -import androidx.compose.foundation.DesktopPlatform -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyMapping -import androidx.compose.foundation.text.createPlatformDefaultKeyMapping -import androidx.compose.foundation.text.overriddenDefaultKeyMapping -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.test.* -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.TextFieldValue -import com.google.common.truth.Truth -import kotlinx.coroutines.runBlocking -import org.junit.After -import org.junit.Rule -import org.junit.Test - - -class SelectionTests { - - @get:Rule - val rule = createComposeRule() - - @After - fun restoreRealDesktopPlatform() { - overriddenDefaultKeyMapping = null - } - - private fun setPlatformDefaultKeyMapping(value: KeyMapping) { - overriddenDefaultKeyMapping = value - } - - suspend fun SemanticsNodeInteraction.waitAndCheck(check: () -> Unit): SemanticsNodeInteraction { - rule.awaitIdle() - check() - return this - } - - @OptIn(ExperimentalTestApi::class) - private fun DesktopPlatform.textFieldSemanticInteraction(initialValue: String = "", semanticNodeContext: suspend SemanticsNodeInteraction.(state: MutableState) -> SemanticsNodeInteraction) = - runBlocking { - setPlatformDefaultKeyMapping(createPlatformDefaultKeyMapping(this@textFieldSemanticInteraction)) - val state = mutableStateOf(TextFieldValue(initialValue)) - - rule.setContent { - BasicTextField( - value = state.value, - onValueChange = { state.value = it }, - modifier = Modifier.testTag("textField") - ) - } - rule.awaitIdle() - val textField = rule.onNodeWithTag("textField") - textField.performMouseInput { - click(Offset(0f, 0f)) - } - - rule.awaitIdle() - textField.assertIsFocused() - - Truth.assertThat(state.value.selection).isEqualTo(TextRange(0, 0)) - - semanticNodeContext.invoke(textField, state) - } - - - @OptIn(ExperimentalTestApi::class) - private fun DesktopPlatform.selectLineStart(keyboardInteraction: KeyInjectionScope.() -> Unit) { - textFieldSemanticInteraction("line 1\nline 2\nline 3\nline 4\nline 5") { state -> - performKeyInput { - pressKey(Key.DirectionRight) - pressKey(Key.DirectionDown) - } - .waitAndCheck { - Truth.assertThat(state.value.selection).isEqualTo(TextRange(8, 8)) - } - .performKeyInput(keyboardInteraction) - .waitAndCheck { - Truth.assertThat(state.value.selection).isEqualTo(TextRange(8, 7)) - } - } - } - - @OptIn(ExperimentalTestApi::class) - private fun DesktopPlatform.selectTextStart(keyboardInteraction: KeyInjectionScope.() -> Unit) { - textFieldSemanticInteraction("line 1\nline 2\nline 3\nline 4\nline 5") { state -> - performKeyInput { - pressKey(Key.DirectionRight) - pressKey(Key.DirectionDown) - }.waitAndCheck { - Truth.assertThat(state.value.selection).isEqualTo(TextRange(8, 8)) - } - performKeyInput(keyboardInteraction) - .waitAndCheck { Truth.assertThat(state.value.selection).isEqualTo(TextRange(8, 0)) } - } - } - - @OptIn(ExperimentalTestApi::class) - private fun DesktopPlatform.selectTextEnd(keyboardInteraction: KeyInjectionScope.() -> Unit) { - textFieldSemanticInteraction("line 1\nline 2\nline 3\nline 4\nline 5") { state -> - performKeyInput { - pressKey(Key.DirectionRight) - pressKey(Key.DirectionDown) - } - .waitAndCheck { - Truth.assertThat(state.value.selection).isEqualTo(TextRange(8, 8)) - } - .performKeyInput(keyboardInteraction) - .waitAndCheck { - Truth.assertThat(state.value.selection).isEqualTo(TextRange(8, 34)) - } - } - } - @OptIn(ExperimentalTestApi::class) - private fun DesktopPlatform.selectLineEnd(keyboardInteraction: KeyInjectionScope.() -> Unit) { - textFieldSemanticInteraction("line 1\nline 2\nline 3\nline 4\nline 5") { state -> - performKeyInput { - pressKey(Key.DirectionRight) - pressKey(Key.DirectionDown) - }.waitAndCheck { - Truth.assertThat(state.value.selection).isEqualTo(TextRange(8, 8)) - } - .performKeyInput(keyboardInteraction) - .waitAndCheck { - Truth.assertThat(state.value.selection).isEqualTo(TextRange(8, 13)) - } - } - } - - @Test - fun `Select till line start with DesktopPlatform-Windows`() = runBlocking { - DesktopPlatform.Windows.selectLineStart { - keyDown(Key.ShiftLeft) - pressKey(Key.MoveHome) - keyUp(Key.ShiftLeft) - } - } - - @Test - fun `Select till text start with DesktopPlatform-Windows`() = runBlocking { - DesktopPlatform.Windows.selectTextStart { - keyDown(Key.CtrlLeft) - keyDown(Key.ShiftLeft) - pressKey(Key.MoveHome) - keyUp(Key.ShiftLeft) - keyUp(Key.CtrlLeft) - } - } - - @Test - fun `Select till line end with DesktopPlatform-Windows`() = runBlocking { - DesktopPlatform.Windows.selectLineEnd { - keyDown(Key.ShiftLeft) - pressKey(Key.MoveEnd) - keyUp(Key.ShiftLeft) - } - } - - @Test - fun `Select till text end with DesktopPlatform-Windows`() = runBlocking { - DesktopPlatform.Windows.selectTextEnd { - keyDown(Key.CtrlLeft) - keyDown(Key.ShiftLeft) - pressKey(Key.MoveEnd) - keyUp(Key.ShiftLeft) - keyUp(Key.CtrlLeft) - } - } - - - @Test - fun `Select till line start with DesktopPlatform-MacOs`() = runBlocking { - DesktopPlatform.MacOS.selectLineStart() { - keyDown(Key.ShiftLeft) - keyDown(Key.MetaLeft) - pressKey(Key.DirectionLeft) - keyUp(Key.ShiftLeft) - keyUp(Key.MetaLeft) - } - } - - @Test - fun `Select till text start with DesktopPlatform-MacOs`() = runBlocking { - DesktopPlatform.MacOS.selectTextStart { - keyDown(Key.ShiftLeft) - pressKey(Key.Home) - keyUp(Key.ShiftLeft) - } - } - - @Test - fun `Select till line end with DesktopPlatform-Macos`() = runBlocking { - DesktopPlatform.MacOS.selectLineEnd { - keyDown(Key.ShiftLeft) - keyDown(Key.MetaLeft) - pressKey(Key.DirectionRight) - keyUp(Key.ShiftLeft) - keyUp(Key.MetaLeft) - } - } - - @Test - fun `Select till text end with DesktopPlatform-Macos`() = runBlocking { - DesktopPlatform.MacOS.selectTextEnd { - keyDown(Key.ShiftLeft) - pressKey(Key.MoveEnd) - keyUp(Key.ShiftLeft) - } - } - - @OptIn(ExperimentalTestApi::class) - private fun DesktopPlatform.deleteAllFromKeyBoard( - initialText: String, deleteAllInteraction: KeyInjectionScope.() -> Unit - ) { - textFieldSemanticInteraction(initialText) { state -> - performKeyInput(deleteAllInteraction).waitAndCheck { Truth.assertThat(state.value.text).isEqualTo("") } - } - } - - - @Test - fun `Delete backwards on an empty line with DesktopPlatform-Windows`() { - DesktopPlatform.Windows.deleteAllFromKeyBoard("") { - keyDown(Key.CtrlLeft) - keyDown(Key.Backspace) - } - } - - @Test - fun `Delete backwards on an empty line with DesktopPlatform-Macos`() { - DesktopPlatform.MacOS.deleteAllFromKeyBoard("") { - keyDown(Key.MetaLeft) - keyDown(Key.Delete) - } - } - - @OptIn(ExperimentalTestApi::class) - private fun DesktopPlatform.selectAllTest(selectAllInteraction: KeyInjectionScope.() -> Unit) { - textFieldSemanticInteraction("Select this text") { state -> - performKeyInput(selectAllInteraction) - .waitAndCheck { - Truth.assertThat(state.value.selection).isEqualTo(TextRange(0, 16)) - } - .performKeyInput { keyDown(Key.Delete) } - .waitAndCheck { - Truth.assertThat(state.value.selection).isEqualTo(TextRange(0, 0)) - Truth.assertThat(state.value.text).isEqualTo("") - } - } - } - - @Test - fun `Select all with DesktopPlatform-Windows`() = runBlocking { - DesktopPlatform.Windows.selectAllTest { - keyDown(Key.CtrlLeft) - pressKey(Key.A) - keyUp(Key.CtrlLeft) - } - } - - @Test - fun `Select all with DesktopPlatform-Macos`() = runBlocking { - DesktopPlatform.MacOS.selectAllTest { - keyDown(Key.MetaLeft) - pressKey(Key.A) - keyUp(Key.MetaLeft) - } - } -} diff --git a/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/text/selection/CommonSelectionTest.kt b/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/text/selection/CommonSelectionTest.kt new file mode 100644 index 0000000000000..800e06144d51f --- /dev/null +++ b/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/text/selection/CommonSelectionTest.kt @@ -0,0 +1,168 @@ +/* + * Copyright 2024 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.foundation.text.selection + +import androidx.compose.foundation.assertThat +import androidx.compose.foundation.isEqualTo +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyMapping +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.ComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.assertIsFocused +import androidx.compose.ui.test.click +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performKeyInput +import androidx.compose.ui.test.performMouseInput +import androidx.compose.ui.test.pressKey +import androidx.compose.ui.test.runComposeUiTest +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import kotlin.test.Test + +internal abstract class CommonSelectionTests( + val keyboardActions: KeyboardActions, + private val keyMapping: KeyMapping +) : KeyboardActions by keyboardActions { + + abstract fun setPlatformDefaultKeyMapping(value: KeyMapping) + + @OptIn(ExperimentalTestApi::class) + private fun textFieldSemanticInteraction( + initialValue: String = "", + runAction: ComposeUiTest.(node: SemanticsNodeInteraction, state: MutableState) -> Unit + ) = + runComposeUiTest { + setPlatformDefaultKeyMapping(keyMapping) + val state = mutableStateOf(TextFieldValue(initialValue)) + + waitForIdle() + + setContent { + BasicTextField( + value = state.value, + onValueChange = { state.value = it }, + modifier = Modifier.testTag("textField") + ) + } + + onNodeWithTag("textField").apply { + performMouseInput { + click(Offset(0f, 0f)) + } + + assertIsFocused() + + assertThat(state.value.selection).isEqualTo(TextRange(0, 0)) + + runAction(this, state) + } + } + + + @OptIn(ExperimentalTestApi::class) + @Test + fun selectLineStart() { + textFieldSemanticInteraction("line 1\nline 2\nline 3\nline 4\nline 5") { node, state -> + node.performKeyInput { + pressKey(Key.DirectionRight) + pressKey(Key.DirectionDown) + } + assertThat(state.value.selection).isEqualTo(TextRange(8, 8)) + + node.performKeyInput { this.selectLineStart() } + + assertThat(state.value.selection).isEqualTo(TextRange(8, 7)) + } + } + + @OptIn(ExperimentalTestApi::class) + @Test + fun selectTextStart() { + textFieldSemanticInteraction("line 1\nline 2\nline 3\nline 4\nline 5") { node, state -> + node.performKeyInput { + pressKey(Key.DirectionRight) + pressKey(Key.DirectionDown) + } + + assertThat(state.value.selection).isEqualTo(TextRange(8, 8)) + + node.performKeyInput { this.selectTextStart() } + + assertThat(state.value.selection).isEqualTo(TextRange(8, 0)) + } + } + + @OptIn(ExperimentalTestApi::class) + @Test + fun selectTextEnd() { + textFieldSemanticInteraction("line 1\nline 2\nline 3\nline 4\nline 5") { node, state -> + node.performKeyInput { + pressKey(Key.DirectionRight) + pressKey(Key.DirectionDown) + } + assertThat(state.value.selection).isEqualTo(TextRange(8, 8)) + + node.performKeyInput { this.selectTextEnd() } + assertThat(state.value.selection).isEqualTo(TextRange(8, 34)) + } + } + + @OptIn(ExperimentalTestApi::class) + @Test + fun selectLineEnd() { + textFieldSemanticInteraction("line 1\nline 2\nline 3\nline 4\nline 5") { node, state -> + node.performKeyInput { + pressKey(Key.DirectionRight) + pressKey(Key.DirectionDown) + } + + assertThat(state.value.selection).isEqualTo(TextRange(8, 8)) + + node.performKeyInput { this.selectLineEnd() } + assertThat(state.value.selection).isEqualTo(TextRange(8, 13)) + } + } + + @OptIn(ExperimentalTestApi::class) + @Test + fun deleteAll() { + textFieldSemanticInteraction("") { node, state -> + node.performKeyInput { this.deleteAll() } + assertThat(state.value.text).isEqualTo("") + } + } + + @OptIn(ExperimentalTestApi::class) + @Test + fun selectAll() { + textFieldSemanticInteraction("Select this text") { node, state -> + node.performKeyInput { this.selectAll() } + assertThat(state.value.selection).isEqualTo(TextRange(0, 16)) + + node.performKeyInput { keyDown(Key.Delete) } + assertThat(state.value.selection).isEqualTo(TextRange(0, 0)) + assertThat(state.value.text).isEqualTo("") + } + } +} diff --git a/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/text/selection/KeyboardActions.kt b/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/text/selection/KeyboardActions.kt new file mode 100644 index 0000000000000..ece9f91226466 --- /dev/null +++ b/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/text/selection/KeyboardActions.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2024 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.foundation.text.selection + +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.test.KeyInjectionScope +import androidx.compose.ui.test.pressKey + +interface KeyboardActions { + fun KeyInjectionScope.selectAll() + fun KeyInjectionScope.selectLineStart() + fun KeyInjectionScope.selectTextStart() + fun KeyInjectionScope.selectLineEnd() + fun KeyInjectionScope.selectTextEnd() + fun KeyInjectionScope.deleteAll() +} + +object DefaultKeyboardActions : KeyboardActions { + override fun KeyInjectionScope.selectAll() { + keyDown(Key.CtrlLeft) + pressKey(Key.A) + keyUp(Key.CtrlLeft) + } + + override fun KeyInjectionScope.selectLineStart() { + keyDown(Key.ShiftLeft) + pressKey(Key.MoveHome) + keyUp(Key.ShiftLeft) + } + + override fun KeyInjectionScope.selectTextStart() { + keyDown(Key.CtrlLeft) + keyDown(Key.ShiftLeft) + pressKey(Key.MoveHome) + keyUp(Key.ShiftLeft) + keyUp(Key.CtrlLeft) + } + + override fun KeyInjectionScope.selectLineEnd() { + keyDown(Key.ShiftLeft) + pressKey(Key.MoveEnd) + keyUp(Key.ShiftLeft) + } + + override fun KeyInjectionScope.selectTextEnd() { + keyDown(Key.CtrlLeft) + keyDown(Key.ShiftLeft) + pressKey(Key.MoveEnd) + keyUp(Key.ShiftLeft) + keyUp(Key.CtrlLeft) + } + + override fun KeyInjectionScope.deleteAll() { + keyDown(Key.CtrlLeft) + keyDown(Key.Backspace) + } + + override fun toString() = "Win" +} + +object MacosKeyboardActions : KeyboardActions { + override fun KeyInjectionScope.selectAll() { + keyDown(Key.MetaLeft) + pressKey(Key.A) + keyUp(Key.MetaLeft) + } + + override fun KeyInjectionScope.selectLineStart() { + keyDown(Key.ShiftLeft) + keyDown(Key.MetaLeft) + pressKey(Key.DirectionLeft) + keyUp(Key.ShiftLeft) + keyUp(Key.MetaLeft) + } + + override fun KeyInjectionScope.selectTextStart() { + keyDown(Key.ShiftLeft) + pressKey(Key.Home) + keyUp(Key.ShiftLeft) + } + + override fun KeyInjectionScope.selectLineEnd() { + keyDown(Key.ShiftLeft) + keyDown(Key.MetaLeft) + pressKey(Key.DirectionRight) + keyUp(Key.ShiftLeft) + keyUp(Key.MetaLeft) + } + + override fun KeyInjectionScope.selectTextEnd() { + keyDown(Key.ShiftLeft) + pressKey(Key.MoveEnd) + keyUp(Key.ShiftLeft) + } + + override fun KeyInjectionScope.deleteAll() { + keyDown(Key.MetaLeft) + keyDown(Key.Delete) + } + + override fun toString() = "MacOS" +} \ No newline at end of file diff --git a/compose/foundation/foundation/src/wasmJsTest/kotlin/androidx.compose.foundation.text.selection/WasmSelectionTests.kt b/compose/foundation/foundation/src/wasmJsTest/kotlin/androidx.compose.foundation.text.selection/WasmSelectionTests.kt new file mode 100644 index 0000000000000..f24eff9d4bbb8 --- /dev/null +++ b/compose/foundation/foundation/src/wasmJsTest/kotlin/androidx.compose.foundation.text.selection/WasmSelectionTests.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024 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.foundation.text.selection + +import androidx.compose.foundation.text.KeyMapping +import androidx.compose.foundation.text.createPlatformDefaultKeyMapping +import org.jetbrains.skiko.hostOs +import kotlin.test.Test +import org.jetbrains.skiko.OS + +private val ResolvedKeyboardActions + get() = when (hostOs) { + OS.MacOS -> MacosKeyboardActions + else -> DefaultKeyboardActions + } + +internal class WasmSelectionTests : + CommonSelectionTests( + ResolvedKeyboardActions, + createPlatformDefaultKeyMapping(hostOs) + ) { + + override fun setPlatformDefaultKeyMapping(value: KeyMapping) { + } +} diff --git a/compose/foundation/foundation/src/webCommonW3C/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.js.kt b/compose/foundation/foundation/src/webCommonW3C/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.js.kt index 36cf957bc6285..ee83d5660887e 100644 --- a/compose/foundation/foundation/src/webCommonW3C/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.js.kt +++ b/compose/foundation/foundation/src/webCommonW3C/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.js.kt @@ -18,13 +18,12 @@ package androidx.compose.foundation.text import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.isMetaPressed - -import org.jetbrains.skiko.* +import org.jetbrains.skiko.SkikoKeyboardEventKind +import org.jetbrains.skiko.SkikoPlatformKeyboardEvent actual val KeyEvent.isTypedEvent: Boolean get() = nativeKeyEvent.kind == SkikoKeyboardEventKind.DOWN && !isMetaPressed && nativeKeyEvent.platform?.isPrintable() == true - private fun SkikoPlatformKeyboardEvent.isPrintable(): Boolean { - return key.firstOrNull()?.toChar()?.toString() == key + return key.firstOrNull()?.toString() == key } diff --git a/compose/ui/ui-test/src/wasmJsMain/kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt b/compose/ui/ui-test/src/wasmJsMain/kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt index f2ec9aa63bbbc..317f04e2d237a 100644 --- a/compose/ui/ui-test/src/wasmJsMain/kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt +++ b/compose/ui/ui-test/src/wasmJsMain/kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt @@ -13,13 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@file:OptIn(ExperimentalTestApi::class) package androidx.compose.ui.test import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.KeyEventType +import org.jetbrains.skiko.SkikoInputModifiers +import org.jetbrains.skiko.SkikoKey +import org.jetbrains.skiko.SkikoKeyboardEvent +import org.jetbrains.skiko.SkikoKeyboardEventKind +import org.w3c.dom.events.InputEvent +import org.w3c.dom.events.KeyboardEvent +import org.w3c.dom.events.KeyboardEventInit /** * The [KeyEvent] is usually created by the system. This function creates an instance of @@ -27,6 +33,62 @@ import androidx.compose.ui.input.key.KeyEventType */ internal actual fun keyEvent( key: Key, keyEventType: KeyEventType, modifiers: Int -): KeyEvent = TODO() +): KeyEvent { + val nativeCode = key.keyCode.toInt() -internal actual fun Int.updatedKeyboardModifiers(key: Key, down: Boolean): Int = this // TODO: implement updatedKeyboardModifiers + val kind = when (keyEventType) { + KeyEventType.KeyUp -> SkikoKeyboardEventKind.UP + KeyEventType.KeyDown -> SkikoKeyboardEventKind.DOWN + else -> SkikoKeyboardEventKind.UNKNOWN + } + + return KeyEvent( + SkikoKeyboardEvent( + kind = kind, + key = SkikoKey.valueOf(nativeCode), + modifiers = SkikoInputModifiers(modifiers), + platform = KeyboardEvent(when (kind) { + SkikoKeyboardEventKind.DOWN -> "keydown" + SkikoKeyboardEventKind.UP -> "keyup" + else -> "keypress" + }, KeyboardEventInit( + key= if (key.isPrintable()) nativeCode.toChar().toString() else "Dead"), + ) + ) + ) +} + +private fun Key.isPrintable(): Boolean { + // Basically this is what we've tried to avoid in isTypedEvent implementation, but in tests it's totally fine + // Just add any non-printable key you need for tests here + return when(this) { + Key.AltLeft -> false + Key.MetaLeft -> false + Key.ShiftLeft -> false + Key.CtrlLeft -> false + Key.Delete -> false + Key.DirectionLeft -> false + Key.DirectionRight -> false + Key.DirectionDown -> false + Key.DirectionUp -> false + Key.Home -> false + Key.MoveEnd -> false + else -> true + } +} + +internal actual fun Int.updatedKeyboardModifiers(key: Key, down: Boolean): Int { + val mask = when (key) { + Key.ShiftLeft, Key.ShiftRight -> SkikoInputModifiers.SHIFT.value + Key.CtrlLeft, Key.CtrlRight -> SkikoInputModifiers.CONTROL.value + Key.MetaLeft, Key.MetaRight -> SkikoInputModifiers.META.value + Key.AltLeft, Key.AltRight -> SkikoInputModifiers.ALT.value + else -> null + } + + return if (mask != null) { + if (down) this or mask else this xor mask + } else { + this + } +} \ No newline at end of file From 8871ddb3ecce331209c966740b7c936fda5344f3 Mon Sep 17 00:00:00 2001 From: Shagen Ogandzhanian Date: Wed, 28 Feb 2024 10:26:21 +0100 Subject: [PATCH 2/4] Introduce helper NonPrintableKeys private set --- .../ui/test/InputDispatcher.wasmMain.kt | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/compose/ui/ui-test/src/wasmJsMain/kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt b/compose/ui/ui-test/src/wasmJsMain/kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt index 317f04e2d237a..7e0e1baf3cfe7 100644 --- a/compose/ui/ui-test/src/wasmJsMain/kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt +++ b/compose/ui/ui-test/src/wasmJsMain/kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt @@ -52,29 +52,30 @@ internal actual fun keyEvent( SkikoKeyboardEventKind.UP -> "keyup" else -> "keypress" }, KeyboardEventInit( - key= if (key.isPrintable()) nativeCode.toChar().toString() else "Dead"), + key= if (key.isPrintable()) nativeCode.toChar().toString() else "Unknown"), ) ) ) } +// Basically this is what we've tried to avoid in isTypedEvent implementation, but in tests it's totally fine +// Just add any non-printable key you need for tests here +private val NonPrintableKeys = setOf( + Key.AltLeft, + Key.MetaLeft, + Key.ShiftLeft, + Key.CtrlLeft, + Key.Delete, + Key.DirectionLeft, + Key.DirectionRight, + Key.DirectionDown, + Key.DirectionUp, + Key.Home, + Key.MoveEnd +) + private fun Key.isPrintable(): Boolean { - // Basically this is what we've tried to avoid in isTypedEvent implementation, but in tests it's totally fine - // Just add any non-printable key you need for tests here - return when(this) { - Key.AltLeft -> false - Key.MetaLeft -> false - Key.ShiftLeft -> false - Key.CtrlLeft -> false - Key.Delete -> false - Key.DirectionLeft -> false - Key.DirectionRight -> false - Key.DirectionDown -> false - Key.DirectionUp -> false - Key.Home -> false - Key.MoveEnd -> false - else -> true - } + return !NonPrintableKeys.contains(this) } internal actual fun Int.updatedKeyboardModifiers(key: Key, down: Boolean): Int { From af4fd3943f63da42d234439f73de358ad7b090a4 Mon Sep 17 00:00:00 2001 From: Shagen Ogandzhanian Date: Wed, 28 Feb 2024 10:31:23 +0100 Subject: [PATCH 3/4] Remove redundant org.w3c.dom.events.InputEvent import --- .../kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/compose/ui/ui-test/src/wasmJsMain/kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt b/compose/ui/ui-test/src/wasmJsMain/kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt index 7e0e1baf3cfe7..ff9885875101e 100644 --- a/compose/ui/ui-test/src/wasmJsMain/kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt +++ b/compose/ui/ui-test/src/wasmJsMain/kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt @@ -23,7 +23,6 @@ import org.jetbrains.skiko.SkikoInputModifiers import org.jetbrains.skiko.SkikoKey import org.jetbrains.skiko.SkikoKeyboardEvent import org.jetbrains.skiko.SkikoKeyboardEventKind -import org.w3c.dom.events.InputEvent import org.w3c.dom.events.KeyboardEvent import org.w3c.dom.events.KeyboardEventInit From 38f74b228d54b9a8a102522dbc45bc46a05c49a8 Mon Sep 17 00:00:00 2001 From: Shagen Ogandzhanian Date: Wed, 28 Feb 2024 13:40:05 +0100 Subject: [PATCH 4/4] Minor formatting --- .../compose/ui/test/InputDispatcher.wasmMain.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/compose/ui/ui-test/src/wasmJsMain/kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt b/compose/ui/ui-test/src/wasmJsMain/kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt index ff9885875101e..71b069d849bcb 100644 --- a/compose/ui/ui-test/src/wasmJsMain/kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt +++ b/compose/ui/ui-test/src/wasmJsMain/kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt @@ -46,12 +46,14 @@ internal actual fun keyEvent( kind = kind, key = SkikoKey.valueOf(nativeCode), modifiers = SkikoInputModifiers(modifiers), - platform = KeyboardEvent(when (kind) { - SkikoKeyboardEventKind.DOWN -> "keydown" - SkikoKeyboardEventKind.UP -> "keyup" - else -> "keypress" - }, KeyboardEventInit( - key= if (key.isPrintable()) nativeCode.toChar().toString() else "Unknown"), + platform = KeyboardEvent( + when (kind) { + SkikoKeyboardEventKind.DOWN -> "keydown" + SkikoKeyboardEventKind.UP -> "keyup" + else -> "keypress" + }, KeyboardEventInit( + key = if (key.isPrintable()) nativeCode.toChar().toString() else "Unknown" + ) ) ) )