From 5f7e07dd3c3e29e5d863a1eb4c500c6c7de32e45 Mon Sep 17 00:00:00 2001 From: Shagen Ogandzhanian Date: Thu, 19 Sep 2024 15:28:36 +0200 Subject: [PATCH 1/9] Introduce MinimalChannelTest --- .../compose/ui/input/MinimalChannelTest.kt | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/MinimalChannelTest.kt diff --git a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/MinimalChannelTest.kt b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/MinimalChannelTest.kt new file mode 100644 index 0000000000000..f4b97a9075ea0 --- /dev/null +++ b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/MinimalChannelTest.kt @@ -0,0 +1,40 @@ +/* + * 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.ui.input + +import androidx.compose.ui.sendFromScope +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlinx.browser.window +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.test.runTest + +class MinimalChannelTest { + + @Test + fun ping() = runTest { + val minimalChannel = Channel(1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + + window.setTimeout({ + minimalChannel.sendFromScope(42) + null + }, 3000) + + assertEquals(42, minimalChannel.receive()) + } +} \ No newline at end of file From 8a462f61235a5fb818f091e9fb6ca670a29723aa Mon Sep 17 00:00:00 2001 From: Shagen Ogandzhanian Date: Thu, 19 Sep 2024 16:50:15 +0200 Subject: [PATCH 2/9] Introduce MinimalChannelTest::domEventChannelTest --- .../compose/ui/input/MinimalChannelTest.kt | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/MinimalChannelTest.kt b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/MinimalChannelTest.kt index f4b97a9075ea0..8edb255083081 100644 --- a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/MinimalChannelTest.kt +++ b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/MinimalChannelTest.kt @@ -16,9 +16,11 @@ package androidx.compose.ui.input +import androidx.compose.ui.events.createMouseEvent import androidx.compose.ui.sendFromScope import kotlin.test.Test import kotlin.test.assertEquals +import kotlinx.browser.document import kotlinx.browser.window import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel @@ -27,7 +29,7 @@ import kotlinx.coroutines.test.runTest class MinimalChannelTest { @Test - fun ping() = runTest { + fun pureChannelTest() = runTest { val minimalChannel = Channel(1, onBufferOverflow = BufferOverflow.DROP_OLDEST) window.setTimeout({ @@ -37,4 +39,18 @@ class MinimalChannelTest { assertEquals(42, minimalChannel.receive()) } + + @Test + fun domEventChannelTest() = runTest { + val minimalChannel = Channel(1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + + val el = document.createElement("div") + el.addEventListener("mousedown", { + minimalChannel.sendFromScope(42) + }) + + el.dispatchEvent(createMouseEvent("mousedown")) + + assertEquals(42, minimalChannel.receive()) + } } \ No newline at end of file From c8fceeca7bb3975d8940c02d80be516ad0cdaea6 Mon Sep 17 00:00:00 2001 From: Shagen Ogandzhanian Date: Thu, 12 Sep 2024 13:26:36 +0200 Subject: [PATCH 3/9] [web] Introduce ScrollTests for checking DOM interactions --- .../androidx/compose/ui/input/ScrollTests.kt | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/ScrollTests.kt diff --git a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/ScrollTests.kt b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/ScrollTests.kt new file mode 100644 index 0000000000000..132d0f6517241 --- /dev/null +++ b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/ScrollTests.kt @@ -0,0 +1,121 @@ +/* + * 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.ui.input + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.OnCanvasTests +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionOnScreen +import androidx.compose.ui.sendFromScope +import androidx.compose.ui.unit.dp +import kotlin.test.Test +import kotlin.test.assertTrue +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.test.runTest +import org.w3c.dom.events.WheelEvent +import org.w3c.dom.events.WheelEventInit + +class ScrollTests : OnCanvasTests { + @Test + fun scrollTillEnd() = runTest { + val firstRowScrollPositionResolved = Channel( + 1, onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + + val lastRowScrollPositionResolved = Channel( + 1, onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + + createComposeWindow { + Column( + modifier = Modifier.fillMaxWidth().height(300.dp).wrapContentSize(Alignment.Center) + .verticalScroll( + rememberScrollState() + ) + ) { + Box( + modifier = Modifier.height(100.dp).fillMaxWidth().background(Color(237, 40, 57)) + .onGloballyPositioned { coordinates -> + val screenPosition = coordinates.positionOnScreen() + if (screenPosition == Offset(0f, -200f)) { + firstRowScrollPositionResolved.sendFromScope(true) + } + } + ) + Box( + modifier = Modifier.height(100.dp).fillMaxWidth().background(Color(255, 88, 0)) + ) + Box( + modifier = Modifier.height(100.dp).fillMaxWidth().background(Color(255, 211, 0)) + ) + Box( + modifier = Modifier.height(100.dp).fillMaxWidth().background(Color(0, 173, 131)) + ) + Box( + modifier = Modifier.height(100.dp).fillMaxWidth() + .background(Color(190, 219, 237)) + ) + Box( + modifier = Modifier.height(100.dp).fillMaxWidth() + .background(Color(31, 117, 254)) + ) + Box( + modifier = Modifier.height(100.dp).fillMaxWidth().background(Color(143, 0, 255)).onGloballyPositioned { coordinates -> + val screenPosition = coordinates.positionOnScreen() + if (screenPosition == Offset(0f, 1000f)) { + lastRowScrollPositionResolved.sendFromScope(true) + } + } + ) + } + } + + dispatchEvents(createWheelEvent(clientX = 100, clientY = 100, deltaX = 0.0, deltaY = 200.0)) + + assertTrue(firstRowScrollPositionResolved.receive(), "first row scroll position is not resolved") + assertTrue(lastRowScrollPositionResolved.receive(), "last row scroll position is not resolved") + } +} + + +private fun createWheelEvent( + deltaX: Double, + deltaY: Double, + clientX: Int, + clientY: Int +): WheelEvent { + return WheelEvent( + "wheel", WheelEventInit( + deltaX = deltaX, + deltaY = deltaY, + clientX = clientX, + clientY = clientY + ) + ) +} \ No newline at end of file From d6abd0a0753b8d224958b5cf6219722bdb1fa1a1 Mon Sep 17 00:00:00 2001 From: Shagen Ogandzhanian Date: Fri, 20 Sep 2024 10:00:59 +0200 Subject: [PATCH 4/9] Debug in MinimalChannelTest::domEventChannelTest --- .../kotlin/androidx/compose/ui/input/MinimalChannelTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/MinimalChannelTest.kt b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/MinimalChannelTest.kt index 8edb255083081..e52894b06060c 100644 --- a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/MinimalChannelTest.kt +++ b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/MinimalChannelTest.kt @@ -33,6 +33,7 @@ class MinimalChannelTest { val minimalChannel = Channel(1, onBufferOverflow = BufferOverflow.DROP_OLDEST) window.setTimeout({ + println("[pureChannelTest] sending from scope") minimalChannel.sendFromScope(42) null }, 3000) @@ -46,6 +47,7 @@ class MinimalChannelTest { val el = document.createElement("div") el.addEventListener("mousedown", { + println("[domEventChannelTest] sending on mouseDown") minimalChannel.sendFromScope(42) }) From 5a4ec4d6fd0c9610b5f3b930eacff83a8604d68f Mon Sep 17 00:00:00 2001 From: Shagen Ogandzhanian Date: Fri, 20 Sep 2024 10:48:28 +0200 Subject: [PATCH 5/9] REVERT run tests in Firefox --- ...droidXComposeMultiplatformExtensionImpl.kt | 3 +- .../compose/ui/input/TextInputTests.kt | 99 +++++++++++++++++++ mpp/karma.config.d/wasm/config.js | 16 +-- 3 files changed, 109 insertions(+), 9 deletions(-) diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeMultiplatformExtensionImpl.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeMultiplatformExtensionImpl.kt index 961e34d32f700..de0ad23df1610 100644 --- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeMultiplatformExtensionImpl.kt +++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeMultiplatformExtensionImpl.kt @@ -95,7 +95,8 @@ open class AndroidXComposeMultiplatformExtensionImpl @Inject constructor( browser { testTask { it.useKarma { - useChrome() + //useChrome() + useFirefox() useConfigDirectory( project.rootProject.projectDir.resolve("mpp/karma.config.d/wasm") ) diff --git a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/TextInputTests.kt b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/TextInputTests.kt index 7460ab1ecd4eb..57eaf9294e4c2 100644 --- a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/TextInputTests.kt +++ b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/TextInputTests.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.events.keyDownEvent import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.sendFromScope +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -135,4 +136,102 @@ class TextInputTests : OnCanvasTests { assertEquals("step1step5", textInputChannel.receive()) } + + @Test + fun keyboardEventPassedToTextFieldMinimal() = runTest { + + val textInputChannel = Channel( + 1, onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + + val (firstFocusRequester, secondFocusRequester) = FocusRequester.createRefs() + + createComposeWindow { + TextField( + value = "", + onValueChange = { value -> + textInputChannel.sendFromScope(value) + }, + modifier = Modifier.focusRequester(firstFocusRequester) + ) + + TextField( + value = "", + onValueChange = { value -> + textInputChannel.sendFromScope(value) + }, + modifier = Modifier.focusRequester(secondFocusRequester) + ) + + SideEffect { + secondFocusRequester.requestFocus() + firstFocusRequester.requestFocus() + } + } + + assertNull(document.querySelector("textarea")) + + dispatchEvents( + keyDownEvent("s"), + keyDownEvent("t"), + keyDownEvent("e"), + keyDownEvent("p"), + keyDownEvent("1") + ) + + + assertEquals("step1", textInputChannel.receive()) + assertNull(document.querySelector("textarea")) + + // trigger virtual keyboard + dispatchEvents(createTouchEvent("touchstart")) + secondFocusRequester.requestFocus() + + assertNotNull(document.querySelector("textarea")) + + dispatchEvents( + keyDownEvent("s"), + keyDownEvent("t"), + keyDownEvent("e"), + keyDownEvent("p"), + keyDownEvent("2") + ) + + assertEquals("step2", textInputChannel.receive()) + + val backingField = document.querySelector("textarea")!! + + dispatchEvents( + keyDownEvent("s"), + keyDownEvent("t"), + keyDownEvent("e"), + keyDownEvent("p"), + keyDownEvent("3") + ) + + assertEquals("step2step3", textInputChannel.receive()) + + backingField.dispatchEvent(InputEvent("input", InputEventInit("insertText", "step4XX"))) + + assertEquals("step2step3step4XX", textInputChannel.receive()) + + backingField.dispatchEvent(InputEvent("input", InputEventInit("deleteContentBackward", ""))) + backingField.dispatchEvent(InputEvent("input", InputEventInit("deleteContentBackward", ""))) + assertEquals("step2step3step4", textInputChannel.receive()) + + // trigger hardware keyboard + dispatchEvents(createMouseEvent("mousedown")) + firstFocusRequester.requestFocus() + + dispatchEvents( + keyDownEvent("s"), + keyDownEvent("t"), + keyDownEvent("e"), + keyDownEvent("p"), + keyDownEvent("5") + ) + + assertEquals("step1step5", textInputChannel.receive()) + } + } \ No newline at end of file diff --git a/mpp/karma.config.d/wasm/config.js b/mpp/karma.config.d/wasm/config.js index ef889918d71a3..8e9be830ee1ef 100644 --- a/mpp/karma.config.d/wasm/config.js +++ b/mpp/karma.config.d/wasm/config.js @@ -61,11 +61,11 @@ config.plugins.push(KarmaWebpackOutputPlugin); config.frameworks.push("webpack-output"); -config.customLaunchers = { - ChromeForComposeTests: { - base: "Chrome", - flags: ["--disable-search-engine-choice-screen"] - } -} - -config.browsers = ["ChromeForComposeTests"] \ No newline at end of file +// config.customLaunchers = { +// ChromeForComposeTests: { +// base: "Chrome", +// flags: ["--disable-search-engine-choice-screen"] +// } +// } +// +// config.browsers = ["ChromeForComposeTests"] \ No newline at end of file From 637bc91bf21a67977c29d2606a30c9caada876ea Mon Sep 17 00:00:00 2001 From: Shagen Ogandzhanian Date: Fri, 20 Sep 2024 11:02:12 +0200 Subject: [PATCH 6/9] Smaller minimal example --- .../compose/ui/input/TextInputTests.kt | 78 +------------------ 1 file changed, 3 insertions(+), 75 deletions(-) diff --git a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/TextInputTests.kt b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/TextInputTests.kt index 57eaf9294e4c2..dd02d779d726b 100644 --- a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/TextInputTests.kt +++ b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/TextInputTests.kt @@ -139,12 +139,11 @@ class TextInputTests : OnCanvasTests { @Test fun keyboardEventPassedToTextFieldMinimal() = runTest { - val textInputChannel = Channel( 1, onBufferOverflow = BufferOverflow.DROP_OLDEST ) - val (firstFocusRequester, secondFocusRequester) = FocusRequester.createRefs() + val (firstFocusRequester) = FocusRequester.createRefs() createComposeWindow { TextField( @@ -154,84 +153,13 @@ class TextInputTests : OnCanvasTests { }, modifier = Modifier.focusRequester(firstFocusRequester) ) - - TextField( - value = "", - onValueChange = { value -> - textInputChannel.sendFromScope(value) - }, - modifier = Modifier.focusRequester(secondFocusRequester) - ) - - SideEffect { - secondFocusRequester.requestFocus() - firstFocusRequester.requestFocus() - } } - assertNull(document.querySelector("textarea")) - dispatchEvents( - keyDownEvent("s"), - keyDownEvent("t"), - keyDownEvent("e"), - keyDownEvent("p"), - keyDownEvent("1") + keyDownEvent("X"), ) - - assertEquals("step1", textInputChannel.receive()) - assertNull(document.querySelector("textarea")) - - // trigger virtual keyboard - dispatchEvents(createTouchEvent("touchstart")) - secondFocusRequester.requestFocus() - - assertNotNull(document.querySelector("textarea")) - - dispatchEvents( - keyDownEvent("s"), - keyDownEvent("t"), - keyDownEvent("e"), - keyDownEvent("p"), - keyDownEvent("2") - ) - - assertEquals("step2", textInputChannel.receive()) - - val backingField = document.querySelector("textarea")!! - - dispatchEvents( - keyDownEvent("s"), - keyDownEvent("t"), - keyDownEvent("e"), - keyDownEvent("p"), - keyDownEvent("3") - ) - - assertEquals("step2step3", textInputChannel.receive()) - - backingField.dispatchEvent(InputEvent("input", InputEventInit("insertText", "step4XX"))) - - assertEquals("step2step3step4XX", textInputChannel.receive()) - - backingField.dispatchEvent(InputEvent("input", InputEventInit("deleteContentBackward", ""))) - backingField.dispatchEvent(InputEvent("input", InputEventInit("deleteContentBackward", ""))) - assertEquals("step2step3step4", textInputChannel.receive()) - - // trigger hardware keyboard - dispatchEvents(createMouseEvent("mousedown")) - firstFocusRequester.requestFocus() - - dispatchEvents( - keyDownEvent("s"), - keyDownEvent("t"), - keyDownEvent("e"), - keyDownEvent("p"), - keyDownEvent("5") - ) - - assertEquals("step1step5", textInputChannel.receive()) + assertEquals("X", textInputChannel.receive()) } } \ No newline at end of file From 9902c7cd92790a65dc683df7b2f37550fafbb15c Mon Sep 17 00:00:00 2001 From: Shagen Ogandzhanian Date: Fri, 20 Sep 2024 15:14:28 +0200 Subject: [PATCH 7/9] in webTest leave MinimalChannelTest only --- .../compose/ui/SelectionContainerTests.kt | 220 ------------------ .../kotlin/androidx/compose/ui/TextTests.kt | 78 ------- .../compose/ui/input/IsTypedEventTests.kt | 129 ---------- .../ui/input/KeyEventConversionTests.kt | 214 ----------------- .../androidx/compose/ui/input/ScrollTests.kt | 121 ---------- .../compose/ui/input/TextInputTests.kt | 165 ------------- .../ui/window/ComposeWindowLifecycleTest.kt | 70 ------ .../compose/ui/window/KeyEventTests.kt | 128 ---------- .../compose/ui/window/MouseEventsTest.kt | 151 ------------ .../compose/ui/window/PreventDefaultTest.kt | 76 ------ 10 files changed, 1352 deletions(-) delete mode 100644 compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/SelectionContainerTests.kt delete mode 100644 compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/TextTests.kt delete mode 100644 compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/IsTypedEventTests.kt delete mode 100644 compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/KeyEventConversionTests.kt delete mode 100644 compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/ScrollTests.kt delete mode 100644 compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/TextInputTests.kt delete mode 100644 compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/window/ComposeWindowLifecycleTest.kt delete mode 100644 compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/window/KeyEventTests.kt delete mode 100644 compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/window/MouseEventsTest.kt delete mode 100644 compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/window/PreventDefaultTest.kt diff --git a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/SelectionContainerTests.kt b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/SelectionContainerTests.kt deleted file mode 100644 index 1065a3e46e1d0..0000000000000 --- a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/SelectionContainerTests.kt +++ /dev/null @@ -1,220 +0,0 @@ -/* - * 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. - */ - -@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") - -package androidx.compose.ui - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.text.selection.Selection -import androidx.compose.material.Text -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalViewConfiguration -import androidx.compose.ui.platform.ViewConfiguration -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.delay -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.withContext -import org.w3c.dom.HTMLCanvasElement -import org.w3c.dom.events.MouseEvent -import org.w3c.dom.events.MouseEventInit - -class SelectionContainerTests : OnCanvasTests { - - private fun HTMLCanvasElement.doClick() { - dispatchEvent(MouseEvent("mousedown", MouseEventInit(5, 5, 5, 5, buttons = 1, button = 1))) - dispatchEvent(MouseEvent("mouseup", MouseEventInit(5, 5, 5, 5, buttons = 0, button = 1))) - } - - @Test - fun canSelectOneWordUsingDoubleClick() = runTest { - val syncChannel = Channel( - 1, onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - - var viewConfiguration: ViewConfiguration? = null - - createComposeWindow { - var selection by remember { mutableStateOf(null) } - - androidx.compose.foundation.text.selection.SelectionContainer( - modifier = Modifier.fillMaxSize(), - selection = selection, - onSelectionChange = { - selection = it - syncChannel.sendFromScope(it) - }, - children = { - Column { - Text("qwerty uiopasdfghjklzxcvbnm") - Text("mnbvcxzlkjhgfdsapoiuytrewq") - } - - viewConfiguration = LocalViewConfiguration.current - } - ) - } - - val canvas = getCanvas() - canvas.dispatchEvent(MouseEvent("mouseenter")) - - // single click - no selection expected - canvas.doClick() - - var selection = syncChannel.receive() - assertFalse(selection.exists()) - - // delay to prevent interpreting next single click as a second click of the previous click, - // so we make sure it won't appear as a double click - withContext(Dispatchers.Default) { - delay(viewConfiguration!!.doubleTapTimeoutMillis) - } - - // now double click: - canvas.doClick() - canvas.doClick() - - selection = syncChannel.receive() - assertTrue(selection.exists()) - assertEquals(0, selection!!.start.offset) - assertEquals(6, selection!!.end.offset) - - withContext(Dispatchers.Default) { - delay(viewConfiguration!!.doubleTapTimeoutMillis) - } - // reset selection by clicking - canvas.doClick() - selection = syncChannel.receive() - assertFalse(selection.exists()) - } - - @Test - fun canSelectOneLineUsingTripleClick() = runTest { - val syncChannel = Channel( - 1, onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - - var viewConfiguration: ViewConfiguration? = null - - createComposeWindow { - var selection by remember { mutableStateOf(null) } - - androidx.compose.foundation.text.selection.SelectionContainer( - modifier = Modifier.fillMaxSize(), - selection = selection, - onSelectionChange = { - selection = it - syncChannel.sendFromScope(it) - }, - children = { - Column { - Text("012345 uiopasdfghjklzxcvbnm") - Text("mnbvcxzlkjhgfdsapoiuytrewq") - } - - viewConfiguration = LocalViewConfiguration.current - } - ) - } - - val canvas = getCanvas() - canvas.dispatchEvent(MouseEvent("mouseenter")) - - // triple click - canvas.doClick() - canvas.doClick() - canvas.doClick() - - var selection = syncChannel.receive() - assertTrue(selection.exists()) - assertEquals(0, selection!!.start.offset) - assertEquals(27, selection!!.end.offset) - - withContext(Dispatchers.Default) { - delay(viewConfiguration!!.doubleTapTimeoutMillis) - } - // reset selection by clicking - canvas.doClick() - selection = syncChannel.receive() - assertFalse(selection.exists()) - } - - @Test - fun twoSingleClicksDoNotTriggerSelection() = runTest { - val syncChannel = Channel( - 5, onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - var selectionCallbackCounter = 0 - - var viewConfiguration: ViewConfiguration? = null - - createComposeWindow { - var selection by remember { mutableStateOf(null) } - - androidx.compose.foundation.text.selection.SelectionContainer( - modifier = Modifier.fillMaxSize(), - selection = selection, - onSelectionChange = { - selection = it - syncChannel.sendFromScope(it) - selectionCallbackCounter++ - }, - children = { - Column { - Text("asdfgh uiopasdfghjklzxcvbnm") - Text("mnbvcxzlkjhgfdsapoiuytrewq") - } - - viewConfiguration = LocalViewConfiguration.current - } - ) - } - - val canvas = getCanvas() - canvas.dispatchEvent(MouseEvent("mouseenter")) - - // first single click - canvas.doClick() - // pause to ensure no double-click - withContext(Dispatchers.Default) { - delay(viewConfiguration!!.doubleTapTimeoutMillis) - } - // second single click - canvas.doClick() - withContext(Dispatchers.Default) { - delay(viewConfiguration!!.doubleTapTimeoutMillis) - } - - repeat(selectionCallbackCounter) { - val selection = syncChannel.receive() - val actualSelectionLength = - (selection?.end?.offset ?: 0) - (selection?.start?.offset ?: 0) - assertEquals(0, actualSelectionLength) - } - } -} - -private fun Selection?.exists() = (this !== null) && !this.toTextRange().collapsed diff --git a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/TextTests.kt b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/TextTests.kt deleted file mode 100644 index 4c8cb02a8daa0..0000000000000 --- a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/TextTests.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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.ui - -import androidx.compose.foundation.layout.Row -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.ui.layout.FirstBaseline -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity -import kotlin.math.abs -import kotlin.test.Test -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.test.runTest - -class TextTests : OnCanvasTests { - - companion object { - private fun assertApproximatelyEqual( - expected: Float, - actual: Float, - tolerance: Float = 1f - ) { - if (abs(expected - actual) > tolerance) { - throw AssertionError("Expected $expected but got $actual. Difference is more than the allowed delta $tolerance") - } - } - } - - @Test - // https://github.com/JetBrains/compose-multiplatform/issues/4078 - fun baselineShouldBeNotZero() = runTest { - val headingOnPositioned = Channel(10) - val subtitleOnPositioned = Channel(10) - - createComposeWindow { - val density = LocalDensity.current.density - Row { - Text( - "Heading", - modifier = Modifier.alignByBaseline() - .onGloballyPositioned { - headingOnPositioned.sendFromScope(it[FirstBaseline] / density) - }, - style = MaterialTheme.typography.h4 - ) - Text( - " — Subtitle", - modifier = Modifier.alignByBaseline() - .onGloballyPositioned { - subtitleOnPositioned.sendFromScope(it[FirstBaseline] / density) - }, - style = MaterialTheme.typography.subtitle1 - ) - } - } - - val headingAlignment = headingOnPositioned.receive() - val subtitleAlignment = subtitleOnPositioned.receive() - - assertApproximatelyEqual(29f, headingAlignment) - assertApproximatelyEqual(19f, subtitleAlignment) - } -} \ No newline at end of file diff --git a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/IsTypedEventTests.kt b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/IsTypedEventTests.kt deleted file mode 100644 index bb687c2078b68..0000000000000 --- a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/IsTypedEventTests.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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.ui.input - -import androidx.compose.foundation.text.isTypedEvent -import androidx.compose.ui.events.keyDownEvent -import androidx.compose.ui.input.key.toComposeEvent -import kotlin.test.Test -import kotlin.test.assertFalse -import kotlin.test.assertTrue -import org.w3c.dom.events.KeyboardEvent - -class IsTypedEventTests { - private fun KeyboardEvent.assertIsTyped(message: String? = null) { - val composeEvent = toComposeEvent() - assertTrue(composeEvent.isTypedEvent, message ?: "event ${composeEvent} supposed to be typed but actually is not") - } - - private fun KeyboardEvent.assertIsNotTyped(message: String? = null) { - val composeEvent = toComposeEvent() - assertFalse(composeEvent.isTypedEvent, message ?: "event ${composeEvent} not supposed to be typed but actually is") - } - - @Test - fun charsAreTyped() { - val chars = listOf( - "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", - "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", - - "a", "б", "в", "г", "д", "е", "ё", "ж", "з", "и", "й", "к", - "л", "м", "н", "о", "п", "р", "с", "т", "у", "ф", "х", "ц", - "ш", "щ", "ь", "ъ", "э", "ю", "я" - ) - - chars.forEach { char -> keyDownEvent(char).assertIsTyped() } - } - - @Test - fun shortcutsAreNotTyped() { - val keyDownEvents = listOf( - keyDownEvent("c", metaKey = true, ctrlKey = true), - keyDownEvent("p", metaKey = true, ctrlKey = true), - keyDownEvent("v", metaKey = true, ctrlKey = true) - ) - - keyDownEvents.forEach { event -> event.assertIsNotTyped() } - } - - @Test - fun shortcutsWithCtrlOnlyAreNotTyped() { - val keyDownEvents = listOf( - keyDownEvent("c", metaKey = false, ctrlKey = true), - keyDownEvent("p", metaKey = false, ctrlKey = true), - keyDownEvent("v", metaKey = false, ctrlKey = true) - ) - - keyDownEvents.forEach { event -> event.assertIsNotTyped() } - } - - @Test - fun shortcutsWithMetaOnlyAreNotTyped() { - val keyDownEvents = listOf( - keyDownEvent("c", metaKey = true, ctrlKey = false), - keyDownEvent("p", metaKey = true, ctrlKey = false), - keyDownEvent("v", metaKey = true, ctrlKey = false) - ) - - keyDownEvents.forEach { event -> event.assertIsNotTyped() } - } - - @Test - fun altProducesATypedEvent() { - val keyDownEvents = listOf( - keyDownEvent("c", altKey = true), - keyDownEvent("p", altKey = true), - keyDownEvent("v", altKey = true) - ) - - keyDownEvents.forEach { event -> event.assertIsTyped() } - } - - @Test - fun functionalsAreNotTyped() { - val keyDownEvents = listOf( - keyDownEvent("Backspace", code="Backspace"), - keyDownEvent("Clear", code="Backspace"), - keyDownEvent("Home", code="Home"), - keyDownEvent("End", code="End"), - keyDownEvent("PageUp", code="PageUp"), - keyDownEvent("PageDown", code="PageDown"), - keyDownEvent("F1", code="F1"), - keyDownEvent("F2", code="F2"), - keyDownEvent("F3", code="F3"), - keyDownEvent("F4", code="F4"), - keyDownEvent("F5", code="F5"), - keyDownEvent("F6", code="F6"), - keyDownEvent("F7", code="F7"), - keyDownEvent("F8", code="F8"), - keyDownEvent("F9", code="F9"), - keyDownEvent("F10", code="F10"), - keyDownEvent("F11", code="F11"), - keyDownEvent("F12", code="F12"), - keyDownEvent("F13", code="F13"), - keyDownEvent("F14", code="F14"), - keyDownEvent("F15", code="F15"), - keyDownEvent("F16", code="F16"), - keyDownEvent("F17", code="F17"), - keyDownEvent("F18", code="F18"), - keyDownEvent("F19", code="F19"), - ) - - keyDownEvents.forEach { event -> event.assertIsNotTyped() } - } - -} \ No newline at end of file diff --git a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/KeyEventConversionTests.kt b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/KeyEventConversionTests.kt deleted file mode 100644 index 354910e29f9c2..0000000000000 --- a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/KeyEventConversionTests.kt +++ /dev/null @@ -1,214 +0,0 @@ -/* - * 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.ui.input - -import androidx.compose.ui.events.keyDownEvent -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.toComposeEvent -import androidx.compose.ui.input.key.utf16CodePoint -import kotlin.test.Test -import kotlin.test.assertEquals -import org.w3c.dom.events.KeyboardEvent - -private fun KeyboardEvent.assertEquivalence(key: Key, codePoint: Int = key.keyCode.toInt()) { - val keyEvent = toComposeEvent() - assertEquals(actual = keyEvent.key, expected = key, message = "key doesn't match for ${this.key} / ${this.code}") - assertEquals(actual = keyEvent.utf16CodePoint, expected = codePoint, message = "utf16CodePoint doesn't match for ${this.key} / ${this.code}") -} - -class KeyEventConversionTests { - - @Test - fun standardKeyboardLayout() { - keyDownEvent("Escape", code = "Escape", keyCode = Key.Escape.keyCode.toInt()).assertEquivalence(key = Key.Escape) - - keyDownEvent("CapsLock", code = "CapsLock", keyCode = Key.CapsLock.keyCode.toInt()).assertEquivalence(key = Key.CapsLock) - keyDownEvent("Tab", code = "Tab", keyCode = Key.Tab.keyCode.toInt()).assertEquivalence(key = Key.Tab) - keyDownEvent("Enter", code = "Enter", keyCode = Key.Enter.keyCode.toInt()).assertEquivalence(key = Key.Enter) - - keyDownEvent("a", code = "KeyA").assertEquivalence(key = Key.A, codePoint = 97) - keyDownEvent("b", code = "KeyB").assertEquivalence(key = Key.B, codePoint = 98) - keyDownEvent("c", code = "KeyC").assertEquivalence(key = Key.C, codePoint = 99) - keyDownEvent("d", code = "KeyD").assertEquivalence(key = Key.D, codePoint = 100) - keyDownEvent("e", code = "KeyE").assertEquivalence(key = Key.E, codePoint = 101) - keyDownEvent("f", code = "KeyF").assertEquivalence(key = Key.F, codePoint = 102) - keyDownEvent("g", code = "KeyG").assertEquivalence(key = Key.G, codePoint = 103) - keyDownEvent("h", code = "KeyH").assertEquivalence(key = Key.H, codePoint = 104) - keyDownEvent("i", code = "KeyI").assertEquivalence(key = Key.I, codePoint = 105) - keyDownEvent("j", code = "KeyJ").assertEquivalence(key = Key.J, codePoint = 106) - keyDownEvent("k", code = "KeyK").assertEquivalence(key = Key.K, codePoint = 107) - keyDownEvent("l", code = "KeyL").assertEquivalence(key = Key.L, codePoint = 108) - keyDownEvent("m", code = "KeyM").assertEquivalence(key = Key.M, codePoint = 109) - keyDownEvent("n", code = "KeyN").assertEquivalence(key = Key.N, codePoint = 110) - keyDownEvent("o", code = "KeyO").assertEquivalence(key = Key.O, codePoint = 111) - keyDownEvent("p", code = "KeyP").assertEquivalence(key = Key.P, codePoint = 112) - keyDownEvent("q", code = "KeyQ").assertEquivalence(key = Key.Q, codePoint = 113) - keyDownEvent("r", code = "KeyR").assertEquivalence(key = Key.R, codePoint = 114) - keyDownEvent("s", code = "KeyS").assertEquivalence(key = Key.S, codePoint = 115) - keyDownEvent("t", code = "KeyT").assertEquivalence(key = Key.T, codePoint = 116) - keyDownEvent("u", code = "KeyU").assertEquivalence(key = Key.U, codePoint = 117) - keyDownEvent("v", code = "KeyV").assertEquivalence(key = Key.V, codePoint = 118) - keyDownEvent("w", code = "KeyW").assertEquivalence(key = Key.W, codePoint = 119) - keyDownEvent("x", code = "KeyX").assertEquivalence(key = Key.X, codePoint = 120) - keyDownEvent("y", code = "KeyY").assertEquivalence(key = Key.Y, codePoint = 121) - keyDownEvent("z", code = "KeyZ").assertEquivalence(key = Key.Z, codePoint = 122) - - keyDownEvent("`", code = "Backquote", keyCode = Key.Grave.keyCode.toInt()).assertEquivalence(key = Key.Grave, codePoint = 96) - keyDownEvent("0", code = "Digit0").assertEquivalence(key = Key.Zero) - keyDownEvent("1", code = "Digit1").assertEquivalence(key = Key.One) - keyDownEvent("2", code = "Digit2").assertEquivalence(key = Key.Two) - keyDownEvent("3", code = "Digit3").assertEquivalence(key = Key.Three) - keyDownEvent("4", code = "Digit4").assertEquivalence(key = Key.Four) - keyDownEvent("5", code = "Digit5").assertEquivalence(key = Key.Five) - keyDownEvent("6", code = "Digit6").assertEquivalence(key = Key.Six) - keyDownEvent("7", code = "Digit7").assertEquivalence(key = Key.Seven) - keyDownEvent("8", code = "Digit8").assertEquivalence(key = Key.Eight) - keyDownEvent("9", code = "Digit9").assertEquivalence(key = Key.Nine) - - keyDownEvent("0", code = "Numpad0").assertEquivalence(key = Key.NumPad0, codePoint = 48) - keyDownEvent("1", code = "Numpad1").assertEquivalence(key = Key.NumPad1, codePoint = 49) - keyDownEvent("2", code = "Numpad2").assertEquivalence(key = Key.NumPad2, codePoint = 50) - keyDownEvent("3", code = "Numpad3").assertEquivalence(key = Key.NumPad3, codePoint = 51) - keyDownEvent("4", code = "Numpad4").assertEquivalence(key = Key.NumPad4, codePoint = 52) - keyDownEvent("5", code = "Numpad5").assertEquivalence(key = Key.NumPad5, codePoint = 53) - keyDownEvent("6", code = "Numpad6").assertEquivalence(key = Key.NumPad6, codePoint = 54) - keyDownEvent("7", code = "Numpad7").assertEquivalence(key = Key.NumPad7, codePoint = 55) - keyDownEvent("8", code = "Numpad8").assertEquivalence(key = Key.NumPad8, codePoint = 56) - keyDownEvent("9", code = "Numpad9").assertEquivalence(key = Key.NumPad9, codePoint = 57) - - keyDownEvent("=", code = "NumpadEqual", keyCode = Key.NumPadEquals.keyCode.toInt()).assertEquivalence(key = Key.NumPadEquals, codePoint = 61) - keyDownEvent("/", code = "NumpadDivide", keyCode = Key.NumPadDivide.keyCode.toInt()).assertEquivalence(key = Key.NumPadDivide, codePoint = 47) - keyDownEvent("*", code = "NumpadMultiply", keyCode = Key.NumPadMultiply.keyCode.toInt()).assertEquivalence(key = Key.NumPadMultiply, codePoint = 42) - keyDownEvent("-", code = "NumpadSubtract", keyCode = Key.NumPadSubtract.keyCode.toInt()).assertEquivalence(key = Key.NumPadSubtract, codePoint = 45) - keyDownEvent("+", code = "NumpadAdd", keyCode = Key.NumPadAdd.keyCode.toInt()).assertEquivalence(key = Key.NumPadAdd, codePoint = 43) - - //TODO: Situation with NumpadEnter is not clear so far keyDownEvent("Enter", code = "NumpadEnter").assertEquivalence(key = Key.NumPadEnter, codePoint = 13) - keyDownEvent(".", code = "NumpadDecimal", keyCode = Key.NumPadDot.keyCode.toInt()).assertEquivalence(key = Key.NumPadDot, codePoint = 46) - - keyDownEvent("Backspace", code = "Backspace", keyCode = Key.Backspace.keyCode.toInt()).assertEquivalence(key = Key.Backspace) - keyDownEvent("Delete", code = "Delete", keyCode = Key.Delete.keyCode.toInt()).assertEquivalence(key = Key.Delete) - - keyDownEvent("[", code = "BracketLeft", keyCode = Key.LeftBracket.keyCode.toInt()).assertEquivalence(key = Key.LeftBracket, codePoint = 91) - keyDownEvent("]", code = "BracketRight", keyCode = Key.RightBracket.keyCode.toInt()).assertEquivalence(key = Key.RightBracket, codePoint = 93) - keyDownEvent("\\", code = "Backslash", keyCode = Key.Backslash.keyCode.toInt()).assertEquivalence(key = Key.Backslash, codePoint = 92) - - keyDownEvent("Home", code = "Home", keyCode = Key.MoveHome.keyCode.toInt()).assertEquivalence(key = Key.MoveHome) - keyDownEvent("End", code = "End", keyCode = Key.MoveEnd.keyCode.toInt()).assertEquivalence(key = Key.MoveEnd) - keyDownEvent("PageUp", code = "PageUp", keyCode = Key.PageUp.keyCode.toInt()).assertEquivalence(key = Key.PageUp) - keyDownEvent("PageDown", code = "PageDown", keyCode = Key.PageDown.keyCode.toInt()).assertEquivalence(key = Key.PageDown) - - - keyDownEvent("ShiftLeft", code = "ShiftLeft", keyCode = Key.ShiftLeft.keyCode.toInt()).assertEquivalence(key = Key.ShiftLeft) - keyDownEvent("ShiftRight", code = "ShiftRight", keyCode = Key.ShiftRight.keyCode.toInt()).assertEquivalence(key = Key.ShiftRight) - - keyDownEvent("Control", code = "ControlLeft", keyCode = Key.CtrlLeft.keyCode.toInt()).assertEquivalence(key = Key.CtrlLeft) - keyDownEvent("Control", code = "ControlRight", keyCode = Key.CtrlRight.keyCode.toInt()).assertEquivalence(key = Key.CtrlRight) - - keyDownEvent("Meta", code = "MetaLeft", keyCode = Key.MetaLeft.keyCode.toInt()).assertEquivalence(key = Key.MetaLeft) - keyDownEvent("Meta", code = "MetaRight", keyCode = Key.MetaRight.keyCode.toInt()).assertEquivalence(key = Key.MetaRight) - - keyDownEvent("-", code = "Minus", keyCode = Key.Minus.keyCode.toInt()).assertEquivalence(key = Key.Minus, codePoint = 45) - keyDownEvent("=", code = "Equal", keyCode = Key.Equals.keyCode.toInt()).assertEquivalence(key = Key.Equals, codePoint = 61) - keyDownEvent(";", code = "Semicolon", keyCode = Key.Semicolon.keyCode.toInt()).assertEquivalence(key = Key.Semicolon, codePoint = 59) - - keyDownEvent(",", code = "Comma", keyCode = Key.Comma.keyCode.toInt()).assertEquivalence(key = Key.Comma, codePoint = 44) - keyDownEvent(".", code = "Period", keyCode = Key.Period.keyCode.toInt()).assertEquivalence(key = Key.Period, codePoint = 46) - keyDownEvent("/", code = "Slash", keyCode = Key.Slash.keyCode.toInt()).assertEquivalence(key = Key.Slash, codePoint = 47) - - keyDownEvent("Alt", code = "AltLeft", keyCode = Key.AltLeft.keyCode.toInt()).assertEquivalence(key = Key.AltLeft) - keyDownEvent("Alt", code = "AltRight", keyCode = Key.AltRight.keyCode.toInt()).assertEquivalence(key = Key.AltRight) - - keyDownEvent("ArrowUp", code = "ArrowUp", keyCode = Key.DirectionUp.keyCode.toInt()).assertEquivalence(key = Key.DirectionUp) - keyDownEvent("ArrowRight", code = "ArrowRight", keyCode = Key.DirectionRight.keyCode.toInt()).assertEquivalence(key = Key.DirectionRight) - keyDownEvent("ArrowDown", code = "ArrowDown", keyCode = Key.DirectionDown.keyCode.toInt()).assertEquivalence(key = Key.DirectionDown) - keyDownEvent("ArrowLeft", code = "ArrowLeft", keyCode = Key.DirectionLeft.keyCode.toInt()).assertEquivalence(key = Key.DirectionLeft) - - keyDownEvent("CapsLock", code = "CapsLock", keyCode = Key.CapsLock.keyCode.toInt()).assertEquivalence(key = Key.CapsLock) - - keyDownEvent("F1", code = "F1", keyCode = Key.F1.keyCode.toInt()).assertEquivalence(key = Key.F1) - keyDownEvent("F2", code = "F2", keyCode = Key.F2.keyCode.toInt()).assertEquivalence(key = Key.F2) - keyDownEvent("F3", code = "F3", keyCode = Key.F3.keyCode.toInt()).assertEquivalence(key = Key.F3) - keyDownEvent("F4", code = "F4", keyCode = Key.F4.keyCode.toInt()).assertEquivalence(key = Key.F4) - keyDownEvent("F5", code = "F5", keyCode = Key.F5.keyCode.toInt()).assertEquivalence(key = Key.F5) - keyDownEvent("F6", code = "F6", keyCode = Key.F6.keyCode.toInt()).assertEquivalence(key = Key.F6) - keyDownEvent("F7", code = "F7", keyCode = Key.F7.keyCode.toInt()).assertEquivalence(key = Key.F7) - keyDownEvent("F8", code = "F8", keyCode = Key.F8.keyCode.toInt()).assertEquivalence(key = Key.F8) - keyDownEvent("F9", code = "F9", keyCode = Key.F9.keyCode.toInt()).assertEquivalence(key = Key.F9) - keyDownEvent("F10", code = "F10", keyCode = Key.F10.keyCode.toInt()).assertEquivalence(key = Key.F10) - keyDownEvent("F11", code = "F11", keyCode = Key.F11.keyCode.toInt()).assertEquivalence(key = Key.F11) - keyDownEvent("F12", code = "F12", keyCode = Key.F12.keyCode.toInt()).assertEquivalence(key = Key.F12) - - keyDownEvent("", code = "Space", keyCode = Key.Spacebar.keyCode.toInt()).assertEquivalence(key = Key.Spacebar) - } - - @Test - fun standardKeyboardLayoutUpper() { - keyDownEvent("A", code = "KeyA").assertEquivalence(key = Key.A) - keyDownEvent("B", code = "KeyB").assertEquivalence(key = Key.B) - keyDownEvent("C", code = "KeyC").assertEquivalence(key = Key.C) - keyDownEvent("D", code = "KeyD").assertEquivalence(key = Key.D) - keyDownEvent("E", code = "KeyE").assertEquivalence(key = Key.E) - keyDownEvent("F", code = "KeyF").assertEquivalence(key = Key.F) - keyDownEvent("G", code = "KeyG").assertEquivalence(key = Key.G) - keyDownEvent("H", code = "KeyH").assertEquivalence(key = Key.H) - keyDownEvent("I", code = "KeyI").assertEquivalence(key = Key.I) - keyDownEvent("J", code = "KeyJ").assertEquivalence(key = Key.J) - keyDownEvent("K", code = "KeyK").assertEquivalence(key = Key.K) - keyDownEvent("L", code = "KeyL").assertEquivalence(key = Key.L) - keyDownEvent("M", code = "KeyM").assertEquivalence(key = Key.M) - keyDownEvent("N", code = "KeyN").assertEquivalence(key = Key.N) - keyDownEvent("O", code = "KeyO").assertEquivalence(key = Key.O) - keyDownEvent("P", code = "KeyP").assertEquivalence(key = Key.P) - keyDownEvent("Q", code = "KeyQ").assertEquivalence(key = Key.Q) - keyDownEvent("R", code = "KeyR").assertEquivalence(key = Key.R) - keyDownEvent("S", code = "KeyS").assertEquivalence(key = Key.S) - keyDownEvent("T", code = "KeyT").assertEquivalence(key = Key.T) - keyDownEvent("U", code = "KeyU").assertEquivalence(key = Key.U) - keyDownEvent("V", code = "KeyV").assertEquivalence(key = Key.V) - keyDownEvent("W", code = "KeyW").assertEquivalence(key = Key.W) - keyDownEvent("X", code = "KeyX").assertEquivalence(key = Key.X) - keyDownEvent("Y", code = "KeyY").assertEquivalence(key = Key.Y) - keyDownEvent("Z", code = "KeyZ").assertEquivalence(key = Key.Z) - - keyDownEvent("~", code = "Backquote", keyCode = Key.Grave.keyCode.toInt()).assertEquivalence(key = Key.Grave, codePoint = 126) - keyDownEvent(")", code = "Digit0", keyCode = Key.Zero.keyCode.toInt()).assertEquivalence(key = Key.Zero, codePoint = 41) - keyDownEvent("!", code = "Digit1", keyCode = Key.One.keyCode.toInt()).assertEquivalence(key = Key.One, codePoint = 33) - keyDownEvent("@", code = "Digit2", keyCode = Key.Two.keyCode.toInt()).assertEquivalence(key = Key.Two, codePoint = 64) - keyDownEvent("#", code = "Digit3", keyCode = Key.Three.keyCode.toInt()).assertEquivalence(key = Key.Three, codePoint = 35) - keyDownEvent("$", code = "Digit4", keyCode = Key.Four.keyCode.toInt()).assertEquivalence(key = Key.Four, codePoint = 36) - keyDownEvent("%", code = "Digit5", keyCode = Key.Five.keyCode.toInt()).assertEquivalence(key = Key.Five, codePoint = 37) - keyDownEvent("^", code = "Digit6", keyCode = Key.Six.keyCode.toInt()).assertEquivalence(key = Key.Six, codePoint = 94) - keyDownEvent("&", code = "Digit7", keyCode = Key.Seven.keyCode.toInt()).assertEquivalence(key = Key.Seven, codePoint = 38) - keyDownEvent("*", code = "Digit8", keyCode = Key.Eight.keyCode.toInt()).assertEquivalence(key = Key.Eight, codePoint = 42) - keyDownEvent("(", code = "Digit9", keyCode = Key.Nine.keyCode.toInt()).assertEquivalence(key = Key.Nine, codePoint = 40) - keyDownEvent("_", code = "Minus", keyCode = Key.Minus.keyCode.toInt()).assertEquivalence(key = Key.Minus, codePoint = 95) - keyDownEvent("+", code = "Equal", keyCode = Key.Equals.keyCode.toInt()).assertEquivalence(key = Key.Equals, codePoint = 43) - } - - @Test - fun standardVirtualKeyboardLayout() { - // Virtual keyboard generates actual keyboard events for some of the keys pressed - // This keyboard events, however, actually differ - the code is always "" while key contains the value that we need - keyDownEvent("ArrowRight", code = "", keyCode = Key.DirectionRight.keyCode.toInt()).assertEquivalence(key = Key.DirectionRight) - keyDownEvent("ArrowLeft", code = "", keyCode = Key.DirectionLeft.keyCode.toInt()).assertEquivalence(key = Key.DirectionLeft) - keyDownEvent("Delete", code = "", keyCode = Key.Delete.keyCode.toInt()).assertEquivalence(key = Key.Delete) - keyDownEvent("Backspace", code = "", keyCode = Key.Backspace.keyCode.toInt()).assertEquivalence(key = Key.Backspace) - keyDownEvent("Enter", code = "", keyCode = Key.Enter.keyCode.toInt()).assertEquivalence(key = Key.Enter) - } - -} diff --git a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/ScrollTests.kt b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/ScrollTests.kt deleted file mode 100644 index 132d0f6517241..0000000000000 --- a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/ScrollTests.kt +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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.ui.input - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.OnCanvasTests -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.layout.positionOnScreen -import androidx.compose.ui.sendFromScope -import androidx.compose.ui.unit.dp -import kotlin.test.Test -import kotlin.test.assertTrue -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.test.runTest -import org.w3c.dom.events.WheelEvent -import org.w3c.dom.events.WheelEventInit - -class ScrollTests : OnCanvasTests { - @Test - fun scrollTillEnd() = runTest { - val firstRowScrollPositionResolved = Channel( - 1, onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - - val lastRowScrollPositionResolved = Channel( - 1, onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - - createComposeWindow { - Column( - modifier = Modifier.fillMaxWidth().height(300.dp).wrapContentSize(Alignment.Center) - .verticalScroll( - rememberScrollState() - ) - ) { - Box( - modifier = Modifier.height(100.dp).fillMaxWidth().background(Color(237, 40, 57)) - .onGloballyPositioned { coordinates -> - val screenPosition = coordinates.positionOnScreen() - if (screenPosition == Offset(0f, -200f)) { - firstRowScrollPositionResolved.sendFromScope(true) - } - } - ) - Box( - modifier = Modifier.height(100.dp).fillMaxWidth().background(Color(255, 88, 0)) - ) - Box( - modifier = Modifier.height(100.dp).fillMaxWidth().background(Color(255, 211, 0)) - ) - Box( - modifier = Modifier.height(100.dp).fillMaxWidth().background(Color(0, 173, 131)) - ) - Box( - modifier = Modifier.height(100.dp).fillMaxWidth() - .background(Color(190, 219, 237)) - ) - Box( - modifier = Modifier.height(100.dp).fillMaxWidth() - .background(Color(31, 117, 254)) - ) - Box( - modifier = Modifier.height(100.dp).fillMaxWidth().background(Color(143, 0, 255)).onGloballyPositioned { coordinates -> - val screenPosition = coordinates.positionOnScreen() - if (screenPosition == Offset(0f, 1000f)) { - lastRowScrollPositionResolved.sendFromScope(true) - } - } - ) - } - } - - dispatchEvents(createWheelEvent(clientX = 100, clientY = 100, deltaX = 0.0, deltaY = 200.0)) - - assertTrue(firstRowScrollPositionResolved.receive(), "first row scroll position is not resolved") - assertTrue(lastRowScrollPositionResolved.receive(), "last row scroll position is not resolved") - } -} - - -private fun createWheelEvent( - deltaX: Double, - deltaY: Double, - clientX: Int, - clientY: Int -): WheelEvent { - return WheelEvent( - "wheel", WheelEventInit( - deltaX = deltaX, - deltaY = deltaY, - clientX = clientX, - clientY = clientY - ) - ) -} \ No newline at end of file diff --git a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/TextInputTests.kt b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/TextInputTests.kt deleted file mode 100644 index dd02d779d726b..0000000000000 --- a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/TextInputTests.kt +++ /dev/null @@ -1,165 +0,0 @@ -/* - * 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.ui.input - -import androidx.compose.material.TextField -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.OnCanvasTests -import androidx.compose.ui.events.InputEvent -import androidx.compose.ui.events.InputEventInit -import androidx.compose.ui.events.createMouseEvent -import androidx.compose.ui.events.createTouchEvent -import androidx.compose.ui.events.keyDownEvent -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.sendFromScope -import kotlin.test.Ignore -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlinx.browser.document -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.test.runTest - -class TextInputTests : OnCanvasTests { - - @Test - fun keyboardEventPassedToTextField() = runTest { - - val textInputChannel = Channel( - 1, onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - - val (firstFocusRequester, secondFocusRequester) = FocusRequester.createRefs() - - createComposeWindow { - TextField( - value = "", - onValueChange = { value -> - textInputChannel.sendFromScope(value) - }, - modifier = Modifier.focusRequester(firstFocusRequester) - ) - - TextField( - value = "", - onValueChange = { value -> - textInputChannel.sendFromScope(value) - }, - modifier = Modifier.focusRequester(secondFocusRequester) - ) - - SideEffect { - secondFocusRequester.requestFocus() - firstFocusRequester.requestFocus() - } - } - - assertNull(document.querySelector("textarea")) - - dispatchEvents( - keyDownEvent("s"), - keyDownEvent("t"), - keyDownEvent("e"), - keyDownEvent("p"), - keyDownEvent("1") - ) - - - assertEquals("step1", textInputChannel.receive()) - assertNull(document.querySelector("textarea")) - - // trigger virtual keyboard - dispatchEvents(createTouchEvent("touchstart")) - secondFocusRequester.requestFocus() - - assertNotNull(document.querySelector("textarea")) - - dispatchEvents( - keyDownEvent("s"), - keyDownEvent("t"), - keyDownEvent("e"), - keyDownEvent("p"), - keyDownEvent("2") - ) - - assertEquals("step2", textInputChannel.receive()) - - val backingField = document.querySelector("textarea")!! - - dispatchEvents( - keyDownEvent("s"), - keyDownEvent("t"), - keyDownEvent("e"), - keyDownEvent("p"), - keyDownEvent("3") - ) - - assertEquals("step2step3", textInputChannel.receive()) - - backingField.dispatchEvent(InputEvent("input", InputEventInit("insertText", "step4XX"))) - - assertEquals("step2step3step4XX", textInputChannel.receive()) - - backingField.dispatchEvent(InputEvent("input", InputEventInit("deleteContentBackward", ""))) - backingField.dispatchEvent(InputEvent("input", InputEventInit("deleteContentBackward", ""))) - assertEquals("step2step3step4", textInputChannel.receive()) - - // trigger hardware keyboard - dispatchEvents(createMouseEvent("mousedown")) - firstFocusRequester.requestFocus() - - dispatchEvents( - keyDownEvent("s"), - keyDownEvent("t"), - keyDownEvent("e"), - keyDownEvent("p"), - keyDownEvent("5") - ) - - assertEquals("step1step5", textInputChannel.receive()) - } - - @Test - fun keyboardEventPassedToTextFieldMinimal() = runTest { - val textInputChannel = Channel( - 1, onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - - val (firstFocusRequester) = FocusRequester.createRefs() - - createComposeWindow { - TextField( - value = "", - onValueChange = { value -> - textInputChannel.sendFromScope(value) - }, - modifier = Modifier.focusRequester(firstFocusRequester) - ) - } - - dispatchEvents( - keyDownEvent("X"), - ) - - assertEquals("X", textInputChannel.receive()) - } - -} \ No newline at end of file diff --git a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/window/ComposeWindowLifecycleTest.kt b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/window/ComposeWindowLifecycleTest.kt deleted file mode 100644 index cb98c32d47caa..0000000000000 --- a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/window/ComposeWindowLifecycleTest.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.ui.window - -import androidx.compose.ui.OnCanvasTests -import androidx.compose.ui.sendFromScope -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.LifecycleOwner -import kotlin.test.Ignore -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue -import kotlinx.browser.document -import kotlinx.browser.window -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.test.runTest - - -class ComposeWindowLifecycleTest : OnCanvasTests { - @Test - @Ignore // ignored while investigating CI issues: this test opens a new browser window which can be the cause - fun allEvents() = runTest { - val canvas = getCanvas() - canvas.focus() - - val lifecycleOwner = ComposeWindow( - canvas = canvas, - content = {}, - state = DefaultWindowState(document.documentElement!!) - ) - - val eventsChannel = Channel(10) - - lifecycleOwner.lifecycle.addObserver(object : LifecycleEventObserver { - override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { - eventsChannel.sendFromScope(event) - } - }) - - assertEquals(Lifecycle.State.CREATED, eventsChannel.receive().targetState) - assertEquals(Lifecycle.State.STARTED, eventsChannel.receive().targetState) - assertEquals(Lifecycle.State.RESUMED, eventsChannel.receive().targetState) - - // Browsers don't allow to blur the window from code: - // https://developer.mozilla.org/en-US/docs/Web/API/Window/blur - // So we simulate a new tab being open: - val anotherWindow = window.open("about:config") - assertTrue(anotherWindow != null) - assertEquals(Lifecycle.State.STARTED, eventsChannel.receive().targetState) - - // Now go back to the original window - anotherWindow.close() - assertEquals(Lifecycle.State.RESUMED, eventsChannel.receive().targetState) - } -} diff --git a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/window/KeyEventTests.kt b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/window/KeyEventTests.kt deleted file mode 100644 index aabab3f264eab..0000000000000 --- a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/window/KeyEventTests.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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.ui.window - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size -import androidx.compose.material.Text -import androidx.compose.material.TextField -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Modifier -import androidx.compose.ui.OnCanvasTests -import androidx.compose.ui.events.keyDownEvent -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.focusTarget -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.KeyEvent -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.onKeyEvent -import androidx.compose.ui.input.key.onPreviewKeyEvent -import androidx.compose.ui.unit.dp -import kotlin.test.Test -import kotlin.test.assertEquals - -class KeyEventTests : OnCanvasTests { - @Test - // https://github.com/JetBrains/compose-multiplatform/issues/3644 - fun keyMappingIsValid() { - val fr = FocusRequester() - var mapping = "" - var k: Key? = null - createComposeWindow { - Box( - Modifier.size(1000.dp).background(Color.Red).focusRequester(fr).focusTarget() - .onKeyEvent { - k = it.key - mapping = it.key.toString() - false - }) { - Text("Try to press different keys and look at the console...") - } - SideEffect { - fr.requestFocus() - } - } - - val listOfKeys = listOf( - Key.A, Key.B, Key.C, Key.D, Key.E, Key.F, Key.G, - Key.H, Key.I, Key.J, Key.K, Key.L, Key.M, Key.N, - Key.O, Key.P, Key.Q, Key.R, Key.S, Key.T, Key.U, - Key.V, Key.W, Key.X, Key.Y, Key.Z - ) - - - ('a'..'z').forEachIndexed { index, c -> - dispatchEvents(keyDownEvent(c.toString())) - assertEquals(listOfKeys[index], k) - } - - val listOfNumbers = listOf( - Key.Zero, Key.One, Key.Two, Key.Three, Key.Four, - Key.Five, Key.Six, Key.Seven, Key.Eight, Key.Nine - ) - - ('0'..'9').forEachIndexed { index, c -> - val id = c.toString() - dispatchEvents(keyDownEvent(id, code = "Digit${id}")) - assertEquals(listOfNumbers[index], k) - } - } - - @Test - // https://github.com/JetBrains/compose-multiplatform/issues/2296 - fun onPreviewKeyEventShouldWork() { - - val fr = FocusRequester() - val textValue = mutableStateOf("") - var lastKeyEvent: KeyEvent? = null - var stopPropagation = true - - createComposeWindow { - TextField( - value = textValue.value, - onValueChange = { textValue.value = it }, - modifier = Modifier.fillMaxSize().focusRequester(fr).onPreviewKeyEvent { - lastKeyEvent = it - return@onPreviewKeyEvent stopPropagation - } - ) - SideEffect { - fr.requestFocus() - } - } - - dispatchEvents(keyDownEvent("t")) - assertEquals(Key.T, lastKeyEvent!!.key) - assertEquals("", textValue.value) - - stopPropagation = false - dispatchEvents( - keyDownEvent("t"), - keyDownEvent("e"), - keyDownEvent("s"), - keyDownEvent("t"), - keyDownEvent("x") - ) - assertEquals(Key.X, lastKeyEvent!!.key) - assertEquals("testx", textValue.value) - } -} \ No newline at end of file diff --git a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/window/MouseEventsTest.kt b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/window/MouseEventsTest.kt deleted file mode 100644 index d6ebc889db4e9..0000000000000 --- a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/window/MouseEventsTest.kt +++ /dev/null @@ -1,151 +0,0 @@ -/* - * 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.ui.window - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.PointerMatcher -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.onClick -import androidx.compose.ui.Modifier -import androidx.compose.ui.OnCanvasTests -import androidx.compose.ui.input.pointer.PointerButton -import androidx.compose.ui.input.pointer.PointerEvent -import androidx.compose.ui.input.pointer.PointerEventType -import androidx.compose.ui.input.pointer.pointerInput -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlinx.coroutines.NonCancellable.isActive -import kotlinx.coroutines.test.runTest -import org.w3c.dom.events.MouseEvent -import org.w3c.dom.events.MouseEventInit - -class MouseEventsTest : OnCanvasTests { - - @Test - fun testPointerEvents() = runTest { - val pointerEvents = mutableListOf() - - createComposeWindow { - Box( - modifier = Modifier - .fillMaxSize() - .pointerInput(Unit) { - awaitPointerEventScope { - while (isActive) { - pointerEvents.add(awaitPointerEvent()) - } - } - } - ) {} - } - - dispatchEvents( - MouseEvent("mouseenter", MouseEventInit(100, 100)), - MouseEvent("mousedown", MouseEventInit(100, 100, button = 0, buttons = 1)), - MouseEvent("mouseup", MouseEventInit(100, 100, button = 0, buttons = 0)) - ) - - assertEquals(3, pointerEvents.size) - assertEquals(PointerEventType.Enter, pointerEvents[0].type) - - // Check for primary button - assertEquals(PointerEventType.Press, pointerEvents[1].type) - assertEquals(PointerButton.Primary, pointerEvents[1].button) - assertEquals(PointerEventType.Release, pointerEvents[2].type) - assertEquals(PointerButton.Primary, pointerEvents[2].button) - - dispatchEvents( - MouseEvent("mousedown", MouseEventInit(100, 100, button = 2, buttons = 2)), - MouseEvent("mouseup", MouseEventInit(100, 100, button = 2, buttons = 0)) - ) - - assertEquals(5, pointerEvents.size) - - // Check for secondary button - assertEquals(PointerEventType.Press, pointerEvents[3].type) - assertEquals(PointerButton.Secondary, pointerEvents[3].button) - assertEquals(PointerEventType.Release, pointerEvents[4].type) - assertEquals(PointerButton.Secondary, pointerEvents[4].button) - } - - @OptIn(ExperimentalFoundationApi::class) - @Test - fun testOnClickWithPointerMatchers() = runTest { - var primaryClickedCounter = 0 - var secondaryClickedCounter = 0 - - createComposeWindow { - Box( - modifier = Modifier - .fillMaxSize() - .onClick(matcher = PointerMatcher.Primary) { primaryClickedCounter++ } - .onClick(matcher = PointerMatcher.mouse(PointerButton.Secondary)) { secondaryClickedCounter++ } - ) {} - } - - dispatchEvents( - MouseEvent("mouseenter", MouseEventInit(100, 100)), - MouseEvent("mousedown", MouseEventInit(100, 100, button = 0, buttons = 1)), - MouseEvent("mouseup", MouseEventInit(100, 100, button = 0, buttons = 0)) - ) - - assertEquals(1, primaryClickedCounter) - assertEquals(0, secondaryClickedCounter) - - dispatchEvents( - MouseEvent("mousedown", MouseEventInit(100, 100, button = 2, buttons = 2)), - MouseEvent("mouseup", MouseEventInit(100, 100, button = 2, buttons = 0)) - ) - - assertEquals(1, primaryClickedCounter) - assertEquals(1, secondaryClickedCounter) - } - - @Test - fun testPointerButtonIsNullForNoClickEvents() = runTest { - var event: PointerEvent? = null - - createComposeWindow { - Box( - modifier = Modifier - .fillMaxSize() - .pointerInput(Unit) { - awaitPointerEventScope { - while (isActive) { - event = awaitPointerEvent() - } - } - } - ) {} - } - - assertEquals(null, event) - - dispatchEvents(MouseEvent("mouseenter", MouseEventInit(100, 100))) - assertEquals(PointerEventType.Enter, event!!.type) - assertEquals(null, event!!.button) - - dispatchEvents(MouseEvent("mousemove", MouseEventInit(101, 101, clientX = 101, clientY = 101))) - assertEquals(PointerEventType.Move, event!!.type) - assertEquals(null, event!!.button) - - dispatchEvents(MouseEvent("mouseleave", MouseEventInit(0, 0, clientX = 0, clientY = 0))) - assertEquals(PointerEventType.Exit, event!!.type) - assertEquals(null, event!!.button) - } -} \ No newline at end of file diff --git a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/window/PreventDefaultTest.kt b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/window/PreventDefaultTest.kt deleted file mode 100644 index 63d69bf568ab1..0000000000000 --- a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/window/PreventDefaultTest.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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.ui.window - -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.TextField -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.OnCanvasTests -import androidx.compose.ui.events.keyDownEvent -import androidx.compose.ui.events.keyDownEventUnprevented -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class PreventDefaultTest : OnCanvasTests { - - @Test - fun testPreventDefault() { - val fr = FocusRequester() - var changedValue = "" - createComposeWindow { - TextField( - value = "", - onValueChange = { changedValue = it }, - modifier = Modifier.fillMaxSize().focusRequester(fr) - ) - SideEffect { - fr.requestFocus() - } - } - - var stack = mutableListOf() - - getCanvas().addEventListener("keydown", { event -> - stack.add(event.defaultPrevented) - }) - - // dispatchEvent synchronously invokes all the listeners - dispatchEvents(keyDownEvent("c")) - assertEquals(1, stack.size) - assertTrue(stack.last()) - - dispatchEvents(keyDownEventUnprevented()) - assertEquals(2, stack.size) - assertFalse(stack.last()) - - assertEquals(changedValue, "c") - - // copy shortcut should not be prevented (we let browser create a corresponding event) - dispatchEvents(keyDownEvent("c", metaKey = true, ctrlKey = true)) - assertEquals(3, stack.size) - assertFalse(stack.last()) - - assertEquals(changedValue, "c") - - } -} \ No newline at end of file From 915d34c908525ed91240291b83ee804af31e66da Mon Sep 17 00:00:00 2001 From: Shagen Ogandzhanian Date: Mon, 23 Sep 2024 18:05:17 +0200 Subject: [PATCH 8/9] Handling js-related global errors in web tests This is the minimal version of experiment done in sh/dev/js-in-web-tests --- mpp/karma.config.d/wasm/config.js | 12 +++++-- .../wasm/static/common-tests.js | 35 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 mpp/karma.config.d/wasm/static/common-tests.js diff --git a/mpp/karma.config.d/wasm/config.js b/mpp/karma.config.d/wasm/config.js index 8e9be830ee1ef..6a9de9bfa6e09 100644 --- a/mpp/karma.config.d/wasm/config.js +++ b/mpp/karma.config.d/wasm/config.js @@ -30,7 +30,8 @@ config.client.mocha.timeout = 10000; // This enables running tests on a custom html page without iframe config.client.useIframe = false config.client.runInParent = true -config.customClientContextFile = path.resolve(configPath, "static", "client_with_context.html") +config.staticFilesDir = path.resolve(configPath, "static"); +config.customClientContextFile = path.resolve(config.staticFilesDir, "client_with_context.html") function KarmaWebpackOutputFramework(config) { // This controller is instantiated and set during the preprocessor phase by the karma-webpack plugin @@ -45,12 +46,19 @@ function KarmaWebpackOutputFramework(config) { return } + config.files.push({ + pattern: `${config.staticFilesDir}/**/*.js`, + included: true, + served: true, + watched: false + }); + config.files.push({ pattern: `${controller.outputPath}/**/*`, included: false, served: true, watched: false - }) + }); } const KarmaWebpackOutputPlugin = { diff --git a/mpp/karma.config.d/wasm/static/common-tests.js b/mpp/karma.config.d/wasm/static/common-tests.js new file mode 100644 index 0000000000000..59fd4916350c4 --- /dev/null +++ b/mpp/karma.config.d/wasm/static/common-tests.js @@ -0,0 +1,35 @@ +/* + * 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. + */ + +window.addEventListener("error", (message, source, lineno, colno, error) => { + console.log(`[web] error message: ${message} \n`); + console.log(`[web] error source: ${source} \n`); + console.log(`[web] error lineno: ${lineno} \n`); + console.log(`[web] error colno: ${colno} \n`); + console.log(`[web] error error: ${error} \n`); + + return true; +}); + + +window.addEventListener("unhandledrejection", (event) => { + console.log(`[web] unhandled Promise rejection ${event.reason} \n`); +}); + +window.addEventListener("rejectionhandled", (event) => { + console.log(`[web] handled Promise rejection; reason: ${event.reason} \n`); + }, false +); \ No newline at end of file From 2489210672c1140e909e5423794e361f0c2d17b0 Mon Sep 17 00:00:00 2001 From: Shagen Ogandzhanian Date: Tue, 24 Sep 2024 10:28:21 +0200 Subject: [PATCH 9/9] Running in Chrome again --- .../AndroidXComposeMultiplatformExtensionImpl.kt | 3 +-- mpp/karma.config.d/wasm/config.js | 16 ++++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeMultiplatformExtensionImpl.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeMultiplatformExtensionImpl.kt index de0ad23df1610..961e34d32f700 100644 --- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeMultiplatformExtensionImpl.kt +++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeMultiplatformExtensionImpl.kt @@ -95,8 +95,7 @@ open class AndroidXComposeMultiplatformExtensionImpl @Inject constructor( browser { testTask { it.useKarma { - //useChrome() - useFirefox() + useChrome() useConfigDirectory( project.rootProject.projectDir.resolve("mpp/karma.config.d/wasm") ) diff --git a/mpp/karma.config.d/wasm/config.js b/mpp/karma.config.d/wasm/config.js index 6a9de9bfa6e09..fee8353f01525 100644 --- a/mpp/karma.config.d/wasm/config.js +++ b/mpp/karma.config.d/wasm/config.js @@ -69,11 +69,11 @@ config.plugins.push(KarmaWebpackOutputPlugin); config.frameworks.push("webpack-output"); -// config.customLaunchers = { -// ChromeForComposeTests: { -// base: "Chrome", -// flags: ["--disable-search-engine-choice-screen"] -// } -// } -// -// config.browsers = ["ChromeForComposeTests"] \ No newline at end of file +config.customLaunchers = { + ChromeForComposeTests: { + base: "Chrome", + flags: ["--disable-search-engine-choice-screen"] + } +} + +config.browsers = ["ChromeForComposeTests"] \ No newline at end of file