Browse Source

Web/controlled inputs (#1031)

* web: make TextInput manage its view state

* web: make TextInput manage its view state

* web: make TextInput manage its view state

* web: add tests for controlled TextArea and RadioButton

* Add more tests for controlled and uncontrolled inputs

* Add more tests for controlled and uncontrolled inputs

* Update date input tests

* Update time input tests

* Update date input tests

* Add docs for controlled and unctontrolled input modes

* web: improve docs for controlled inputs

* web: make benchmark add1kItems run only 2 times instead of 3 (to prevent it from throwing timeout)

Co-authored-by: Oleksandr Karpovich <oleksandr.karpovich@jetbrains.com>
pull/1048/head
Oleksandr Karpovich 3 years ago committed by GitHub
parent
commit
db53b95a31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 0
      web/benchmark-core/src/jsMain/kotlin/com/sample/content/CodeSamplesSwitcher.kt
  2. 4
      web/benchmark-core/src/jsTest/kotlin/BenchmarkTests.kt
  3. 9
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/DomApplier.kt
  4. 30
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/Attrs.kt
  5. 7
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/AttrsBuilder.kt
  6. 61
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/InputAttrsBuilder.kt
  7. 74
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/InternalControlledInputUtils.kt
  8. 10
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/TextAreaAttrsBuilder.kt
  9. 99
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt
  10. 127
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/InputElements.kt
  11. 108
      web/core/src/jsTest/kotlin/ControlledRadioGroupsTests.kt
  12. 7
      web/core/src/jsTest/kotlin/elements/EventTests.kt
  13. 20
      web/core/src/jsTest/kotlin/elements/InputsGenerateCorrectHtmlTests.kt
  14. 52
      web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/Sample.kt
  15. 7
      web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/Common.kt
  16. 412
      web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/ControlledInputsTests.kt
  17. 16
      web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/InputsTests.kt
  18. 190
      web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/UncontrolledInputsTests.kt
  19. 507
      web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/ControlledInputsTests.kt
  20. 27
      web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/InputsTests.kt
  21. 208
      web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/UncontrolledInputsTests.kt
  22. 21
      web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/common/BaseIntegrationTests.kt

0
web/benchmark-core/src/jsMain/kotlin/com/sample/content/CodeSamplesSwitcher.kt

4
web/benchmark-core/src/jsTest/kotlin/BenchmarkTests.kt

@ -65,8 +65,8 @@ class BenchmarkTests {
return duration return duration
} }
@Test // add1kItems overrides default `repeat` value (was - 5, now - 3) to avoid getting swallowed on CI @Test // add1kItems overrides default `repeat` value (was - 5, now - 2) to avoid getting swallowed on CI
fun add1kItems() = runBenchmark(name = "add1000Items", repeat = 3) { fun add1kItems() = runBenchmark(name = "add1000Items", repeat = 2) {
addNItems(1000) addNItems(1000)
} }

9
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.setProperty
import org.jetbrains.compose.web.dom.setVariable import org.jetbrains.compose.web.dom.setVariable
import kotlinx.dom.clear import kotlinx.dom.clear
import org.w3c.dom.Element import org.w3c.dom.*
import org.w3c.dom.HTMLElement
import org.w3c.dom.Node
import org.w3c.dom.get
internal class DomApplier( internal class DomApplier(
root: DomNodeWrapper root: DomNodeWrapper
@ -109,7 +106,9 @@ internal class DomElementWrapper(override val node: HTMLElement): DomNodeWrapper
fun updateProperties(list: List<Pair<(Element, Any) -> Unit, Any>>) { fun updateProperties(list: List<Pair<(Element, Any) -> Unit, Any>>) {
if (node.className.isNotEmpty()) node.className = "" if (node.className.isNotEmpty()) node.className = ""
list.forEach { it.first(node, it.second) } list.forEach {
it.first(node, it.second)
}
} }
fun updateStyleDeclarations(style: StyleHolder?) { fun updateStyleDeclarations(style: StyleHolder?) {

30
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/Attrs.kt

@ -1,5 +1,6 @@
package org.jetbrains.compose.web.attributes package org.jetbrains.compose.web.attributes
import org.jetbrains.compose.web.attributes.builders.saveControlledInputState
import org.jetbrains.compose.web.events.SyntheticSubmitEvent import org.jetbrains.compose.web.events.SyntheticSubmitEvent
import org.w3c.dom.HTMLAnchorElement import org.w3c.dom.HTMLAnchorElement
import org.w3c.dom.HTMLButtonElement import org.w3c.dom.HTMLButtonElement
@ -127,9 +128,6 @@ fun AttrsBuilder<HTMLInputElement>.autoFocus() =
fun AttrsBuilder<HTMLInputElement>.capture(value: String) = fun AttrsBuilder<HTMLInputElement>.capture(value: String) =
attr("capture", value) // type: file only attr("capture", value) // type: file only
fun AttrsBuilder<HTMLInputElement>.checked() =
attr("checked", "") // radio, checkbox
fun AttrsBuilder<HTMLInputElement>.dirName(value: String) = fun AttrsBuilder<HTMLInputElement>.dirName(value: String) =
attr("dirname", value) // text, search attr("dirname", value) // text, search
@ -202,14 +200,6 @@ fun AttrsBuilder<HTMLInputElement>.src(value: String) =
fun AttrsBuilder<HTMLInputElement>.step(value: Number) = fun AttrsBuilder<HTMLInputElement>.step(value: Number) =
attr("step", value.toString()) // numeric types only attr("step", value.toString()) // numeric types only
fun AttrsBuilder<HTMLInputElement>.valueAttr(value: String) =
attr("value", value)
fun AttrsBuilder<HTMLInputElement>.value(value: String): AttrsBuilder<HTMLInputElement> {
prop(setInputValue, value)
return this
}
/* Option attributes */ /* Option attributes */
fun AttrsBuilder<HTMLOptionElement>.value(value: String) = fun AttrsBuilder<HTMLOptionElement>.value(value: String) =
@ -299,11 +289,6 @@ fun AttrsBuilder<HTMLTextAreaElement>.rows(value: Int) =
fun AttrsBuilder<HTMLTextAreaElement>.wrap(value: TextAreaWrap) = fun AttrsBuilder<HTMLTextAreaElement>.wrap(value: TextAreaWrap) =
attr("wrap", value.str) attr("wrap", value.str)
fun AttrsBuilder<HTMLTextAreaElement>.value(value: String): AttrsBuilder<HTMLTextAreaElement> {
prop(setInputValue, value)
return this
}
/* Img attributes */ /* Img attributes */
fun AttrsBuilder<HTMLImageElement>.src(value: String): AttrsBuilder<HTMLImageElement> = fun AttrsBuilder<HTMLImageElement>.src(value: String): AttrsBuilder<HTMLImageElement> =
@ -312,8 +297,19 @@ fun AttrsBuilder<HTMLImageElement>.src(value: String): AttrsBuilder<HTMLImageEle
fun AttrsBuilder<HTMLImageElement>.alt(value: String): AttrsBuilder<HTMLImageElement> = fun AttrsBuilder<HTMLImageElement>.alt(value: String): AttrsBuilder<HTMLImageElement> =
attr("alt", value) attr("alt", value)
private val setInputValue: (HTMLInputElement, String) -> Unit = { e, v ->
internal val setInputValue: (HTMLInputElement, String) -> Unit = { e, v ->
e.value = 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 */ /* Img attributes */

7
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/AttrsBuilder.kt

@ -19,11 +19,16 @@ import org.w3c.dom.HTMLElement
*/ */
open class AttrsBuilder<TElement : Element> : EventsListenerBuilder() { open class AttrsBuilder<TElement : Element> : EventsListenerBuilder() {
internal val attributesMap = mutableMapOf<String, String>() internal val attributesMap = mutableMapOf<String, String>()
val styleBuilder = StyleBuilderImpl() internal val styleBuilder = StyleBuilderImpl()
internal val propertyUpdates = mutableListOf<Pair<(Element, Any) -> Unit, Any>>() internal val propertyUpdates = mutableListOf<Pair<(Element, Any) -> Unit, Any>>()
internal var refEffect: (DisposableEffectScope.(TElement) -> DisposableEffectResult)? = null 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 * [style] add inline CSS-style properties to the element via [StyleBuilder] context
* *

61
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.jetbrains.compose.web.events.SyntheticSelectEvent
import org.w3c.dom.HTMLInputElement 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<ValueType>( class InputAttrsBuilder<ValueType>(
val inputType: InputType<ValueType> val inputType: InputType<ValueType>
) : AttrsBuilder<HTMLInputElement>() { ) : AttrsBuilder<HTMLInputElement>() {
fun value(value: String): InputAttrsBuilder<ValueType> {
when (inputType) {
InputType.Checkbox,
InputType.Radio,
InputType.Hidden,
InputType.Submit -> attr("value", value)
else -> prop(setInputValue, value)
}
return this
}
fun value(value: Number): InputAttrsBuilder<ValueType> {
value(value.toString())
return this
}
fun checked(checked: Boolean): InputAttrsBuilder<ValueType> {
prop(setCheckedValue, checked)
return this
}
fun defaultChecked(): InputAttrsBuilder<ValueType> {
attr("checked", "")
return this
}
fun defaultValue(value: String): InputAttrsBuilder<ValueType> {
attr("value", value)
return this
}
fun defaultValue(value: Number): InputAttrsBuilder<ValueType> {
attr("value", value.toString())
return this
}
fun onInvalid( fun onInvalid(
options: Options = Options.DEFAULT, options: Options = Options.DEFAULT,
listener: (SyntheticEvent<HTMLInputElement>) -> Unit listener: (SyntheticEvent<HTMLInputElement>) -> Unit
@ -51,3 +103,12 @@ class InputAttrsBuilder<ValueType>(
listeners.add(SelectEventListener(options, listener)) 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
}

74
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<JsWeakMap>()
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 <V : Any> 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<String, MutableSet<HTMLInputElement>>()
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<HTMLInputElement>.DisposeRadioGroupEffect() {
DisposableRefEffect { ref ->
onDispose {
controlledRadioGroups[ref.name]?.remove(ref)
if (controlledRadioGroups[ref.name]?.isEmpty() == true) {
controlledRadioGroups.remove(ref.name)
}
}
}
}

10
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<HTMLTextAreaElement>() { class TextAreaAttrsBuilder : AttrsBuilder<HTMLTextAreaElement>() {
fun value(value: String): AttrsBuilder<HTMLTextAreaElement> {
prop(setInputValue, value)
return this
}
fun defaultValue(value: String): AttrsBuilder<HTMLTextAreaElement> {
prop(setTextAreaDefaultValue, value)
return this
}
fun onInput( fun onInput(
options: Options = Options.DEFAULT, options: Options = Options.DEFAULT,
listener: (SyntheticInputEvent<String, HTMLTextAreaElement>) -> Unit listener: (SyntheticInputEvent<String, HTMLTextAreaElement>) -> Unit

99
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.Composable
import androidx.compose.runtime.ComposeNode 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 androidx.compose.web.attributes.SelectAttrsBuilder
import org.jetbrains.compose.web.attributes.builders.TextAreaAttrsBuilder
import org.jetbrains.compose.web.DomApplier import org.jetbrains.compose.web.DomApplier
import org.jetbrains.compose.web.DomNodeWrapper import org.jetbrains.compose.web.DomNodeWrapper
import kotlinx.browser.document import kotlinx.browser.document
import org.jetbrains.compose.web.attributes.* 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.CSSRuleDeclarationList
import org.jetbrains.compose.web.css.StyleSheetBuilder import org.jetbrains.compose.web.css.StyleSheetBuilder
import org.jetbrains.compose.web.css.StyleSheetBuilderImpl import org.jetbrains.compose.web.css.StyleSheetBuilderImpl
@ -650,22 +650,51 @@ fun Section(
content = content content = content
) )
/**
* Adds <textarea> element.
* Same as [Input], [TextArea] has two modes: controlled and uncontrolled.
*
* Controlled mode means that <textarea> value can be changed only by passing a different [value].
* Uncontrolled mode means that <textarea> uses its default state management.
*
* To use controlled mode, simply pass non-null [value].
* By default [value] is null and [TextArea] will be in uncontrolled mode.
*
* Use `defaultValue("some default text")` in uncontrolled mode to set a default text if needed:
*
* ```
* TextArea {
* defaultValue("Some Default Text")
* }
* ```
*/
@Composable @Composable
fun TextArea( fun TextArea(
attrs: (TextAreaAttrsBuilder.() -> Unit)? = null, value: String? = null,
value: String attrs: (TextAreaAttrsBuilder.() -> Unit)? = null
) = TagElement( ) {
// if firstProvidedValueWasNotNull then TextArea behaves as controlled input
val firstProvidedValueWasNotNull = remember { value != null }
TagElement(
elementBuilder = TextArea, elementBuilder = TextArea,
applyAttrs = { applyAttrs = {
val taab = TextAreaAttrsBuilder() val taab = TextAreaAttrsBuilder()
if (attrs != null) { if (attrs != null) {
taab.attrs() taab.attrs()
} }
taab.value(value) if (firstProvidedValueWasNotNull) {
this.copyFrom(taab) taab.value(value ?: "")
} }
) {
Text(value) taab.onInput {
restoreControlledTextAreaState(it.target)
}
this.copyFrom(taab)
},
content = null
)
} }
@Composable @Composable
@ -921,6 +950,37 @@ fun Style(
Style(applyAttrs, builder.cssRules) Style(applyAttrs, builder.cssRules)
} }
/**
* Adds <input> element of [type].
*
* Input has two modes: controlled and uncontrolled.
* Uncontrolled is a default mode. The input's state is managed by [HTMLInputElement] itself.
* Controlled mode means that the input's state is managed by compose state.
* To use Input in controlled mode, it's required to set its state by calling `value(String|Number)`.
*
* Consider using [TextInput], [CheckboxInput], [RadioInput], [NumberInput] etc. to use controlled mode.
*
* Code example of a controlled Input:
* ```
* val textInputState by remember { mutableStateOf("initial text") }
*
* Input(type = InputType.Text) {
* value(textInputState)
* onInput { event ->
* textInputState = event.value // without updating the state, the <input> will keep showing an old value
* }
* }
* ```
*
* Code example of an uncontrolled Input:
* ```
* Input(type = InputType.Text) {
* defaultValue("someDefaultValue") // calling `defaultValue` is optional
* // No value set explicitly.
* // Whatever typed into the input will be immediately displayed in UI without handling any onInput events.
* }
* ```
*/
@Composable @Composable
fun <K> Input( fun <K> Input(
type: InputType<K>, type: InputType<K>,
@ -932,21 +992,22 @@ fun <K> Input(
val inputAttrsBuilder = InputAttrsBuilder(type) val inputAttrsBuilder = InputAttrsBuilder(type)
inputAttrsBuilder.type(type) inputAttrsBuilder.type(type)
inputAttrsBuilder.attrs() inputAttrsBuilder.attrs()
inputAttrsBuilder.onInput {
restoreControlledInputState(type = type, inputElement = it.target)
}
this.copyFrom(inputAttrsBuilder) this.copyFrom(inputAttrsBuilder)
}, },
content = null content = {
if (type == InputType.Radio) {
DisposeRadioGroupEffect()
}
}
) )
} }
@Composable @Composable
fun <K> Input(type: InputType<K>) { fun <K> Input(type: InputType<K>) {
TagElement( Input(type) {}
elementBuilder = Input,
applyAttrs = {
val inputAttrsBuilder = InputAttrsBuilder(type)
inputAttrsBuilder.type(type)
this.copyFrom(inputAttrsBuilder)
},
content = null
)
} }

127
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/InputElements.kt

@ -13,54 +13,110 @@ private fun InputAttrsBuilder<String>.applyAttrsWithStringValue(
attrs() attrs()
} }
/**
* It's a controlled Input of [InputType.Checkbox].
* Controlled input means that its state is always equal [checked] value.
* If you need an uncontrolled behaviour, see [Input].
*
* @see [Input] for more details on controlled and uncontrolled modes.
*/
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun CheckboxInput(checked: Boolean = false, attrs: InputAttrsBuilder<Boolean>.() -> Unit = {}) { fun CheckboxInput(checked: Boolean = false, attrs: InputAttrsBuilder<Boolean>.() -> Unit = {}) {
Input( Input(
type = InputType.Checkbox, type = InputType.Checkbox,
attrs = { attrs = {
if (checked) checked() checked(checked)
this.attrs() this.attrs()
} }
) )
} }
/**
* It's a controlled Input of [InputType.Date].
* Controlled input means that its state is always equal [value].
* If you need an uncontrolled behaviour, see [Input].
*
* @see [Input] for more details on controlled and uncontrolled modes.
*/
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun DateInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) { fun DateInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Date, attrs = { applyAttrsWithStringValue(value, attrs) }) Input(type = InputType.Date, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
/**
* It's a controlled Input of [InputType.DateTimeLocal].
* Controlled input means that its state is always equal [value].
* If you need an uncontrolled behaviour, see [Input].
*
* @see [Input] for more details on controlled and uncontrolled modes.
*/
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun DateTimeLocalInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) { fun DateTimeLocalInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.DateTimeLocal, attrs = { applyAttrsWithStringValue(value, attrs) }) Input(type = InputType.DateTimeLocal, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
/**
* It's a controlled Input of [InputType.Email].
* Controlled input means that its state is always equal [value].
* If you need an uncontrolled behaviour, see [Input].
*
* @see [Input] for more details on controlled and uncontrolled modes.
*/
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun EmailInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) { fun EmailInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Email, attrs = { applyAttrsWithStringValue(value, attrs) }) Input(type = InputType.Email, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
/**
* It's a controlled Input of [InputType.File].
* Controlled input means that its state is always equal [value].
* If you need an uncontrolled behaviour, see [Input].
*
* @see [Input] for more details on controlled and uncontrolled modes.
*/
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun FileInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) { fun FileInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.File, attrs = { applyAttrsWithStringValue(value, attrs) }) Input(type = InputType.File, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
/**
* It's a controlled Input of [InputType.Hidden].
* Controlled input means that its state is always equal [value].
* If you need an uncontrolled behaviour, see [Input].
*
* @see [Input] for more details on controlled and uncontrolled modes.
*/
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun HiddenInput(attrs: InputAttrsBuilder<String>.() -> Unit = {}) { fun HiddenInput(attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Hidden, attrs = attrs) Input(type = InputType.Hidden, attrs = attrs)
} }
/**
* It's a controlled Input of [InputType.Month].
* Controlled input means that its state is always equal [value].
* If you need an uncontrolled behaviour, see [Input].
*
* @see [Input] for more details on controlled and uncontrolled modes.
*/
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun MonthInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) { fun MonthInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Month, attrs = { applyAttrsWithStringValue(value, attrs) }) Input(type = InputType.Month, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
/**
* It's a controlled Input of [InputType.Number].
* Controlled input means that its state is always equal [value].
* If you need an uncontrolled behaviour, see [Input].
*
* @see [Input] for more details on controlled and uncontrolled modes.
*/
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun NumberInput( fun NumberInput(
@ -80,24 +136,45 @@ fun NumberInput(
) )
} }
/**
* It's a controlled Input of [InputType.Password].
* Controlled input means that its state is always equal [value].
* If you need an uncontrolled behaviour, see [Input].
*
* @see [Input] for more details on controlled and uncontrolled modes.
*/
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun PasswordInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) { fun PasswordInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Password, attrs = { applyAttrsWithStringValue(value, attrs) }) Input(type = InputType.Password, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
/**
* It's a controlled Input of [InputType.Radio].
* Controlled input means that its state is always equal [checked] value.
* If you need an uncontrolled behaviour, see [Input].
*
* @see [Input] for more details on controlled and uncontrolled modes.
*/
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun RadioInput(checked: Boolean = false, attrs: InputAttrsBuilder<Boolean>.() -> Unit = {}) { fun RadioInput(checked: Boolean = false, attrs: InputAttrsBuilder<Boolean>.() -> Unit = {}) {
Input( Input(
type = InputType.Radio, type = InputType.Radio,
attrs = { attrs = {
if (checked) checked() checked(checked)
attrs() attrs()
} }
) )
} }
/**
* It's a controlled Input of [InputType.Range].
* Controlled input means that its state is always equal [value].
* If you need an uncontrolled behaviour, see [Input].
*
* @see [Input] for more details on controlled and uncontrolled modes.
*/
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun RangeInput( fun RangeInput(
@ -119,42 +196,88 @@ fun RangeInput(
) )
} }
/**
* It's a controlled Input of [InputType.Search].
* Controlled input means that its state is always equal [value].
* If you need an uncontrolled behaviour, see [Input].
*
* @see [Input] for more details on controlled and uncontrolled modes.
*/
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun SearchInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) { fun SearchInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Search, attrs = { applyAttrsWithStringValue(value, attrs) }) Input(type = InputType.Search, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
/**
* It's a controlled Input of [InputType.Submit].
* If you need an uncontrolled behaviour, see [Input].
*/
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun SubmitInput(attrs: InputAttrsBuilder<Unit>.() -> Unit = {}) { fun SubmitInput(attrs: InputAttrsBuilder<Unit>.() -> Unit = {}) {
Input(type = InputType.Submit, attrs = attrs) Input(type = InputType.Submit, attrs = attrs)
} }
/**
* It's a controlled Input of [InputType.Tel].
* Controlled input means that its state is always equal [value].
* If you need an uncontrolled behaviour, see [Input].
*
* @see [Input] for more details on controlled and uncontrolled modes.
*/
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun TelInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) { fun TelInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Tel, attrs = { applyAttrsWithStringValue(value, attrs) }) Input(type = InputType.Tel, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
/**
* It's a controlled Input of [InputType.Text].
* Controlled input means that its state is always equal [value].
* If you need an uncontrolled behaviour, see [Input].
*
* @see [Input] for more details on controlled and uncontrolled modes.
*/
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun TextInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) { fun TextInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Text, attrs = { applyAttrsWithStringValue(value, attrs) }) Input(type = InputType.Text, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
/**
* It's a controlled Input of [InputType.Time].
* Controlled input means that its state is always equal [value].
* If you need an uncontrolled behaviour, see [Input].
*
* @see [Input] for more details on controlled and uncontrolled modes.
*/
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun TimeInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) { fun TimeInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Time, attrs = { applyAttrsWithStringValue(value, attrs) }) Input(type = InputType.Time, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
/**
* It's a controlled Input of [InputType.Url].
* Controlled input means that its state is always equal [value].
* If you need an uncontrolled behaviour, see [Input].
*
* @see [Input] for more details on controlled and uncontrolled modes.
*/
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun UrlInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) { fun UrlInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Url, attrs = { applyAttrsWithStringValue(value, attrs) }) Input(type = InputType.Url, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
/**
* It's a controlled Input of [InputType.Week].
* Controlled input means that its state is always equal [value].
* If you need an uncontrolled behaviour, see [Input].
*
* @see [Input] for more details on controlled and uncontrolled modes.
*/
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun WeekInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) { fun WeekInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {

108
web/core/src/jsTest/kotlin/ControlledRadioGroupsTests.kt

@ -0,0 +1,108 @@
package org.jetbrains.compose.web.core.tests
import androidx.compose.runtime.*
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.attributes.builders.controlledRadioGroups
import org.jetbrains.compose.web.attributes.name
import org.jetbrains.compose.web.dom.RadioInput
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class ControlledRadioGroupsTests {
@Test
fun controlledRadioGroupGetsUpdated() = runTest {
var countOfRadio by mutableStateOf(0)
composition {
repeat(countOfRadio) {
key (it) {
RadioInput(checked = false) {
id("r$it")
name("group1")
}
}
}
}
assertEquals(0, controlledRadioGroups.size)
countOfRadio = 5
waitChanges()
assertEquals(1, controlledRadioGroups.size)
assertTrue(controlledRadioGroups.keys.first() == "group1")
assertEquals(5, controlledRadioGroups["group1"]!!.size)
countOfRadio = 2
waitChanges()
assertEquals(1, controlledRadioGroups.size)
assertTrue(controlledRadioGroups.keys.first() == "group1")
assertEquals(2, controlledRadioGroups["group1"]!!.size)
countOfRadio = 0
waitChanges()
assertEquals(0, controlledRadioGroups.size)
}
@Test
fun multipleControlledRadioGroupsGetUpdated() = runTest {
var countOfRadioG1 by mutableStateOf(0)
var countOfRadioG2 by mutableStateOf(0)
composition {
Div {
repeat(countOfRadioG1) {
key(it) {
RadioInput(checked = false) {
id("r1-$it")
name("group1")
}
}
}
}
Div {
repeat(countOfRadioG2) {
key(it) {
RadioInput(checked = false) {
id("r2-$it")
name("group2")
}
}
}
}
}
assertEquals(0, controlledRadioGroups.size)
countOfRadioG1 = 5
countOfRadioG2 = 10
waitChanges()
assertEquals(2, controlledRadioGroups.size)
assertEquals(5, controlledRadioGroups["group1"]!!.size)
assertEquals(10, controlledRadioGroups["group2"]!!.size)
countOfRadioG2 = 2
waitChanges()
assertEquals(2, controlledRadioGroups.size)
assertEquals(5, controlledRadioGroups["group1"]!!.size)
assertEquals(2, controlledRadioGroups["group2"]!!.size)
countOfRadioG1 = 0
waitChanges()
assertEquals(1, controlledRadioGroups.size)
assertEquals(2, controlledRadioGroups["group2"]!!.size)
countOfRadioG2 = 0
waitChanges()
assertEquals(0, controlledRadioGroups.size)
}
}

7
web/core/src/jsTest/kotlin/elements/EventTests.kt

@ -81,11 +81,10 @@ class EventTests {
composition { composition {
TextArea( TextArea(
{
onInput { handled = true }
},
value = "" value = ""
) ) {
onInput { handled = true }
}
} }
val radio = root.firstChild as HTMLTextAreaElement val radio = root.firstChild as HTMLTextAreaElement

20
web/core/src/jsTest/kotlin/elements/InputsGenerateCorrectHtmlTests.kt

@ -4,9 +4,11 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import org.jetbrains.compose.web.attributes.* import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.core.tests.asHtmlElement
import org.jetbrains.compose.web.core.tests.runTest import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.core.tests.waitForAnimationFrame import org.jetbrains.compose.web.core.tests.waitForAnimationFrame
import org.jetbrains.compose.web.dom.* import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.renderComposable
import org.w3c.dom.HTMLInputElement import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLTextAreaElement import org.w3c.dom.HTMLTextAreaElement
import kotlin.test.Test import kotlin.test.Test
@ -405,9 +407,9 @@ class InputsGenerateCorrectHtmlTests {
@Test @Test
fun textAreaWithAutoComplete() = runTest { fun textAreaWithAutoComplete() = runTest {
composition { composition {
TextArea({ TextArea(attrs = {
autoComplete(AutoComplete.email) autoComplete(AutoComplete.email)
}, value = "") })
} }
assertEquals("""<textarea autocomplete="email"></textarea>""", root.innerHTML) assertEquals("""<textarea autocomplete="email"></textarea>""", root.innerHTML)
} }
@ -503,4 +505,18 @@ class InputsGenerateCorrectHtmlTests {
assertEquals("text", (root.firstChild as HTMLTextAreaElement).value) 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("<textarea></textarea><textarea>not-empty-default-value</textarea>", root.innerHTML)
}
} }

52
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.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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.className
import org.jetbrains.compose.web.css.selectors.hover import org.jetbrains.compose.web.css.selectors.hover
import org.jetbrains.compose.web.css.selectors.plus 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.renderComposableInBody
import org.jetbrains.compose.web.sample.tests.launchTestCase import org.jetbrains.compose.web.sample.tests.launchTestCase
import kotlinx.browser.window import kotlinx.browser.window
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch 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.css.*
import org.jetbrains.compose.web.dom.*
import org.w3c.dom.url.URLSearchParams import org.w3c.dom.url.URLSearchParams
class State { class State {
@ -143,6 +134,45 @@ fun main() {
println("renderComposable") println("renderComposable")
val counter = remember { mutableStateOf(0) } 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<Number>(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) CounterApp(counter)
val inputValue = remember { mutableStateOf("") } val inputValue = remember { mutableStateOf("") }

7
web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/Common.kt

@ -1,7 +1,9 @@
package org.jetbrains.compose.web.sample.tests package org.jetbrains.compose.web.sample.tests
import androidx.compose.runtime.Composable 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.SelectElementTests
import androidx.compose.web.sample.tests.UncontrolledInputsTests
import org.jetbrains.compose.web.dom.Span import org.jetbrains.compose.web.dom.Span
import org.jetbrains.compose.web.dom.Text import org.jetbrains.compose.web.dom.Text
import org.jetbrains.compose.web.renderComposableInBody import org.jetbrains.compose.web.renderComposableInBody
@ -33,7 +35,10 @@ internal val testCases = mutableMapOf<String, TestCase>()
fun launchTestCase(testCaseId: String) { fun launchTestCase(testCaseId: String) {
// this makes test cases get initialised: // this makes test cases get initialised:
listOf<Any>(TestCases1(), InputsTests(), EventsTests(), SelectElementTests()) listOf<Any>(
TestCases1(), InputsTests(), EventsTests(),
SelectElementTests(), ControlledInputsTests(), UncontrolledInputsTests()
)
if (testCaseId !in testCases) error("Test Case '$testCaseId' not found") if (testCaseId !in testCases) error("Test Case '$testCaseId' not found")

412
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
}
}
}
}

16
web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/InputsTests.kt

@ -14,7 +14,6 @@ class InputsTests {
TestText(value = state) TestText(value = state)
TextArea( TextArea(
value = state,
attrs = { attrs = {
id("input") id("input")
onInput { state = it.value } onInput { state = it.value }
@ -23,14 +22,14 @@ class InputsTests {
} }
val textInputGetsPrinted by testCase { val textInputGetsPrinted by testCase {
var state by remember { mutableStateOf("") } var state by remember { mutableStateOf("Initial-") }
TestText(value = state) TestText(value = state)
Input( Input(
type = InputType.Text, type = InputType.Text,
attrs = { attrs = {
value(state) defaultValue(state)
id("input") id("input")
onInput { state = it.value } onInput { state = it.value }
} }
@ -46,9 +45,7 @@ class InputsTests {
type = InputType.Checkbox, type = InputType.Checkbox,
attrs = { attrs = {
id("checkbox") id("checkbox")
if (checked) { checked(checked)
checked()
}
onInput { checked = !checked } onInput { checked = !checked }
} }
) )
@ -227,7 +224,8 @@ class InputsTests {
P { TestText(state) } P { TestText(state) }
Div { Div {
TextArea(value = state, attrs = { TextArea(attrs = {
defaultValue(state)
id("textArea") id("textArea")
onChange { state = it.value } onChange { state = it.value }
}) })
@ -243,7 +241,7 @@ class InputsTests {
Div { Div {
TextInput(value = "", attrs = { TextInput(value = inputState, attrs = {
id("textInput") id("textInput")
onBeforeInput { onBeforeInput {
state = it.data ?: "" state = it.data ?: ""
@ -264,7 +262,7 @@ class InputsTests {
Div { Div {
TextArea(value = "", attrs = { TextArea(attrs = {
id("textArea") id("textArea")
onBeforeInput { onBeforeInput {
state = it.data ?: "" state = it.data ?: ""

190
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
}
}
}
}

507
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
}
}

27
web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/InputsTests.kt

@ -25,11 +25,12 @@ class InputsTests : BaseIntegrationTests() {
@ResolveDrivers @ResolveDrivers
fun `text input gets printed`(driver: WebDriver) { fun `text input gets printed`(driver: WebDriver) {
driver.openTestPage("textInputGetsPrinted") driver.openTestPage("textInputGetsPrinted")
driver.waitTextToBe(textId = "txt", value = "Initial-")
val input = driver.findElement(By.id("input")) val input = driver.findElement(By.id("input"))
input.sendKeys("Hello World!") input.sendKeys("Hello World!")
driver.waitTextToBe(textId = "txt", value = "Hello World!") driver.waitTextToBe(textId = "txt", value = "Initial-Hello World!")
} }
@ResolveDrivers @ResolveDrivers
@ -98,17 +99,19 @@ class InputsTests : BaseIntegrationTests() {
driver.waitTextToBe(value = "15:00") driver.waitTextToBe(value = "15:00")
} }
// @_root_ide_package_.org.jetbrains.compose.web.tests.integration.common.ResolveDrivers @ResolveDrivers
// fun `date input updates the text`() { fun `date input updates the text`(driver: WebDriver) {
// openTestPage("dateInputChangesText") driver.openTestPage("dateInputChangesText")
//
// waitTextToBe(value = "") driver.waitTextToBe(value = "")
//
// val timeInput = driver.findElement(By.id("date")) val dateInput = driver.findElement(By.id("date"))
//
// timeInput.sendKeys("12102021") // we use the same value of month and day here to avoid a need for a more complex formatting
// waitTextToBe(value = "2021-10-12") driver.sendKeysForDateInput(dateInput, 2021, 10, 10)
// }
driver.waitTextToBe(value = "2021-10-10")
}
// @_root_ide_package_.org.jetbrains.compose.web.tests.integration.common.ResolveDrivers // @_root_ide_package_.org.jetbrains.compose.web.tests.integration.common.ResolveDrivers
// fun `dateTimeLocal input updates the text`() { // WARNING: It's not supported in Firefox // fun `dateTimeLocal input updates the text`() { // WARNING: It's not supported in Firefox

208
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</")) // inner text keeps default value
check(textArea.getAttribute("value") == "defaultTextAreaValue-TypedText") // this checks the `value` property of the textarea
check(textArea.getAttribute("data-text-area-value") == "defaultTextAreaValue-TypedText")
}
@ResolveDrivers
fun checkBoxDefaultCheckedRemainsTheSameButCheckedCanBeChanged(driver: WebDriver) {
driver.openTestPage("checkBoxDefaultCheckedRemainsTheSameButCheckedCanBeChanged")
val checkbox = driver.findElement(By.id("checkbox"))
val innerTextOfCheckbox1 = driver.outerHtmlOfElementWithId("checkbox")
check(innerTextOfCheckbox1.contains("checked"))
check(checkbox.getAttribute("value") == "checkbox-value")
check(checkbox.getAttribute("data-checkbox") == "true")
check(checkbox.isSelected)
checkbox.click()
val innerTextOfCheckbox2 = driver.outerHtmlOfElementWithId("checkbox")
check(innerTextOfCheckbox2.contains("checked"))
check(checkbox.getAttribute("value") == "checkbox-value")
check(checkbox.getAttribute("data-checkbox") == "false")
check(!checkbox.isSelected)
}
@ResolveDrivers
fun radioDefaultCheckedRemainsTheSameButCheckedCanBeChanged(driver: WebDriver) {
driver.openTestPage("radioDefaultCheckedRemainsTheSameButCheckedCanBeChanged")
val radio1 = driver.findElement(By.id("radio1"))
val radio2 = driver.findElement(By.id("radio2"))
check(radio1.isSelected)
check(!radio2.isSelected)
check(driver.outerHtmlOfElementWithId("radio1").contains("checked"))
check(!driver.outerHtmlOfElementWithId("radio2").contains("checked"))
radio2.click()
check(!radio1.isSelected)
check(radio2.isSelected)
check(driver.outerHtmlOfElementWithId("radio1").contains("checked"))
check(!driver.outerHtmlOfElementWithId("radio2").contains("checked"))
}
@ResolveDrivers
fun numberDefaultValueRemainsTheSameButValueCanBeChanged(driver: WebDriver) {
driver.openTestPage("numberDefaultValueRemainsTheSameButValueCanBeChanged")
driver.waitTextToBe(value = "Value = None")
val numberInput = driver.findElement(By.id("numberInput"))
check(numberInput.getAttribute("value") == "11")
numberInput.sendKeys("5")
driver.waitTextToBe(value = "Value = 511")
check(numberInput.getAttribute("value") == "511")
check(driver.outerHtmlOfElementWithId("numberInput").contains("value=\"11\""))
}
@ResolveDrivers
fun rangeDefaultValueRemainsTheSameButValueCanBeChanged(driver: WebDriver) {
driver.openTestPage("rangeDefaultValueRemainsTheSameButValueCanBeChanged")
driver.waitTextToBe(value = "Value = None")
val numberInput = driver.findElement(By.id("rangeInput"))
check(numberInput.getAttribute("value") == "7")
numberInput.sendKeys(Keys.ARROW_RIGHT)
driver.waitTextToBe(value = "Value = 8")
numberInput.sendKeys(Keys.ARROW_RIGHT)
driver.waitTextToBe(value = "Value = 9")
check(numberInput.getAttribute("value") == "9")
check(driver.outerHtmlOfElementWithId("rangeInput").contains("value=\"7\""))
}
@ResolveDrivers
fun emailDefaultValueRemainsTheSameButValueCanBeChanged(driver: WebDriver) {
driver.openTestPage("emailDefaultValueRemainsTheSameButValueCanBeChanged")
driver.waitTextToBe(value = "Value = None")
val emailInput = driver.findElement(By.id("emailInput"))
check(emailInput.getAttribute("value") == "a@a.abc")
emailInput.clear()
emailInput.sendKeys("u@u.com")
driver.waitTextToBe(value = "Value = u@u.com")
check(emailInput.getAttribute("value") == "u@u.com")
check(driver.outerHtmlOfElementWithId("emailInput").contains("value=\"a@a.abc\""))
}
@ResolveDrivers
fun passwordDefaultValueRemainsTheSameButValueCanBeChanged(driver: WebDriver) {
driver.openTestPage("passwordDefaultValueRemainsTheSameButValueCanBeChanged")
driver.waitTextToBe(value = "Value = None")
val passwordInput = driver.findElement(By.id("passwordInput"))
check(passwordInput.getAttribute("value") == "1111")
passwordInput.clear()
passwordInput.sendKeys("a")
driver.waitTextToBe(value = "Value = a")
check(passwordInput.getAttribute("value") == "a")
check(driver.outerHtmlOfElementWithId("passwordInput").contains("value=\"1111\""))
}
@ResolveDrivers
fun searchDefaultValueRemainsTheSameButValueCanBeChanged(driver: WebDriver) {
driver.openTestPage("searchDefaultValueRemainsTheSameButValueCanBeChanged")
driver.waitTextToBe(value = "Value = None")
val searchInput = driver.findElement(By.id("searchInput"))
check(searchInput.getAttribute("value") == "kotlin")
searchInput.clear()
searchInput.sendKeys("j")
driver.waitTextToBe(value = "Value = j")
check(searchInput.getAttribute("value") == "j")
check(driver.outerHtmlOfElementWithId("searchInput").contains("value=\"kotlin\""))
}
@ResolveDrivers
fun telDefaultValueRemainsTheSameButValueCanBeChanged(driver: WebDriver) {
driver.openTestPage("telDefaultValueRemainsTheSameButValueCanBeChanged")
driver.waitTextToBe(value = "None")
val telInput = driver.findElement(By.id("telInput"))
check(telInput.getAttribute("value") == "123123")
telInput.clear()
telInput.sendKeys("987654321")
driver.waitTextToBe(value = "987654321")
check(telInput.getAttribute("value") == "987654321")
check(driver.outerHtmlOfElementWithId("telInput").contains("value=\"123123\""))
}
@ResolveDrivers
fun urlDefaultValueRemainsTheSameButValueCanBeChanged(driver: WebDriver) {
driver.openTestPage("urlDefaultValueRemainsTheSameButValueCanBeChanged")
driver.waitTextToBe(value = "None")
val urlInput = driver.findElement(By.id("urlInput"))
check(urlInput.getAttribute("value") == "www.site.com")
urlInput.clear()
urlInput.sendKeys("google.com")
driver.waitTextToBe(value = "google.com")
check(urlInput.getAttribute("value") == "google.com")
check(driver.outerHtmlOfElementWithId("urlInput").contains("value=\"www.site.com\""))
}
}

21
web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/common/BaseIntegrationTests.kt

@ -5,7 +5,9 @@ import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource import org.junit.jupiter.params.provider.MethodSource
import org.openqa.selenium.By import org.openqa.selenium.By
import org.openqa.selenium.JavascriptExecutor
import org.openqa.selenium.WebDriver import org.openqa.selenium.WebDriver
import org.openqa.selenium.WebElement
import org.openqa.selenium.chrome.ChromeDriver import org.openqa.selenium.chrome.ChromeDriver
import org.openqa.selenium.chrome.ChromeOptions import org.openqa.selenium.chrome.ChromeOptions
import org.openqa.selenium.firefox.FirefoxDriver import org.openqa.selenium.firefox.FirefoxDriver
@ -83,4 +85,23 @@ abstract class BaseIntegrationTests() {
@JvmStatic @JvmStatic
fun resolveDrivers() = Drivers.activatedDrivers fun resolveDrivers() = Drivers.activatedDrivers
} }
fun WebDriver.outerHtmlOfElementWithId(id: String): String {
val script = """
var callback = arguments[arguments.length - 1];
callback(document.getElementById("$id").outerHTML);
""".trimIndent()
return (this as JavascriptExecutor).executeAsyncScript(script).toString()
}
fun WebDriver.sendKeysForDateInput(input: WebElement, year: Int, month: Int, day: Int) {
val keys = when (this) {
is ChromeDriver -> "${day}${month}${year}"
is FirefoxDriver -> "${year}-${month}-${day}"
else -> ""
}
input.sendKeys(keys)
}
} }

Loading…
Cancel
Save