Skip to content

Commit

Permalink
Introduce dedicated test for virtual keyboard events
Browse files Browse the repository at this point in the history
  • Loading branch information
Schahen committed Jun 7, 2024
1 parent 6aac678 commit 531339c
Show file tree
Hide file tree
Showing 12 changed files with 229 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class SelectionContainerTests : OnCanvasTests {
fun setup() {
// Because AfterTest is fixed only in kotlin 2.0
// https://youtrack.jetbrains.com/issue/KT-61888
document.getElementById(canvasId)?.remove()
commonAfterTest()
}

fun HTMLCanvasElement.doClick() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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.events

import org.w3c.dom.events.Event

internal external interface InputEventInit {
val data: String
val inputType: String
}

internal fun InputEventInit(inputType: String, data: String): InputEventInit = js("({data, inputType})")

internal external class InputEvent(type: String, options: InputEventInit) : Event
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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.events

import org.w3c.dom.events.Event

internal external class MouseEvent(type: String) : Event
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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.events

import org.w3c.dom.events.Event

internal external interface TouchEventInit

internal external class TouchEvent(type: String, initParams: TouchEventInit) : Event
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
* limitations under the License.
*/

package androidx.compose.ui.input
package androidx.compose.ui.events

import androidx.compose.ui.input.key.Key
import org.w3c.dom.events.KeyboardEvent
import org.w3c.dom.events.KeyboardEventInit
import org.w3c.dom.events.MouseEvent

internal external interface KeyboardEventInitExtended : KeyboardEventInit {
var keyCode: Int?
Expand Down Expand Up @@ -54,4 +55,15 @@ internal fun keyDownEvent(
internal fun keyDownEventUnprevented(): KeyboardEvent =
KeyboardEventInit(ctrlKey = true, cancelable = true, key = "Control")
.withKeyCode(Key.CtrlLeft.keyCode.toInt())
.keyDownEvent()
.keyDownEvent()

private fun DummyTouchEventInit(): TouchEventInit = js("({ changedTouches: [new Touch({identifier: 0, target: document})] })")

internal fun createTouchEvent(type: String): TouchEvent {
return TouchEvent(type, DummyTouchEventInit())
}

internal fun createMouseEvent(type: String): MouseEvent {
return MouseEvent(type)
}

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* 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.layout.fillMaxSize
import androidx.compose.material.TextField
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
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.window.CanvasBasedWindow
import kotlin.test.BeforeTest
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
import org.w3c.dom.HTMLCanvasElement

class TextInputTests : OnCanvasTests {

@BeforeTest
fun setup() {
// Because AfterTest is fixed only in kotlin 2.0
// https://youtrack.jetbrains.com/issue/KT-61888
commonAfterTest()
createCanvasAndAttach()
}

@Test
fun keyboardEventPassedToTextField() = runTest {

val textInputChannel = Channel<String>(
1, onBufferOverflow = BufferOverflow.DROP_OLDEST
)

val canvas = document.getElementById(canvasId) as HTMLCanvasElement

CanvasBasedWindow(canvasElementId = canvasId) {
val (firstFocusRequester, secondFocusRequester) = remember { FocusRequester.createRefs() }

TextField(
value = "",
onValueChange = { value ->
textInputChannel.trySend(value)
if (value == "step1") {
canvas.dispatchEvent(createTouchEvent("touchstart"))
secondFocusRequester.requestFocus()
}
},
modifier = Modifier.fillMaxSize().focusRequester(firstFocusRequester)
)

TextField(
value = "",
onValueChange = { value ->
textInputChannel.trySend(value)
if (value == "step2step3step4") {
canvas.dispatchEvent(createMouseEvent("mousedown"))
firstFocusRequester.requestFocus()
}
},
modifier = Modifier.fillMaxSize().focusRequester(secondFocusRequester)
)

SideEffect {
secondFocusRequester.requestFocus()
firstFocusRequester.requestFocus()
}
}

assertNull(document.querySelector("textarea"))

canvas.dispatchEvent(keyDownEvent("s"))
canvas.dispatchEvent(keyDownEvent("t"))
canvas.dispatchEvent(keyDownEvent("e"))
canvas.dispatchEvent(keyDownEvent("p"))
canvas.dispatchEvent(keyDownEvent("1"))

assertEquals("step1", textInputChannel.receive())
assertNotNull(document.querySelector("textarea"))

canvas.dispatchEvent(keyDownEvent("s"))
canvas.dispatchEvent(keyDownEvent("t"))
canvas.dispatchEvent(keyDownEvent("e"))
canvas.dispatchEvent(keyDownEvent("p"))
canvas.dispatchEvent(keyDownEvent("2"))

assertEquals("step2", textInputChannel.receive())

val backingField = document.querySelector("textarea")!!

backingField.dispatchEvent(keyDownEvent("s"))
backingField.dispatchEvent(keyDownEvent("t"))
backingField.dispatchEvent(keyDownEvent("e"))
backingField.dispatchEvent(keyDownEvent("p"))
backingField.dispatchEvent(keyDownEvent("3"))

assertEquals("step2step3", textInputChannel.receive())

backingField.dispatchEvent(InputEvent("input", InputEventInit("insertText", "step4")))

assertEquals("step2step3step4", textInputChannel.receive())

canvas.dispatchEvent(keyDownEvent("s"))
canvas.dispatchEvent(keyDownEvent("t"))
canvas.dispatchEvent(keyDownEvent("e"))
canvas.dispatchEvent(keyDownEvent("p"))
canvas.dispatchEvent(keyDownEvent("5"))

assertEquals("step1step5", textInputChannel.receive())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,17 @@ import androidx.compose.material.TextField
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
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 androidx.compose.ui.focus.focusTarget
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.keyDownEventUnprevented
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.input.keyDownEvent
import androidx.compose.ui.unit.dp
import kotlin.test.Test
import kotlinx.browser.document
Expand All @@ -56,7 +56,6 @@ class CanvasBasedWindowTests {

@Test
fun canCreate() {
if (isHeadlessBrowser()) return
val canvasElement = document.createElement("canvas") as HTMLCanvasElement
canvasElement.setAttribute("id", canvasId)
document.body!!.appendChild(canvasElement)
Expand All @@ -65,7 +64,6 @@ class CanvasBasedWindowTests {

@Test
fun testPreventDefault() {
if (isHeadlessBrowser()) return
val canvasElement = document.createElement("canvas") as HTMLCanvasElement
canvasElement.setAttribute("id", canvasId)
document.body!!.appendChild(canvasElement)
Expand Down Expand Up @@ -106,8 +104,6 @@ class CanvasBasedWindowTests {
@Test
// https://github.com/JetBrains/compose-multiplatform/issues/3644
fun keyMappingIsValid() {
if (isHeadlessBrowser()) return

val canvasElement = document.createElement("canvas") as HTMLCanvasElement
canvasElement.setAttribute("id", canvasId)
document.body!!.appendChild(canvasElement)
Expand Down Expand Up @@ -154,7 +150,6 @@ class CanvasBasedWindowTests {
@Test
// https://github.com/JetBrains/compose-multiplatform/issues/2296
fun onPreviewKeyEventShouldWork() {
if (isHeadlessBrowser()) return
val canvasElement = document.createElement("canvas") as HTMLCanvasElement
canvasElement.setAttribute("id", canvasId)
document.body!!.appendChild(canvasElement)
Expand Down Expand Up @@ -191,8 +186,4 @@ class CanvasBasedWindowTests {
assertEquals(Key.X, lastKeyEvent!!.key)
assertEquals("testx", textValue.value)
}
}


// Unreliable heuristic, but it works for now
internal fun isHeadlessBrowser(): Boolean = false//window.navigator.userAgent.contains("Headless")
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ class ComposeWindowLifecycleTest {
@Test
@Ignore // ignored while investigating CI issues: this test opens a new browser window which can be the cause
fun allEvents() = runTest {
if (isHeadlessBrowser()) return@runTest
val canvas = document.createElement("canvas") as HTMLCanvasElement
canvas.setAttribute("id", canvasId)
canvas.setAttribute("tabindex", "0")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ class MouseEventsTest {

@Test
fun testPointerEvents() = runTest {
if (isHeadlessBrowser()) return@runTest
val canvasElement = document.createElement("canvas") as HTMLCanvasElement
canvasElement.setAttribute("id", canvasId)
document.body!!.appendChild(canvasElement)
Expand Down Expand Up @@ -96,7 +95,6 @@ class MouseEventsTest {
@OptIn(ExperimentalFoundationApi::class)
@Test
fun testOnClickWithPointerMatchers() = runTest {
if (isHeadlessBrowser()) return@runTest
val canvasElement = document.createElement("canvas") as HTMLCanvasElement
canvasElement.setAttribute("id", canvasId)
document.body!!.appendChild(canvasElement)
Expand Down Expand Up @@ -129,7 +127,6 @@ class MouseEventsTest {

@Test
fun testPointerButtonIsNullForNoClickEvents() = runTest {
if (isHeadlessBrowser()) return@runTest
val canvasElement = document.createElement("canvas") as HTMLCanvasElement
canvasElement.setAttribute("id", canvasId)
document.body!!.appendChild(canvasElement)
Expand Down

0 comments on commit 531339c

Please sign in to comment.