diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/content/CodeSamplesSwitcher.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/content/CodeSamplesSwitcher.kt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/web/benchmark-core/src/jsTest/kotlin/BenchmarkTests.kt b/web/benchmark-core/src/jsTest/kotlin/BenchmarkTests.kt index d1e3fb9f31..e607f80414 100644 --- a/web/benchmark-core/src/jsTest/kotlin/BenchmarkTests.kt +++ b/web/benchmark-core/src/jsTest/kotlin/BenchmarkTests.kt @@ -65,8 +65,8 @@ class BenchmarkTests { return duration } - @Test // add1kItems overrides default `repeat` value (was - 5, now - 3) to avoid getting swallowed on CI - fun add1kItems() = runBenchmark(name = "add1000Items", repeat = 3) { + @Test // add1kItems overrides default `repeat` value (was - 5, now - 2) to avoid getting swallowed on CI + fun add1kItems() = runBenchmark(name = "add1000Items", repeat = 2) { addNItems(1000) } @@ -139,4 +139,4 @@ class BenchmarkTests { duration } -} \ No newline at end of file +} diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/DomApplier.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/DomApplier.kt index d769f3e799..59ec4db962 100644 --- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/DomApplier.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/DomApplier.kt @@ -6,10 +6,7 @@ import org.jetbrains.compose.web.css.StyleHolder import org.jetbrains.compose.web.dom.setProperty import org.jetbrains.compose.web.dom.setVariable import kotlinx.dom.clear -import org.w3c.dom.Element -import org.w3c.dom.HTMLElement -import org.w3c.dom.Node -import org.w3c.dom.get +import org.w3c.dom.* internal class DomApplier( root: DomNodeWrapper @@ -109,7 +106,9 @@ internal class DomElementWrapper(override val node: HTMLElement): DomNodeWrapper fun updateProperties(list: List Unit, Any>>) { if (node.className.isNotEmpty()) node.className = "" - list.forEach { it.first(node, it.second) } + list.forEach { + it.first(node, it.second) + } } fun updateStyleDeclarations(style: StyleHolder?) { diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/Attrs.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/Attrs.kt index 38224a2a45..c41ed3a1ff 100644 --- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/Attrs.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/Attrs.kt @@ -1,5 +1,6 @@ package org.jetbrains.compose.web.attributes +import org.jetbrains.compose.web.attributes.builders.saveControlledInputState import org.jetbrains.compose.web.events.SyntheticSubmitEvent import org.w3c.dom.HTMLAnchorElement import org.w3c.dom.HTMLButtonElement @@ -127,9 +128,6 @@ fun AttrsBuilder.autoFocus() = fun AttrsBuilder.capture(value: String) = attr("capture", value) // type: file only -fun AttrsBuilder.checked() = - attr("checked", "") // radio, checkbox - fun AttrsBuilder.dirName(value: String) = attr("dirname", value) // text, search @@ -202,14 +200,6 @@ fun AttrsBuilder.src(value: String) = fun AttrsBuilder.step(value: Number) = attr("step", value.toString()) // numeric types only -fun AttrsBuilder.valueAttr(value: String) = - attr("value", value) - -fun AttrsBuilder.value(value: String): AttrsBuilder { - prop(setInputValue, value) - return this -} - /* Option attributes */ fun AttrsBuilder.value(value: String) = @@ -299,11 +289,6 @@ fun AttrsBuilder.rows(value: Int) = fun AttrsBuilder.wrap(value: TextAreaWrap) = attr("wrap", value.str) -fun AttrsBuilder.value(value: String): AttrsBuilder { - prop(setInputValue, value) - return this -} - /* Img attributes */ fun AttrsBuilder.src(value: String): AttrsBuilder = @@ -312,8 +297,19 @@ fun AttrsBuilder.src(value: String): AttrsBuilder.alt(value: String): AttrsBuilder = attr("alt", value) -private val setInputValue: (HTMLInputElement, String) -> Unit = { e, v -> + +internal val setInputValue: (HTMLInputElement, String) -> Unit = { e, v -> e.value = v + saveControlledInputState(e, v) +} + +internal val setTextAreaDefaultValue: (HTMLTextAreaElement, String) -> Unit = { e, v -> + e.innerText = v +} + +internal val setCheckedValue: (HTMLInputElement, Boolean) -> Unit = { e, v -> + e.checked = v + saveControlledInputState(e, v) } /* Img attributes */ diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/AttrsBuilder.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/AttrsBuilder.kt index 60f51cfd67..7ea8ebf5b3 100644 --- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/AttrsBuilder.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/AttrsBuilder.kt @@ -19,11 +19,16 @@ import org.w3c.dom.HTMLElement */ open class AttrsBuilder : EventsListenerBuilder() { internal val attributesMap = mutableMapOf() - val styleBuilder = StyleBuilderImpl() + internal val styleBuilder = StyleBuilderImpl() internal val propertyUpdates = mutableListOf Unit, Any>>() internal var refEffect: (DisposableEffectScope.(TElement) -> DisposableEffectResult)? = null + internal var inputControlledValueSet = false + internal var inputDefaultValueSet = false + internal var inputControlledCheckedSet = false + internal var inputDefaultCheckedSet = false + /** * [style] add inline CSS-style properties to the element via [StyleBuilder] context * diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/InputAttrsBuilder.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/InputAttrsBuilder.kt index efbd88b53e..9bed698e05 100644 --- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/InputAttrsBuilder.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/InputAttrsBuilder.kt @@ -12,10 +12,62 @@ import org.jetbrains.compose.web.events.SyntheticInputEvent import org.jetbrains.compose.web.events.SyntheticSelectEvent import org.w3c.dom.HTMLInputElement +/** + * An extension of [AttrsBuilder]. + * This class provides a set of methods specific for [Input] element: + * + * [value] - sets the current input's value. + * [defaultValue] - sets the default input's value. + * + * [checked] - sets the current checked/unchecked state of a checkbox or a radio. + * [defaultChecked] - sets the default checked state of a checkbox or a radio. + * + * [onInvalid] - adds invalid` event listener + * [onInput] - adds `input` event listener + * [onChange] - adds `change` event listener + * [onBeforeInput] - add `beforeinput` event listener + * [onSelect] - add `select` event listener + */ class InputAttrsBuilder( val inputType: InputType ) : AttrsBuilder() { + fun value(value: String): InputAttrsBuilder { + when (inputType) { + InputType.Checkbox, + InputType.Radio, + InputType.Hidden, + InputType.Submit -> attr("value", value) + else -> prop(setInputValue, value) + } + return this + } + + fun value(value: Number): InputAttrsBuilder { + value(value.toString()) + return this + } + + fun checked(checked: Boolean): InputAttrsBuilder { + prop(setCheckedValue, checked) + return this + } + + fun defaultChecked(): InputAttrsBuilder { + attr("checked", "") + return this + } + + fun defaultValue(value: String): InputAttrsBuilder { + attr("value", value) + return this + } + + fun defaultValue(value: Number): InputAttrsBuilder { + attr("value", value.toString()) + return this + } + fun onInvalid( options: Options = Options.DEFAULT, listener: (SyntheticEvent) -> Unit @@ -51,3 +103,12 @@ class InputAttrsBuilder( listeners.add(SelectEventListener(options, listener)) } } + +internal external interface JsWeakMap { + fun delete(key: Any) + fun get(key: Any): Any? + fun has(key: Any): Boolean + fun set(key: Any, value: Any): JsWeakMap +} + + diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/InternalControlledInputUtils.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/InternalControlledInputUtils.kt new file mode 100644 index 0000000000..88bbc4af5e --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/InternalControlledInputUtils.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.attributes.builders + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.NonRestartableComposable +import org.jetbrains.compose.web.attributes.InputType +import org.jetbrains.compose.web.dom.ElementScope +import org.w3c.dom.HTMLElement +import org.w3c.dom.HTMLInputElement +import org.w3c.dom.HTMLTextAreaElement + + +private val controlledInputsValuesWeakMap: JsWeakMap = js("new WeakMap();").unsafeCast() + +internal fun restoreControlledInputState(type: InputType<*>, inputElement: HTMLInputElement) { + if (controlledInputsValuesWeakMap.has(inputElement)) { + if (type == InputType.Radio) { + controlledRadioGroups[inputElement.name]?.forEach { radio -> + radio.checked = controlledInputsValuesWeakMap.get(radio).toString().toBoolean() + } + inputElement.checked = controlledInputsValuesWeakMap.get(inputElement).toString().toBoolean() + return + } + + if (type == InputType.Checkbox) { + inputElement.checked = controlledInputsValuesWeakMap.get(inputElement).toString().toBoolean() + } else { + inputElement.value = controlledInputsValuesWeakMap.get(inputElement).toString() + } + } +} + +internal fun restoreControlledTextAreaState(element: HTMLTextAreaElement) { + if (controlledInputsValuesWeakMap.has(element)) { + element.value = controlledInputsValuesWeakMap.get(element).toString() + } +} + +internal fun saveControlledInputState(element: HTMLElement, value: V) { + controlledInputsValuesWeakMap.set(element, value) + + if (element is HTMLInputElement) { + updateRadioGroupIfNeeded(element) + } +} + +// internal only for testing purposes. It actually should be private. +internal val controlledRadioGroups = mutableMapOf>() + +private fun updateRadioGroupIfNeeded(element: HTMLInputElement) { + if (element.type == "radio" && element.name.isNotEmpty()) { + if (!controlledRadioGroups.containsKey(element.name)) { + controlledRadioGroups[element.name] = mutableSetOf() + } + controlledRadioGroups[element.name]!!.add(element) + } +} + +@Composable +@NonRestartableComposable +internal fun ElementScope.DisposeRadioGroupEffect() { + DisposableRefEffect { ref -> + onDispose { + controlledRadioGroups[ref.name]?.remove(ref) + if (controlledRadioGroups[ref.name]?.isEmpty() == true) { + controlledRadioGroups.remove(ref.name) + } + } + } +} diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/TextAreaAttrsBuilder.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/TextAreaAttrsBuilder.kt index 6dd7f6d49e..ff92502cc3 100644 --- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/TextAreaAttrsBuilder.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/TextAreaAttrsBuilder.kt @@ -13,6 +13,16 @@ import org.w3c.dom.HTMLTextAreaElement class TextAreaAttrsBuilder : AttrsBuilder() { + fun value(value: String): AttrsBuilder { + prop(setInputValue, value) + return this + } + + fun defaultValue(value: String): AttrsBuilder { + prop(setTextAreaDefaultValue, value) + return this + } + fun onInput( options: Options = Options.DEFAULT, listener: (SyntheticInputEvent) -> Unit diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt index 00f77316d8..644ae0f79e 100644 --- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt @@ -2,13 +2,13 @@ package org.jetbrains.compose.web.dom import androidx.compose.runtime.Composable import androidx.compose.runtime.ComposeNode -import org.jetbrains.compose.web.attributes.builders.InputAttrsBuilder +import androidx.compose.runtime.remember import androidx.compose.web.attributes.SelectAttrsBuilder -import org.jetbrains.compose.web.attributes.builders.TextAreaAttrsBuilder import org.jetbrains.compose.web.DomApplier import org.jetbrains.compose.web.DomNodeWrapper import kotlinx.browser.document import org.jetbrains.compose.web.attributes.* +import org.jetbrains.compose.web.attributes.builders.* import org.jetbrains.compose.web.css.CSSRuleDeclarationList import org.jetbrains.compose.web.css.StyleSheetBuilder import org.jetbrains.compose.web.css.StyleSheetBuilderImpl @@ -650,22 +650,51 @@ fun Section( content = content ) +/** + * Adds """, root.innerHTML) } @@ -503,4 +505,18 @@ class InputsGenerateCorrectHtmlTests { assertEquals("text", (root.firstChild as HTMLTextAreaElement).value) } + + @Test + fun textAreaWithDefaultValueAndWithoutIt() { + val root = "div".asHtmlElement() + + renderComposable(root = root) { + TextArea() + TextArea { + defaultValue("not-empty-default-value") + } + } + + assertEquals("", root.innerHTML) + } } diff --git a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/Sample.kt b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/Sample.kt index 3f9bf17027..ec3d39f6c0 100644 --- a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/Sample.kt +++ b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/Sample.kt @@ -8,27 +8,18 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import org.jetbrains.compose.web.attributes.Draggable -import org.jetbrains.compose.web.attributes.InputType -import org.jetbrains.compose.web.attributes.name import org.jetbrains.compose.web.css.selectors.className import org.jetbrains.compose.web.css.selectors.hover import org.jetbrains.compose.web.css.selectors.plus -import org.jetbrains.compose.web.dom.A -import org.jetbrains.compose.web.dom.Button -import org.jetbrains.compose.web.dom.Div -import org.jetbrains.compose.web.dom.Input -import org.jetbrains.compose.web.dom.Style -import org.jetbrains.compose.web.dom.Text -import org.jetbrains.compose.web.dom.TextArea import org.jetbrains.compose.web.renderComposableInBody import org.jetbrains.compose.web.sample.tests.launchTestCase import kotlinx.browser.window import kotlinx.coroutines.MainScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.jetbrains.compose.web.attributes.value +import org.jetbrains.compose.web.attributes.* import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.dom.* import org.w3c.dom.url.URLSearchParams class State { @@ -143,6 +134,45 @@ fun main() { println("renderComposable") val counter = remember { mutableStateOf(0) } + CheckboxInput(checked = false) { + onInput { + println("Checkbox input = ${it.value}") + } + onChange { + println("Checkbox onChange = ${it.value}") + } + } + + var emailState by remember { mutableStateOf("") } + var rangeState by remember { mutableStateOf(10) } + + TextInput(value = emailState) { + onInput { + println("Typed value = ${it.value}") + emailState = it.value + } + } + + NumberInput(value = 10) { + onBeforeInput { println(("number onBeforeInput = ${it.value}")) } + onInput { println(("number onInput = ${it.value}")) } + onChange { println(("number onChange = ${it.value}")) } + } + + RangeInput(rangeState) { + onBeforeInput { println(("RangeInput onBeforeInput = ${it.value}")) } + onInput { + println(("RangeInput onInput = ${it.value}")) + rangeState = it.value ?: 0 + } + } + + MonthInput(value = "2021-10") { + onInput { + println("Month = ${it.value}") + } + } + CounterApp(counter) val inputValue = remember { mutableStateOf("") } diff --git a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/Common.kt b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/Common.kt index 6c8f7e25b0..98e05f20b5 100644 --- a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/Common.kt +++ b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/Common.kt @@ -1,7 +1,9 @@ package org.jetbrains.compose.web.sample.tests import androidx.compose.runtime.Composable +import androidx.compose.web.sample.tests.ControlledInputsTests import androidx.compose.web.sample.tests.SelectElementTests +import androidx.compose.web.sample.tests.UncontrolledInputsTests import org.jetbrains.compose.web.dom.Span import org.jetbrains.compose.web.dom.Text import org.jetbrains.compose.web.renderComposableInBody @@ -33,7 +35,10 @@ internal val testCases = mutableMapOf() fun launchTestCase(testCaseId: String) { // this makes test cases get initialised: - listOf(TestCases1(), InputsTests(), EventsTests(), SelectElementTests()) + listOf( + TestCases1(), InputsTests(), EventsTests(), + SelectElementTests(), ControlledInputsTests(), UncontrolledInputsTests() + ) if (testCaseId !in testCases) error("Test Case '$testCaseId' not found") diff --git a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/ControlledInputsTests.kt b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/ControlledInputsTests.kt new file mode 100644 index 0000000000..11782dd5fc --- /dev/null +++ b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/ControlledInputsTests.kt @@ -0,0 +1,412 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package androidx.compose.web.sample.tests + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import org.jetbrains.compose.web.attributes.InputType +import org.jetbrains.compose.web.attributes.name +import org.jetbrains.compose.web.dom.* +import org.jetbrains.compose.web.sample.tests.TestText +import org.jetbrains.compose.web.sample.tests.testCase + +class ControlledInputsTests { + + val textInputHardcodedValueShouldNotChange by testCase { + var onInputText by remember { mutableStateOf("None") } + + P { TestText(onInputText) } + + Div { + TextInput(value = "hardcoded", attrs = { + id("textInput") + onInput { + onInputText = it.value + } + }) + } + } + + val textInputMutableValueShouldGetOverridden by testCase { + var onInputText by remember { mutableStateOf("InitialValue") } + + P { TestText(onInputText) } + + Div { + TextInput(value = onInputText, attrs = { + id("textInput") + onInput { + onInputText = "OVERRIDDEN VALUE" + } + }) + } + } + + val textInputMutableValueShouldChange by testCase { + var onInputText by remember { mutableStateOf("InitialValue") } + + P { TestText(onInputText) } + + Div { + TextInput(value = onInputText, attrs = { + id("textInput") + onInput { + onInputText = it.value + } + }) + } + } + + val textAreaHardcodedValueShouldNotChange by testCase { + var onInputText by remember { mutableStateOf("None") } + + P { TestText(onInputText) } + + Div { + TextArea(value = "hardcoded", attrs = { + id("textArea") + onInput { + onInputText = it.value + } + }) + } + } + + val textAreaMutableValueShouldGetOverridden by testCase { + var onInputText by remember { mutableStateOf("InitialValue") } + + P { TestText(onInputText) } + + Div { + TextArea(value = onInputText, attrs = { + id("textArea") + onInput { + onInputText = "OVERRIDDEN VALUE" + } + }) + } + } + + val textAreaMutableValueShouldChange by testCase { + var onInputText by remember { mutableStateOf("InitialValue") } + + P { TestText(onInputText) } + + Div { + TextArea(value = onInputText, attrs = { + id("textArea") + onInput { + onInputText = it.value + } + }) + } + } + + val checkBoxHardcodedNeverChanges by testCase { + var checkClicked by remember { mutableStateOf(false) } + + P { TestText(checkClicked.toString()) } + + Div { + CheckboxInput(checked = false) { + id("checkbox") + onInput { + checkClicked = it.value + } + } + } + } + + val checkBoxMutableValueChanges by testCase { + var checked by remember { mutableStateOf(false) } + + P { TestText(checked.toString()) } + + Div { + CheckboxInput(checked = checked) { + id("checkbox") + onInput { + checked = it.value + } + } + } + } + + val checkBoxDefaultCheckedChangesDoesntAffectState by testCase { + var checked by remember { mutableStateOf(true) } + + P { TestText(checked.toString()) } + + Div { + Input(type = InputType.Checkbox) { + id("checkboxMirror") + if (checked) defaultChecked() + } + + Input(type = InputType.Checkbox) { + id("checkboxMain") + checked(checked) + onInput { checked = it.value } + } + } + } + + val radioHardcodedNeverChanges by testCase { + Div { + RadioInput(checked = true) { + id("radio1") + name("group1") + } + RadioInput(checked = false) { + id("radio2") + name("group1") + } + } + } + + val radioMutableCheckedChanges by testCase { + var checked by remember { mutableStateOf(0) } + + TestText("Checked - $checked") + + Div { + RadioInput(checked = checked == 1) { + id("radio1") + name("group1") + onInput { checked = 1 } + } + RadioInput(checked = checked == 2) { + id("radio2") + name("group1") + onInput { checked = 2 } + } + } + } + + val numberHardcodedNeverChanges by testCase { + var typedValue by remember { mutableStateOf("None") } + TestText(value = typedValue) + + NumberInput(value = 5, min = 0, max = 100) { + id("numberInput") + onInput { + typedValue = it.value.toString() + } + } + } + + val numberMutableChanges by testCase { + var value by remember { mutableStateOf(5) } + TestText(value = value.toString()) + + NumberInput(value = value, min = 0, max = 100) { + id("numberInput") + onInput { + value = it.value!!.toInt() + } + } + } + + val rangeHardcodedNeverChanges by testCase { + var typedValue by remember { mutableStateOf("None") } + + TestText(value = typedValue) + + RangeInput(value = 21) { + id("rangeInput") + onInput { + typedValue = it.value.toString() + } + } + } + + val rangeMutableChanges by testCase { + var value by remember { mutableStateOf(10) } + + TestText(value = value.toString()) + + RangeInput(value = value) { + id("rangeInput") + onInput { + value = it.value!!.toInt() + } + } + } + + val emailHardcodedNeverChanges by testCase { + var typedValue by remember { mutableStateOf("None") } + TestText(value = typedValue) + + EmailInput(value = "a@a.abc") { + id("emailInput") + onInput { + typedValue = it.value + } + } + } + + val emailMutableChanges by testCase { + var value by remember { mutableStateOf("") } + TestText(value = value) + + EmailInput(value = value) { + id("emailInput") + onInput { + value = it.value + } + } + } + + val passwordHardcodedNeverChanges by testCase { + var typeValue by remember { mutableStateOf("None") } + TestText(value = typeValue) + + PasswordInput(value = "123456") { + id("passwordInput") + onInput { + typeValue = it.value + } + } + } + + val passwordMutableChanges by testCase { + var value by remember { mutableStateOf("") } + TestText(value = value) + + EmailInput(value = value) { + id("passwordInput") + onInput { + value = it.value + } + } + } + + val searchHardcodedNeverChanges by testCase { + var typeValue by remember { mutableStateOf("None") } + TestText(value = typeValue) + + SearchInput(value = "hardcoded") { + id("searchInput") + onInput { + typeValue = it.value + } + } + } + + val searchMutableChanges by testCase { + var typeValue by remember { mutableStateOf("") } + TestText(value = typeValue) + + SearchInput(value = typeValue) { + id("searchInput") + onInput { + typeValue = it.value + } + } + } + + val telHardcodedNeverChanges by testCase { + var typedValue by remember { mutableStateOf("None") } + TestText(value = typedValue) + + TelInput(value = "123456") { + id("telInput") + onInput { + typedValue = it.value + } + } + } + + val telMutableChanges by testCase { + var value by remember { mutableStateOf("") } + TestText(value = value) + + TelInput(value = value) { + id("telInput") + onInput { + value = it.value + } + } + } + + val urlHardcodedNeverChanges by testCase { + var typedValue by remember { mutableStateOf("None") } + TestText(value = typedValue) + + UrlInput(value = "www.site.com") { + id("urlInput") + onInput { + typedValue = it.value + } + } + } + + val urlMutableChanges by testCase { + var value by remember { mutableStateOf("") } + TestText(value = value) + + UrlInput(value = value) { + id("urlInput") + onInput { + value = it.value + } + } + } + + val hardcodedDateInputNeverChanges by testCase { + var inputValue by remember { mutableStateOf("None") } + + TestText(inputValue) + + DateInput(value = "") { + id("dateInput") + onInput { + inputValue = "onInput Caught" + } + } + } + + val mutableDateInputChanges by testCase { + var inputValue by remember { mutableStateOf("") } + + TestText(inputValue) + + DateInput(value = inputValue) { + id("dateInput") + onInput { + inputValue = it.value + } + } + } + + val hardcodedTimeNeverChanges by testCase { + var typedValue by remember { mutableStateOf("None") } + + TestText(typedValue) + + TimeInput(value = "14:00") { + id("time") + onInput { + typedValue = "onInput Caught" + } + } + } + + val mutableTimeChanges by testCase { + var value by remember { mutableStateOf("") } + + TestText(value) + + TimeInput(value = value) { + id("time") + onInput { + value = it.value + } + } + } +} diff --git a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/InputsTests.kt b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/InputsTests.kt index ab525d76ce..fa50ed8584 100644 --- a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/InputsTests.kt +++ b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/InputsTests.kt @@ -14,7 +14,6 @@ class InputsTests { TestText(value = state) TextArea( - value = state, attrs = { id("input") onInput { state = it.value } @@ -23,14 +22,14 @@ class InputsTests { } val textInputGetsPrinted by testCase { - var state by remember { mutableStateOf("") } + var state by remember { mutableStateOf("Initial-") } TestText(value = state) Input( type = InputType.Text, attrs = { - value(state) + defaultValue(state) id("input") onInput { state = it.value } } @@ -46,9 +45,7 @@ class InputsTests { type = InputType.Checkbox, attrs = { id("checkbox") - if (checked) { - checked() - } + checked(checked) onInput { checked = !checked } } ) @@ -227,7 +224,8 @@ class InputsTests { P { TestText(state) } Div { - TextArea(value = state, attrs = { + TextArea(attrs = { + defaultValue(state) id("textArea") onChange { state = it.value } }) @@ -243,7 +241,7 @@ class InputsTests { Div { - TextInput(value = "", attrs = { + TextInput(value = inputState, attrs = { id("textInput") onBeforeInput { state = it.data ?: "" @@ -264,7 +262,7 @@ class InputsTests { Div { - TextArea(value = "", attrs = { + TextArea(attrs = { id("textArea") onBeforeInput { state = it.data ?: "" diff --git a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/UncontrolledInputsTests.kt b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/UncontrolledInputsTests.kt new file mode 100644 index 0000000000..c6dbbd6690 --- /dev/null +++ b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/UncontrolledInputsTests.kt @@ -0,0 +1,190 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package androidx.compose.web.sample.tests + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import org.jetbrains.compose.web.attributes.InputType +import org.jetbrains.compose.web.attributes.name +import org.jetbrains.compose.web.dom.* +import org.jetbrains.compose.web.sample.tests.TestText +import org.jetbrains.compose.web.sample.tests.testCase + +class UncontrolledInputsTests { + + val textInputDefaultValueRemainsTheSameButValueCanBeChanged by testCase { + var inputValue by remember { mutableStateOf("") } + + Input(type = InputType.Text) { + + id("textInput") + defaultValue("defaultInputValue") + + attr("data-input-value", inputValue) + + onInput { + inputValue = it.value + } + } + } + + val textAreaDefaultValueRemainsTheSameButValueCanBeChanged by testCase { + var inputValue by remember { mutableStateOf("") } + + TextArea { + + id("textArea") + defaultValue("defaultTextAreaValue") + + attr("data-text-area-value", inputValue) + + onInput { + inputValue = it.value + } + } + } + + val checkBoxDefaultCheckedRemainsTheSameButCheckedCanBeChanged by testCase { + var checkedValue by remember { mutableStateOf(true) } + + Input(type = InputType.Checkbox) { + id("checkbox") + defaultChecked() + value("checkbox-value") + + attr("data-checkbox", checkedValue.toString()) + + onInput { + checkedValue = it.value + } + } + } + + val radioDefaultCheckedRemainsTheSameButCheckedCanBeChanged by testCase { + var checkedValue by remember { mutableStateOf("") } + + Input(type = InputType.Radio) { + id("radio1") + defaultChecked() + value("radio-value1") + name("radiogroup") + + attr("data-radio", checkedValue) + + onInput { + checkedValue = "radio-value1" + } + } + + Input(type = InputType.Radio) { + id("radio2") + value("radio-value2") + name("radiogroup") + + attr("data-radio", checkedValue) + + onInput { + checkedValue = "radio-value2" + } + } + } + + val numberDefaultValueRemainsTheSameButValueCanBeChanged by testCase { + var typedValue by remember { mutableStateOf("None") } + + TestText(value = "Value = $typedValue") + + Input(type = InputType.Number) { + id("numberInput") + defaultValue(11) + onInput { + typedValue = it.value.toString() + } + } + } + + val rangeDefaultValueRemainsTheSameButValueCanBeChanged by testCase { + var typedValue by remember { mutableStateOf("None") } + + TestText(value = "Value = $typedValue") + + Input(type = InputType.Range) { + id("rangeInput") + defaultValue(7) + onInput { + typedValue = it.value.toString() + } + } + } + + val emailDefaultValueRemainsTheSameButValueCanBeChanged by testCase { + var typedValue by remember { mutableStateOf("None") } + TestText(value = "Value = $typedValue") + + Input(type = InputType.Email) { + id("emailInput") + defaultValue("a@a.abc") + onInput { + typedValue = it.value + } + } + } + + val passwordDefaultValueRemainsTheSameButValueCanBeChanged by testCase { + var typedValue by remember { mutableStateOf("None") } + TestText(value = "Value = $typedValue") + + Input(type = InputType.Password) { + id("passwordInput") + defaultValue("1111") + onInput { + typedValue = it.value + } + } + } + + val searchDefaultValueRemainsTheSameButValueCanBeChanged by testCase { + var typedValue by remember { mutableStateOf("None") } + TestText(value = "Value = $typedValue") + + Input(type = InputType.Search) { + id("searchInput") + defaultValue("kotlin") + onInput { + typedValue = it.value + } + } + } + + val telDefaultValueRemainsTheSameButValueCanBeChanged by testCase { + var typedValue by remember { mutableStateOf("None") } + TestText(value = typedValue) + + Input(type = InputType.Tel) { + id("telInput") + defaultValue("123123") + onInput { + typedValue = it.value + } + } + } + + val urlDefaultValueRemainsTheSameButValueCanBeChanged by testCase { + var typedValue by remember { mutableStateOf("None") } + TestText(value = typedValue) + + Input(type = InputType.Url) { + id("urlInput") + defaultValue("www.site.com") + onInput { + typedValue = it.value + } + } + } + +} diff --git a/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/ControlledInputsTests.kt b/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/ControlledInputsTests.kt new file mode 100644 index 0000000000..ce185b8d31 --- /dev/null +++ b/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/ControlledInputsTests.kt @@ -0,0 +1,507 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.tests.integration + +import org.jetbrains.compose.web.tests.integration.common.BaseIntegrationTests +import org.jetbrains.compose.web.tests.integration.common.ResolveDrivers +import org.jetbrains.compose.web.tests.integration.common.openTestPage +import org.jetbrains.compose.web.tests.integration.common.waitTextToBe +import org.junit.jupiter.api.Assumptions +import org.openqa.selenium.By +import org.openqa.selenium.Keys +import org.openqa.selenium.WebDriver +import org.openqa.selenium.chrome.ChromeDriver + +class ControlledInputsTests : BaseIntegrationTests() { + + @ResolveDrivers + fun textInputHardcodedValueShouldNotChange(driver: WebDriver) { + driver.openTestPage("textInputHardcodedValueShouldNotChange") + driver.waitTextToBe(value = "None") + + val controlledTextInput = driver.findElement(By.id("textInput")) + + controlledTextInput.sendKeys("A") + driver.waitTextToBe(value = "hardcodedA") + + controlledTextInput.sendKeys("B") + driver.waitTextToBe(value = "hardcodedB") + + controlledTextInput.sendKeys("C") + driver.waitTextToBe(value = "hardcodedC") + + check(controlledTextInput.getAttribute("value") == "hardcoded") + } + + @ResolveDrivers + fun textInputMutableValueShouldGetOverridden(driver: WebDriver) { + driver.openTestPage("textInputMutableValueShouldGetOverridden") + driver.waitTextToBe(value = "InitialValue") + + val controlledTextInput = driver.findElement(By.id("textInput")) + controlledTextInput.sendKeys("ABC") + + driver.waitTextToBe(value = "OVERRIDDEN VALUE") + check(controlledTextInput.getAttribute("value") == "OVERRIDDEN VALUE") + } + + @ResolveDrivers + fun textInputMutableValueShouldChange(driver: WebDriver) { + driver.openTestPage("textInputMutableValueShouldChange") + driver.waitTextToBe(value = "InitialValue") + + val controlledTextInput = driver.findElement(By.id("textInput")) + + controlledTextInput.sendKeys("A") + driver.waitTextToBe(value = "InitialValueA") + + controlledTextInput.sendKeys("B") + driver.waitTextToBe(value = "InitialValueAB") + + controlledTextInput.sendKeys("C") + driver.waitTextToBe(value = "InitialValueABC") + + check(controlledTextInput.getAttribute("value") == "InitialValueABC") + } + + @ResolveDrivers + fun textAreaHardcodedValueShouldNotChange(driver: WebDriver) { + driver.openTestPage("textAreaHardcodedValueShouldNotChange") + driver.waitTextToBe(value = "None") + + val controlledTextArea = driver.findElement(By.id("textArea")) + + controlledTextArea.sendKeys("A") + driver.waitTextToBe(value = "hardcodedA") + + controlledTextArea.sendKeys("B") + driver.waitTextToBe(value = "hardcodedB") + + controlledTextArea.sendKeys("C") + driver.waitTextToBe(value = "hardcodedC") + + check(controlledTextArea.getAttribute("value") == "hardcoded") + } + + @ResolveDrivers + fun textAreaMutableValueShouldGetOverridden(driver: WebDriver) { + driver.openTestPage("textAreaMutableValueShouldGetOverridden") + driver.waitTextToBe(value = "InitialValue") + + val controlledTextArea = driver.findElement(By.id("textArea")) + controlledTextArea.sendKeys("ABC") + + driver.waitTextToBe(value = "OVERRIDDEN VALUE") + check(controlledTextArea.getAttribute("value") == "OVERRIDDEN VALUE") + } + + @ResolveDrivers + fun textAreaMutableValueShouldChange(driver: WebDriver) { + driver.openTestPage("textAreaMutableValueShouldChange") + driver.waitTextToBe(value = "InitialValue") + + val controlledTextArea = driver.findElement(By.id("textArea")) + + controlledTextArea.sendKeys("A") + driver.waitTextToBe(value = "InitialValueA") + + controlledTextArea.sendKeys("B") + driver.waitTextToBe(value = "InitialValueAB") + + controlledTextArea.sendKeys("C") + driver.waitTextToBe(value = "InitialValueABC") + + check(controlledTextArea.getAttribute("value") == "InitialValueABC") + } + + @ResolveDrivers + fun checkBoxHardcodedNeverChanges(driver: WebDriver) { + driver.openTestPage("checkBoxHardcodedNeverChanges") + driver.waitTextToBe(value = "false") + + val checkbox = driver.findElement(By.id("checkbox")) + check(!checkbox.isSelected) + + checkbox.click() + + driver.waitTextToBe(value = "true") // input received but ignored + check(!checkbox.isSelected) + } + + @ResolveDrivers + fun checkBoxMutableValueChanges(driver: WebDriver) { + driver.openTestPage("checkBoxMutableValueChanges") + driver.waitTextToBe(value = "false") + + val checkbox = driver.findElement(By.id("checkbox")) + check(!checkbox.isSelected) + + checkbox.click() + + driver.waitTextToBe(value = "true") + check(checkbox.isSelected) + } + + @ResolveDrivers + fun checkBoxDefaultCheckedChangesDoesntAffectState(driver: WebDriver) { + driver.openTestPage("checkBoxDefaultCheckedChangesDoesntAffectState") + driver.waitTextToBe(value = "true") + + val mainCheckbox = driver.findElement(By.id("checkboxMain")) + val mirrorCheckbox = driver.findElement(By.id("checkboxMirror")) + + check(mainCheckbox.isSelected) + check(mirrorCheckbox.isSelected) + + mirrorCheckbox.click() + driver.waitTextToBe(value = "true") + check(!mirrorCheckbox.isSelected) + check(mainCheckbox.isSelected) + + mainCheckbox.click() + driver.waitTextToBe(value = "false") + check(!mainCheckbox.isSelected) + check(!mirrorCheckbox.isSelected) + + mainCheckbox.click() + driver.waitTextToBe(value = "true") + check(mainCheckbox.isSelected) + check(!mirrorCheckbox.isSelected) + } + + @ResolveDrivers + fun radioHardcodedNeverChanges(driver: WebDriver) { + driver.openTestPage("radioHardcodedNeverChanges") + + val radio1 = driver.findElement(By.id("radio1")) + val radio2 = driver.findElement(By.id("radio2")) + + check(radio1.isSelected) + check(!radio2.isSelected) + + check(radio1.getAttribute("name") == radio2.getAttribute("name")) + check(radio1.getAttribute("name") == "group1") + check(radio2.getAttribute("name") == "group1") + + radio2.click() + + check(radio1.isSelected) + check(!radio2.isSelected) + } + + @ResolveDrivers + fun radioMutableCheckedChanges(driver: WebDriver) { + driver.openTestPage("radioMutableCheckedChanges") + driver.waitTextToBe(value = "Checked - 0") + + val radio1 = driver.findElement(By.id("radio1")) + val radio2 = driver.findElement(By.id("radio2")) + + check(!radio1.isSelected) + check(!radio2.isSelected) + + radio2.click() + driver.waitTextToBe(value = "Checked - 2") + + check(!radio1.isSelected) + check(radio2.isSelected) + + radio1.click() + driver.waitTextToBe(value = "Checked - 1") + + check(radio1.isSelected) + check(!radio2.isSelected) + } + + @ResolveDrivers + fun numberHardcodedNeverChanges(driver: WebDriver) { + driver.openTestPage("numberHardcodedNeverChanges") + driver.waitTextToBe(value = "None") + + val numberInput = driver.findElement(By.id("numberInput")) + + check(numberInput.getAttribute("value") == "5") + + numberInput.sendKeys("1") + driver.waitTextToBe(value = "51") + + check(numberInput.getAttribute("value") == "5") + } + + @ResolveDrivers + fun numberMutableChanges(driver: WebDriver) { + driver.openTestPage("numberMutableChanges") + driver.waitTextToBe(value = "5") + + val numberInput = driver.findElement(By.id("numberInput")) + + check(numberInput.getAttribute("value") == "5") + + numberInput.sendKeys("1") + driver.waitTextToBe(value = "51") + + check(numberInput.getAttribute("value") == "51") + } + + @ResolveDrivers + fun rangeHardcodedNeverChanges(driver: WebDriver) { + driver.openTestPage("rangeHardcodedNeverChanges") + driver.waitTextToBe(value = "None") + + val numberInput = driver.findElement(By.id("rangeInput")) + + check(numberInput.getAttribute("value") == "21") + + numberInput.sendKeys(Keys.ARROW_RIGHT) + driver.waitTextToBe(value = "22") + check(numberInput.getAttribute("value") == "21") + + numberInput.sendKeys(Keys.ARROW_RIGHT) + driver.waitTextToBe(value = "22") + check(numberInput.getAttribute("value") == "21") + } + + @ResolveDrivers + fun rangeMutableChanges(driver: WebDriver) { + driver.openTestPage("rangeMutableChanges") + driver.waitTextToBe(value = "10") + + val numberInput = driver.findElement(By.id("rangeInput")) + + check(numberInput.getAttribute("value") == "10") + + numberInput.sendKeys(Keys.ARROW_RIGHT) + driver.waitTextToBe(value = "11") + check(numberInput.getAttribute("value") == "11") + + numberInput.sendKeys(Keys.ARROW_RIGHT) + driver.waitTextToBe(value = "12") + check(numberInput.getAttribute("value") == "12") + } + + @ResolveDrivers + fun emailHardcodedNeverChanges(driver: WebDriver) { + driver.openTestPage("emailHardcodedNeverChanges") + driver.waitTextToBe(value = "None") + + val emailInput = driver.findElement(By.id("emailInput")) + check(emailInput.getAttribute("value") == "a@a.abc") + + emailInput.sendKeys("@") + driver.waitTextToBe(value = "a@a.abc@") + + check(emailInput.getAttribute("value") == "a@a.abc") + } + + @ResolveDrivers + fun emailMutableChanges(driver: WebDriver) { + driver.openTestPage("emailMutableChanges") + driver.waitTextToBe(value = "") + + val emailInput = driver.findElement(By.id("emailInput")) + check(emailInput.getAttribute("value") == "") + + emailInput.sendKeys("a") + driver.waitTextToBe(value = "a") + + check(emailInput.getAttribute("value") == "a") + } + + @ResolveDrivers + fun passwordHardcodedNeverChanges(driver: WebDriver) { + driver.openTestPage("passwordHardcodedNeverChanges") + driver.waitTextToBe(value = "None") + + val passwordInput = driver.findElement(By.id("passwordInput")) + check(passwordInput.getAttribute("value") == "123456") + + passwordInput.sendKeys("a") + driver.waitTextToBe(value = "123456a") + + check(passwordInput.getAttribute("value") == "123456") + } + + @ResolveDrivers + fun passwordMutableChanges(driver: WebDriver) { + driver.openTestPage("passwordMutableChanges") + driver.waitTextToBe(value = "") + + val passwordInput = driver.findElement(By.id("passwordInput")) + check(passwordInput.getAttribute("value") == "") + + passwordInput.sendKeys("a") + driver.waitTextToBe(value = "a") + + check(passwordInput.getAttribute("value") == "a") + } + + @ResolveDrivers + fun searchHardcodedNeverChanges(driver: WebDriver) { + driver.openTestPage("searchHardcodedNeverChanges") + driver.waitTextToBe(value = "None") + + val searchInput = driver.findElement(By.id("searchInput")) + check(searchInput.getAttribute("value") == "hardcoded") + + searchInput.sendKeys("a") + driver.waitTextToBe(value = "hardcodeda") + + check(searchInput.getAttribute("value") == "hardcoded") + } + + @ResolveDrivers + fun searchMutableChanges(driver: WebDriver) { + driver.openTestPage("searchMutableChanges") + driver.waitTextToBe(value = "") + + val searchInput = driver.findElement(By.id("searchInput")) + check(searchInput.getAttribute("value") == "") + + searchInput.sendKeys("a") + driver.waitTextToBe(value = "a") + + check(searchInput.getAttribute("value") == "a") + } + + @ResolveDrivers + fun telHardcodedNeverChanges(driver: WebDriver) { + driver.openTestPage("telHardcodedNeverChanges") + driver.waitTextToBe(value = "None") + + val telInput = driver.findElement(By.id("telInput")) + check(telInput.getAttribute("value") == "123456") + + telInput.sendKeys("7") + driver.waitTextToBe(value = "1234567") + + check(telInput.getAttribute("value") == "123456") + } + + @ResolveDrivers + fun telMutableChanges(driver: WebDriver) { + driver.openTestPage("telMutableChanges") + driver.waitTextToBe(value = "") + + val telInput = driver.findElement(By.id("telInput")) + check(telInput.getAttribute("value") == "") + + telInput.sendKeys("1") + driver.waitTextToBe(value = "1") + + check(telInput.getAttribute("value") == "1") + } + + @ResolveDrivers + fun urlHardcodedNeverChanges(driver: WebDriver) { + driver.openTestPage("urlHardcodedNeverChanges") + driver.waitTextToBe(value = "None") + + val urlInput = driver.findElement(By.id("urlInput")) + check(urlInput.getAttribute("value") == "www.site.com") + + urlInput.sendKeys("a") + driver.waitTextToBe(value = "www.site.coma") + + check(urlInput.getAttribute("value") == "www.site.com") + } + + @ResolveDrivers + fun urlMutableChanges(driver: WebDriver) { + driver.openTestPage("urlMutableChanges") + driver.waitTextToBe(value = "") + + val urlInput = driver.findElement(By.id("urlInput")) + check(urlInput.getAttribute("value") == "") + + urlInput.sendKeys("w") + driver.waitTextToBe(value = "w") + + check(urlInput.getAttribute("value") == "w") + } + + @ResolveDrivers + fun hardcodedDateInputNeverChanges(driver: WebDriver) { + driver.openTestPage("hardcodedDateInputNeverChanges") + driver.waitTextToBe(value = "None") + + val dateInput = driver.findElement(By.id("dateInput")) + check(dateInput.getAttribute("value") == "") + + driver.sendKeysForDateInput(dateInput, 2021, 10, 22) + + driver.waitTextToBe(value = "onInput Caught") + check(dateInput.getAttribute("value") == "") + } + + @ResolveDrivers + fun mutableDateInputChanges(driver: WebDriver) { + // We skip chrome, since for some reason `sendKeys` doesn't work as expected when used for Controlled Input in Chrome + Assumptions.assumeTrue( + driver !is ChromeDriver, + "chrome driver doesn't work properly when using sendKeys on Controlled Input" + ) + + driver.openTestPage("mutableDateInputChanges") + driver.waitTextToBe(value = "") + + val dateInput = driver.findElement(By.id("dateInput")) + check(dateInput.getAttribute("value") == "") + + driver.sendKeysForDateInput(dateInput, 2021, 10, 22) + + driver.waitTextToBe(value = "2021-10-22") + check(dateInput.getAttribute("value") == "2021-10-22") + } + + @ResolveDrivers + fun hardcodedTimeNeverChanges(driver: WebDriver) { + driver.openTestPage("hardcodedTimeNeverChanges") + driver.waitTextToBe(value = "None") + + val timeInput = driver.findElement(By.id("time")) + check(timeInput.getAttribute("value") == "14:00") + + timeInput.sendKeys("18:31") + + driver.waitTextToBe(value = "onInput Caught") + check(timeInput.getAttribute("value") == "14:00") + } + + @ResolveDrivers + fun mutableTimeChanges(driver: WebDriver) { + // We skip chrome, since for some reason `sendKeys` doesn't work as expected when used for Controlled Input in Chrome + Assumptions.assumeTrue( + driver !is ChromeDriver, + "chrome driver doesn't work properly when using sendKeys on Controlled Input" + ) + + driver.openTestPage("mutableTimeChanges") + driver.waitTextToBe(value = "") + + val timeInput = driver.findElement(By.id("time")) + check(timeInput.getAttribute("value") == "") + + timeInput.sendKeys("18:31") + + driver.waitTextToBe(value = "18:31") + check(timeInput.getAttribute("value") == "18:31") + } + + @ResolveDrivers + fun timeInputSendKeysOnChromeFailingTest(driver: WebDriver) { + Assumptions.assumeTrue( + driver is ChromeDriver, + "this a `failing test for Chrome only` to catch when issue with sendKeys is resolved" + ) + driver.openTestPage("mutableTimeChanges") + driver.waitTextToBe(value = "") + + val timeInput = driver.findElement(By.id("time")) + + timeInput.sendKeys("18:31") + driver.waitTextToBe(value = "18:03") // it should be 18:31, but this is a failing test, so wrong value is expected + } +} diff --git a/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/InputsTests.kt b/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/InputsTests.kt index bd7c46fa56..317e0ebe9a 100644 --- a/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/InputsTests.kt +++ b/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/InputsTests.kt @@ -25,11 +25,12 @@ class InputsTests : BaseIntegrationTests() { @ResolveDrivers fun `text input gets printed`(driver: WebDriver) { driver.openTestPage("textInputGetsPrinted") + driver.waitTextToBe(textId = "txt", value = "Initial-") val input = driver.findElement(By.id("input")) input.sendKeys("Hello World!") - driver.waitTextToBe(textId = "txt", value = "Hello World!") + driver.waitTextToBe(textId = "txt", value = "Initial-Hello World!") } @ResolveDrivers @@ -98,17 +99,19 @@ class InputsTests : BaseIntegrationTests() { driver.waitTextToBe(value = "15:00") } -// @_root_ide_package_.org.jetbrains.compose.web.tests.integration.common.ResolveDrivers -// fun `date input updates the text`() { -// openTestPage("dateInputChangesText") -// -// waitTextToBe(value = "") -// -// val timeInput = driver.findElement(By.id("date")) -// -// timeInput.sendKeys("12102021") -// waitTextToBe(value = "2021-10-12") -// } + @ResolveDrivers + fun `date input updates the text`(driver: WebDriver) { + driver.openTestPage("dateInputChangesText") + + driver.waitTextToBe(value = "") + + val dateInput = driver.findElement(By.id("date")) + + // we use the same value of month and day here to avoid a need for a more complex formatting + driver.sendKeysForDateInput(dateInput, 2021, 10, 10) + + driver.waitTextToBe(value = "2021-10-10") + } // @_root_ide_package_.org.jetbrains.compose.web.tests.integration.common.ResolveDrivers // fun `dateTimeLocal input updates the text`() { // WARNING: It's not supported in Firefox diff --git a/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/UncontrolledInputsTests.kt b/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/UncontrolledInputsTests.kt new file mode 100644 index 0000000000..b3136b4c18 --- /dev/null +++ b/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/UncontrolledInputsTests.kt @@ -0,0 +1,208 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.tests.integration + +import org.jetbrains.compose.web.tests.integration.common.BaseIntegrationTests +import org.jetbrains.compose.web.tests.integration.common.ResolveDrivers +import org.jetbrains.compose.web.tests.integration.common.openTestPage +import org.jetbrains.compose.web.tests.integration.common.waitTextToBe +import org.openqa.selenium.By +import org.openqa.selenium.Keys +import org.openqa.selenium.WebDriver + +class UncontrolledInputsTests : BaseIntegrationTests() { + + @ResolveDrivers + fun textInputDefaultValueRemainsTheSameButValueCanBeChanged(driver: WebDriver) { + driver.openTestPage("textInputDefaultValueRemainsTheSameButValueCanBeChanged") + + val input = driver.findElement(By.id("textInput")) + check(input.getAttribute("value") == "defaultInputValue") + + input.sendKeys("-TypedText") + + val inputHtml = driver.outerHtmlOfElementWithId("textInput") + + check(inputHtml.contains("value=\"defaultInputValue\"")) + check(input.getAttribute("value") == "defaultInputValue-TypedText") // this checks the `value` property of the input + check(input.getAttribute("data-input-value") == "defaultInputValue-TypedText") + } + + @ResolveDrivers + fun textAreaDefaultValueRemainsTheSameButValueCanBeChanged(driver: WebDriver) { + driver.openTestPage("textAreaDefaultValueRemainsTheSameButValueCanBeChanged") + + val textArea = driver.findElement(By.id("textArea")) + check(textArea.getAttribute("value") == "defaultTextAreaValue") + + textArea.sendKeys("-TypedText") + + val innerTextOfTextArea = driver.outerHtmlOfElementWithId("textArea") + + check(innerTextOfTextArea.contains(">defaultTextAreaValue "${day}${month}${year}" + is FirefoxDriver -> "${year}-${month}-${day}" + else -> "" + } + + input.sendKeys(keys) + } }