Browse Source

Merge branch 'master' into samples_against_master

samples_against_master
Shagen Ogandzhanian 3 years ago
parent
commit
985f6fc373
  1. 2
      README.md
  2. 6
      components/VideoPlayer/demo/src/jvmMain/kotlin/org/jetbrains/compose/videoplayer/demo/Main.kt
  3. 35
      components/VideoPlayer/library/src/desktopMain/kotlin/org/jetbrains/compose/videoplayer/DesktopVideoPlayer.kt
  4. 2
      compose/frameworks/support
  5. 4
      tutorials/Context_Menu/README.md
  6. 0
      web/benchmark-core/src/jsMain/kotlin/com/sample/content/CodeSamplesSwitcher.kt
  7. 4
      web/benchmark-core/src/jsTest/kotlin/BenchmarkTests.kt
  8. 1
      web/build.gradle.kts
  9. 9
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/DomApplier.kt
  10. 4
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/RenderComposable.kt
  11. 30
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/Attrs.kt
  12. 7
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/AttrsBuilder.kt
  13. 61
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/InputAttrsBuilder.kt
  14. 74
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/InternalControlledInputUtils.kt
  15. 10
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/TextAreaAttrsBuilder.kt
  16. 107
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/filter.kt
  17. 138
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/transform.kt
  18. 111
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt
  19. 127
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/InputElements.kt
  20. 108
      web/core/src/jsTest/kotlin/ControlledRadioGroupsTests.kt
  21. 8
      web/core/src/jsTest/kotlin/DomSideEffectTests.kt
  22. 8
      web/core/src/jsTest/kotlin/InlineStyleTests.kt
  23. 161
      web/core/src/jsTest/kotlin/TestUtils.kt
  24. 130
      web/core/src/jsTest/kotlin/css/FilterTests.kt
  25. 258
      web/core/src/jsTest/kotlin/css/TransformTests.kt
  26. 14
      web/core/src/jsTest/kotlin/elements/AttributesTests.kt
  27. 7
      web/core/src/jsTest/kotlin/elements/EventTests.kt
  28. 31
      web/core/src/jsTest/kotlin/elements/InputsGenerateCorrectHtmlTests.kt
  29. 52
      web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/Sample.kt
  30. 7
      web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/Common.kt
  31. 412
      web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/ControlledInputsTests.kt
  32. 16
      web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/InputsTests.kt
  33. 190
      web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/UncontrolledInputsTests.kt
  34. 507
      web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/ControlledInputsTests.kt
  35. 27
      web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/InputsTests.kt
  36. 208
      web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/UncontrolledInputsTests.kt
  37. 21
      web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/common/BaseIntegrationTests.kt

2
README.md

@ -21,7 +21,7 @@ Preview functionality (check your application UI without building/running it) fo
* [imageviewer](examples/imageviewer) - Image Viewer application for Android and Desktop
* [issues](examples/issues) - GitHub issue tracker with an adaptive UI and ktor-client
* [game](examples/falling-balls) - Simple game
* [game](examples/falling-balls-with-web) - Simple game for web target
* [game](examples/falling-balls-web) - Simple game for web target
* [compose-bird](examples/web-compose-bird) - A flappy bird clone using Compose for Web
* [notepad](examples/notepad) - Notepad, using the new experimental Composable Window API
* [todoapp](examples/todoapp) - TODO items tracker with persistence and multiple screens

6
components/VideoPlayer/demo/src/jvmMain/kotlin/org/jetbrains/compose/videoplayer/demo/Main.kt

@ -13,7 +13,11 @@ fun main() {
) {
MaterialTheme {
DesktopTheme {
VideoPlayer("/System/Library/Compositions/Yosemite.mov", 640, 480)
VideoPlayer(
url = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
width = 640,
height = 480
)
}
}
}

35
components/VideoPlayer/library/src/desktopMain/kotlin/org/jetbrains/compose/videoplayer/DesktopVideoPlayer.kt

@ -1,22 +1,31 @@
package org.jetbrains.compose.videoplayer
import androidx.compose.desktop.SwingPanel
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCompositionContext
import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.SwingPanel
import androidx.compose.ui.graphics.Color
import uk.co.caprica.vlcj.factory.discovery.NativeDiscovery
import uk.co.caprica.vlcj.player.base.MediaPlayer
import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent
import uk.co.caprica.vlcj.player.component.EmbeddedMediaPlayerComponent
import java.util.*
@Composable
internal actual fun VideoPlayerImpl(url: String, width: Int, height: Int) {
println("Video player for $url")
NativeDiscovery().discover()
// Doesn't work on macOS, see https://github.com/caprica/vlcj/issues/887 for suggestions.
val mediaPlayerComponent = remember { EmbeddedMediaPlayerComponent() }
val mediaPlayerComponent = remember {
// see https://github.com/caprica/vlcj/issues/887#issuecomment-503288294 for why we're using CallbackMediaPlayerComponent for macOS.
if (isMacOS()) {
CallbackMediaPlayerComponent()
} else {
EmbeddedMediaPlayerComponent()
}
}
SideEffect {
val ok = mediaPlayerComponent.mediaPlayer().media().play(url)
println("play gave $ok")
@ -29,3 +38,21 @@ internal actual fun VideoPlayerImpl(url: String, width: Int, height: Int) {
}
)
}
/**
* To return mediaPlayer from player components.
* The method names are same, but they don't share the same parent/interface.
* That's why need this method.
*/
private fun Any.mediaPlayer(): MediaPlayer {
return when (this) {
is CallbackMediaPlayerComponent -> mediaPlayer()
is EmbeddedMediaPlayerComponent -> mediaPlayer()
else -> throw IllegalArgumentException("You can only call mediaPlayer() on vlcj player component")
}
}
private fun isMacOS(): Boolean {
val os = System.getProperty("os.name", "generic").lowercase(Locale.ENGLISH)
return os.indexOf("mac") >= 0 || os.indexOf("darwin") >= 0
}

2
compose/frameworks/support

@ -1 +1 @@
Subproject commit 6f678c440ed61acda22918acfb84d7356d3895ca
Subproject commit 30f010967cd12643134a80269c7eb2de65b2332b

4
tutorials/Context_Menu/README.md

@ -105,8 +105,8 @@ fun main() = singleWindowApplication(title = "Context menu") {
```
In this example Text/TextField context menus will be extended with two additional items.
## Context menu for an arbitary area
There is a possibility to create a context menu for an arbitary application window area. This is implemented using ContextMenuArea API that is
## Context menu for an arbitrary area
There is a possibility to create a context menu for an arbitrary application window area. This is implemented using ContextMenuArea API that is
similar to ContextMenuDataProvider.
```kotlin

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
}
@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)
}

1
web/build.gradle.kts

@ -37,6 +37,7 @@ subprojects {
pluginManager.withPlugin("kotlin-multiplatform") {
val printTestBundleSize by tasks.registering {
dependsOn(tasks.named("jsTest"))
doLast {
val bundlePath = buildDir.resolve(
"compileSync/test/testDevelopmentExecutable/kotlin/${rootProject.name}-${project.name}-test.js"

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.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<Pair<(Element, Any) -> 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?) {

4
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/RenderComposable.kt

@ -3,6 +3,7 @@ package org.jetbrains.compose.web
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composition
import androidx.compose.runtime.ControlledComposition
import androidx.compose.runtime.MonotonicFrameClock
import androidx.compose.runtime.DefaultMonotonicFrameClock
import androidx.compose.runtime.Recomposer
import org.jetbrains.compose.web.dom.DOMScope
@ -23,11 +24,12 @@ import org.w3c.dom.get
*/
fun <TElement : Element> renderComposable(
root: TElement,
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
content: @Composable DOMScope<TElement>.() -> Unit
): Composition {
GlobalSnapshotManager.ensureStarted()
val context = DefaultMonotonicFrameClock + JsMicrotasksDispatcher()
val context = monotonicFrameClock + JsMicrotasksDispatcher()
val recomposer = Recomposer(context)
val composition = ControlledComposition(
applier = DomApplier(DomNodeWrapper(root)),

30
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<HTMLInputElement>.autoFocus() =
fun AttrsBuilder<HTMLInputElement>.capture(value: String) =
attr("capture", value) // type: file only
fun AttrsBuilder<HTMLInputElement>.checked() =
attr("checked", "") // radio, checkbox
fun AttrsBuilder<HTMLInputElement>.dirName(value: String) =
attr("dirname", value) // text, search
@ -202,14 +200,6 @@ fun AttrsBuilder<HTMLInputElement>.src(value: String) =
fun AttrsBuilder<HTMLInputElement>.step(value: Number) =
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 */
fun AttrsBuilder<HTMLOptionElement>.value(value: String) =
@ -299,11 +289,6 @@ fun AttrsBuilder<HTMLTextAreaElement>.rows(value: Int) =
fun AttrsBuilder<HTMLTextAreaElement>.wrap(value: TextAreaWrap) =
attr("wrap", value.str)
fun AttrsBuilder<HTMLTextAreaElement>.value(value: String): AttrsBuilder<HTMLTextAreaElement> {
prop(setInputValue, value)
return this
}
/* Img attributes */
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> =
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 */

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() {
internal val attributesMap = mutableMapOf<String, String>()
val styleBuilder = StyleBuilderImpl()
internal val styleBuilder = StyleBuilderImpl()
internal val propertyUpdates = mutableListOf<Pair<(Element, Any) -> 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
*

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.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>(
val inputType: InputType<ValueType>
) : 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(
options: Options = Options.DEFAULT,
listener: (SyntheticEvent<HTMLInputElement>) -> Unit
@ -51,3 +103,12 @@ class InputAttrsBuilder<ValueType>(
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>() {
fun value(value: String): AttrsBuilder<HTMLTextAreaElement> {
prop(setInputValue, value)
return this
}
fun defaultValue(value: String): AttrsBuilder<HTMLTextAreaElement> {
prop(setTextAreaDefaultValue, value)
return this
}
fun onInput(
options: Options = Options.DEFAULT,
listener: (SyntheticInputEvent<String, HTMLTextAreaElement>) -> Unit

107
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/filter.kt

@ -0,0 +1,107 @@
/*
* 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.css
import org.jetbrains.compose.web.ExperimentalComposeWebApi
import org.w3c.dom.css.CSS
fun interface FilterFunction {
fun apply(): String
}
interface FilterBuilder {
fun blur(radius: CSSLengthValue)
fun brightness(amount: Number)
fun brightness(amount: CSSPercentageValue)
fun dropShadow(offsetX: CSSLengthValue, offsetY: CSSLengthValue)
fun dropShadow(offsetX: CSSLengthValue, offsetY: CSSLengthValue, blurRadius: CSSLengthValue)
fun dropShadow(offsetX: CSSLengthValue, offsetY: CSSLengthValue, color: CSSColorValue)
fun dropShadow(offsetX: CSSLengthValue, offsetY: CSSLengthValue, blurRadius: CSSLengthValue, color: CSSColorValue)
fun contrast(amount: Number)
fun contrast(amount: CSSPercentageValue)
fun grayscale(amount: Number)
fun grayscale(amount: CSSPercentageValue)
fun hueRotate(angle: CSSAngleValue)
fun invert(amount: Number)
fun invert(amount: CSSPercentageValue)
fun opacity(amount: Number)
fun opacity(amount: CSSPercentageValue)
fun saturate(amount: Number)
fun saturate(amount: CSSPercentageValue)
fun sepia(amount: Number)
fun sepia(amount: CSSPercentageValue)
}
private class FilterBuilderImplementation : FilterBuilder {
private val transformations = mutableListOf<FilterFunction>()
override fun blur(radius: CSSLengthValue) { transformations.add { "blur($radius)" } }
override fun brightness(amount: Number) { transformations.add { "brightness($amount)" } }
override fun brightness(amount: CSSPercentageValue) { transformations.add { "brightness($amount)" } }
override fun contrast(amount: Number) { transformations.add { "contrast($amount)" } }
override fun contrast(amount: CSSPercentageValue) { transformations.add { "contrast($amount)" } }
override fun grayscale(amount: Number) { transformations.add { "grayscale($amount)" } }
override fun grayscale(amount: CSSPercentageValue) { transformations.add { "grayscale($amount)" } }
override fun hueRotate(angle: CSSAngleValue) { transformations.add { "hue-rotate($angle)" } }
override fun toString(): String {
return transformations.joinToString(" ") { it.apply() }
}
override fun invert(amount: Number) { transformations.add { "invert($amount)" } }
override fun invert(amount: CSSPercentageValue) { transformations.add { "invert($amount)" } }
override fun opacity(amount: Number) { transformations.add { "opacity($amount)" } }
override fun opacity(amount: CSSPercentageValue) { transformations.add { "opacity($amount)" } }
override fun saturate(amount: Number) { transformations.add { "saturate($amount)" } }
override fun saturate(amount: CSSPercentageValue) { transformations.add { "saturate($amount)" } }
override fun sepia(amount: Number) { transformations.add { "sepia($amount)" } }
override fun sepia(amount: CSSPercentageValue) { transformations.add { "sepia($amount)" } }
override fun dropShadow(offsetX: CSSLengthValue, offsetY: CSSLengthValue) {
transformations.add { "drop-shadow($offsetX $offsetY)" }
}
override fun dropShadow(offsetX: CSSLengthValue, offsetY: CSSLengthValue, blurRadius: CSSLengthValue) {
transformations.add { "drop-shadow($offsetX $offsetY $blurRadius)" }
}
override fun dropShadow(offsetX: CSSLengthValue, offsetY: CSSLengthValue, color: CSSColorValue) {
transformations.add { "drop-shadow($offsetX $offsetY $color)" }
}
override fun dropShadow(
offsetX: CSSLengthValue,
offsetY: CSSLengthValue,
blurRadius: CSSLengthValue,
color: CSSColorValue
) {
transformations.add { "drop-shadow($offsetX $offsetY $blurRadius $color)" }
}
}
@ExperimentalComposeWebApi
fun StyleBuilder.filter(filterContext: FilterBuilder.() -> Unit) {
val builder = FilterBuilderImplementation()
property("filter", builder.apply(filterContext).toString())
}

138
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/transform.kt

@ -0,0 +1,138 @@
/*
* 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.css
import org.jetbrains.compose.web.ExperimentalComposeWebApi
fun interface TransformFunction {
fun apply(): String
}
interface TransformBuilder {
fun matrix(a: Number, b: Number, c: Number, d: Number, tx: Number, ty: Number): Boolean
fun matrix3d(
a1: Number, b1: Number, c1: Number, d1: Number,
a2: Number, b2: Number, c2: Number, d2: Number,
a3: Number, b3: Number, c3: Number, d3: Number,
a4: Number, b4: Number, c4: Number, d4: Number
): Boolean
fun perspective(d: CSSLengthValue): Boolean
fun rotate(a: CSSAngleValue): Boolean
fun rotate3d(x: Number, y: Number, z: Number, a: CSSAngleValue): Boolean
fun rotateX(a: CSSAngleValue): Boolean
fun rotateY(a: CSSAngleValue): Boolean
fun rotateZ(a: CSSAngleValue): Boolean
fun scale(sx: Number): Boolean
fun scale(sx: Number, sy: Number): Boolean
fun scale3d(sx: Number, sy: Number, sz: Number): Boolean
fun scaleX(s: Number): Boolean
fun scaleY(s: Number): Boolean
fun scaleZ(s: Number): Boolean
fun skew(ax: CSSAngleValue): Boolean
fun skew(ax: CSSAngleValue, ay: CSSAngleValue): Boolean
fun skewX(a: CSSAngleValue): Boolean
fun skewY(a: CSSAngleValue): Boolean
fun translate(tx: CSSLengthValue): Boolean
fun translate(tx: CSSPercentageValue): Boolean
fun translate(tx: CSSLengthValue, ty: CSSLengthValue): Boolean
fun translate(tx: CSSLengthValue, ty: CSSPercentageValue): Boolean
fun translate(tx: CSSPercentageValue, ty: CSSLengthValue): Boolean
fun translate(tx: CSSPercentageValue, ty: CSSPercentageValue): Boolean
fun translate3d(tx: CSSLengthValue, ty: CSSLengthValue, tz: CSSLengthValue): Boolean
fun translate3d(tx: CSSLengthValue, ty: CSSPercentageValue, tz: CSSLengthValue): Boolean
fun translate3d(tx: CSSPercentageValue, ty: CSSLengthValue, tz: CSSLengthValue): Boolean
fun translate3d(tx: CSSPercentageValue, ty: CSSPercentageValue, tz: CSSLengthValue): Boolean
fun translateX(tx: CSSLengthValue): Boolean
fun translateX(tx: CSSPercentageValue): Boolean
fun translateY(ty: CSSLengthValue): Boolean
fun translateY(ty: CSSPercentageValue): Boolean
fun translateZ(tz: CSSLengthValue): Boolean
}
private class TransformBuilderImplementation : TransformBuilder {
private val transformations = mutableListOf<TransformFunction>()
override fun matrix(a: Number, b: Number, c: Number, d: Number, tx: Number, ty: Number) =
transformations.add { "matrix($a, $b, $c, $d, $tx, $ty)" }
override fun matrix3d(
a1: Number, b1: Number, c1: Number, d1: Number,
a2: Number, b2: Number, c2: Number, d2: Number,
a3: Number, b3: Number, c3: Number, d3: Number,
a4: Number, b4: Number, c4: Number, d4: Number
) =
transformations.add { "matrix3d($a1, $b1, $c1, $d1, $a2, $b2, $c2, $d2, $a3, $b3, $c3, $d3, $a4, $b4, $c4, $d4)" }
override fun perspective(d: CSSLengthValue) = transformations.add { "perspective($d)" }
override fun rotate(a: CSSAngleValue) = transformations.add { "rotate($a)" }
override fun rotate3d(x: Number, y: Number, z: Number, a: CSSAngleValue) =
transformations.add({ "rotate3d($x, $y, $z, $a)" })
override fun rotateX(a: CSSAngleValue) = transformations.add { "rotateX($a)" }
override fun rotateY(a: CSSAngleValue) = transformations.add { "rotateY($a)" }
override fun rotateZ(a: CSSAngleValue) = transformations.add { "rotateZ($a)" }
override fun scale(sx: Number) = transformations.add { "scale($sx)" }
override fun scale(sx: Number, sy: Number) = transformations.add { "scale($sx, $sy)" }
override fun scale3d(sx: Number, sy: Number, sz: Number) =
transformations.add { "scale3d($sx, $sy, $sz)" }
override fun scaleX(s: Number) = transformations.add { "scaleX($s)" }
override fun scaleY(s: Number) = transformations.add { "scaleY($s)" }
override fun scaleZ(s: Number) = transformations.add { "scaleZ($s)" }
override fun skew(ax: CSSAngleValue) = transformations.add { "skew($ax)" }
override fun skew(ax: CSSAngleValue, ay: CSSAngleValue) = transformations.add { "skew($ax, $ay)" }
override fun skewX(a: CSSAngleValue) = transformations.add { "skewX($a)" }
override fun skewY(a: CSSAngleValue) = transformations.add { "skewY($a)" }
override fun translate(tx: CSSLengthValue) = transformations.add { "translate($tx)" }
override fun translate(tx: CSSPercentageValue) = transformations.add { "translate($tx)" }
override fun translate(tx: CSSLengthValue, ty: CSSLengthValue) =
transformations.add { "translate($tx, $ty)" }
override fun translate(tx: CSSLengthValue, ty: CSSPercentageValue) =
transformations.add { "translate($tx, $ty)" }
override fun translate(tx: CSSPercentageValue, ty: CSSLengthValue) =
transformations.add { "translate($tx, $ty)" }
override fun translate(tx: CSSPercentageValue, ty: CSSPercentageValue) =
transformations.add { "translate($tx, $ty)" }
override fun translate3d(tx: CSSLengthValue, ty: CSSLengthValue, tz: CSSLengthValue) =
transformations.add { "translate3d($tx, $ty, $tz)" }
override fun translate3d(tx: CSSLengthValue, ty: CSSPercentageValue, tz: CSSLengthValue) =
transformations.add { "translate3d($tx, $ty, $tz)" }
override fun translate3d(tx: CSSPercentageValue, ty: CSSLengthValue, tz: CSSLengthValue) =
transformations.add { "translate3d($tx, $ty, $tz)" }
override fun translate3d(tx: CSSPercentageValue, ty: CSSPercentageValue, tz: CSSLengthValue) =
transformations.add { "translate3d($tx, $ty, $tz)" }
override fun translateX(tx: CSSLengthValue) = transformations.add { "translateX($tx)" }
override fun translateX(tx: CSSPercentageValue) = transformations.add { "translateX($tx)" }
override fun translateY(ty: CSSLengthValue) = transformations.add { "translateY($ty)" }
override fun translateY(ty: CSSPercentageValue) = transformations.add { "translateY($ty)" }
override fun translateZ(tz: CSSLengthValue) = transformations.add { "translateZ($tz)" }
override fun toString(): String {
return transformations.joinToString(" ") { it.apply() }
}
}
@ExperimentalComposeWebApi
fun StyleBuilder.transform(transformFunction: TransformBuilder.() -> Unit) {
val transformBuilder = TransformBuilderImplementation()
property("transform", transformBuilder.apply(transformFunction).toString())
}

111
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 <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
fun TextArea(
attrs: (TextAreaAttrsBuilder.() -> Unit)? = null,
value: String
) = TagElement(
elementBuilder = TextArea,
applyAttrs = {
val taab = TextAreaAttrsBuilder()
if (attrs != null) {
taab.attrs()
}
taab.value(value)
this.copyFrom(taab)
}
value: String? = null,
attrs: (TextAreaAttrsBuilder.() -> Unit)? = null
) {
Text(value)
// if firstProvidedValueWasNotNull then TextArea behaves as controlled input
val firstProvidedValueWasNotNull = remember { value != null }
TagElement(
elementBuilder = TextArea,
applyAttrs = {
val taab = TextAreaAttrsBuilder()
if (attrs != null) {
taab.attrs()
}
if (firstProvidedValueWasNotNull) {
taab.value(value ?: "")
}
taab.onInput {
restoreControlledTextAreaState(it.target)
}
this.copyFrom(taab)
},
content = null
)
}
@Composable
@ -921,6 +950,37 @@ fun Style(
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
fun <K> Input(
type: InputType<K>,
@ -932,21 +992,22 @@ fun <K> Input(
val inputAttrsBuilder = InputAttrsBuilder(type)
inputAttrsBuilder.type(type)
inputAttrsBuilder.attrs()
inputAttrsBuilder.onInput {
restoreControlledInputState(type = type, inputElement = it.target)
}
this.copyFrom(inputAttrsBuilder)
},
content = null
content = {
if (type == InputType.Radio) {
DisposeRadioGroupEffect()
}
}
)
}
@Composable
fun <K> Input(type: InputType<K>) {
TagElement(
elementBuilder = Input,
applyAttrs = {
val inputAttrsBuilder = InputAttrsBuilder(type)
inputAttrsBuilder.type(type)
this.copyFrom(inputAttrsBuilder)
},
content = null
)
Input(type) {}
}

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

@ -13,54 +13,110 @@ private fun InputAttrsBuilder<String>.applyAttrsWithStringValue(
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
@NonRestartableComposable
fun CheckboxInput(checked: Boolean = false, attrs: InputAttrsBuilder<Boolean>.() -> Unit = {}) {
Input(
type = InputType.Checkbox,
attrs = {
if (checked) checked()
checked(checked)
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
@NonRestartableComposable
fun DateInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
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
@NonRestartableComposable
fun DateTimeLocalInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
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
@NonRestartableComposable
fun EmailInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
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
@NonRestartableComposable
fun FileInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
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
@NonRestartableComposable
fun HiddenInput(attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
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
@NonRestartableComposable
fun MonthInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
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
@NonRestartableComposable
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
@NonRestartableComposable
fun PasswordInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
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
@NonRestartableComposable
fun RadioInput(checked: Boolean = false, attrs: InputAttrsBuilder<Boolean>.() -> Unit = {}) {
Input(
type = InputType.Radio,
attrs = {
if (checked) checked()
checked(checked)
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
@NonRestartableComposable
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
@NonRestartableComposable
fun SearchInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
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
@NonRestartableComposable
fun SubmitInput(attrs: InputAttrsBuilder<Unit>.() -> Unit = {}) {
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
@NonRestartableComposable
fun TelInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
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
@NonRestartableComposable
fun TextInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
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
@NonRestartableComposable
fun TimeInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
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
@NonRestartableComposable
fun UrlInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
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
@NonRestartableComposable
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
waitForRecompositionComplete()
assertEquals(1, controlledRadioGroups.size)
assertTrue(controlledRadioGroups.keys.first() == "group1")
assertEquals(5, controlledRadioGroups["group1"]!!.size)
countOfRadio = 2
waitForRecompositionComplete()
assertEquals(1, controlledRadioGroups.size)
assertTrue(controlledRadioGroups.keys.first() == "group1")
assertEquals(2, controlledRadioGroups["group1"]!!.size)
countOfRadio = 0
waitForRecompositionComplete()
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
waitForRecompositionComplete()
assertEquals(2, controlledRadioGroups.size)
assertEquals(5, controlledRadioGroups["group1"]!!.size)
assertEquals(10, controlledRadioGroups["group2"]!!.size)
countOfRadioG2 = 2
waitForRecompositionComplete()
assertEquals(2, controlledRadioGroups.size)
assertEquals(5, controlledRadioGroups["group1"]!!.size)
assertEquals(2, controlledRadioGroups["group2"]!!.size)
countOfRadioG1 = 0
waitForRecompositionComplete()
assertEquals(1, controlledRadioGroups.size)
assertEquals(2, controlledRadioGroups["group2"]!!.size)
countOfRadioG2 = 0
waitForRecompositionComplete()
assertEquals(0, controlledRadioGroups.size)
}
}

8
web/core/src/jsTest/kotlin/DomSideEffectTests.kt

@ -62,7 +62,7 @@ class DomSideEffectTests {
i = 1
waitChanges()
waitForChanges()
assertEquals(
expected = 1,
actual = disposeCalls.size,
@ -102,7 +102,7 @@ class DomSideEffectTests {
showDiv = false
waitChanges()
waitForChanges()
assertEquals(1, onDisposeCalledTimes)
assertEquals(expected = "<div></div>", actual = root.outerHTML)
}
@ -123,7 +123,7 @@ class DomSideEffectTests {
}
DisposableRefEffect(key) {
effectsList.add("DisposableRefEffect")
onDispose { }
onDispose { }
}
}
}
@ -135,7 +135,7 @@ class DomSideEffectTests {
key = 2
recomposeScope?.invalidate()
waitForAnimationFrame()
waitForRecompositionComplete()
assertEquals(4, effectsList.size)
assertEquals("DisposableRefEffect", effectsList[0])

8
web/core/src/jsTest/kotlin/InlineStyleTests.kt

@ -38,7 +38,7 @@ class InlineStyleTests {
)
isRed = false
waitChanges()
waitForChanges()
assertEquals(
expected = "<span style=\"color: green;\">text</span>",
@ -69,7 +69,7 @@ class InlineStyleTests {
)
isRed = true
waitChanges()
waitForChanges()
assertEquals(
expected = "<span style=\"color: red;\">text</span>",
@ -100,7 +100,7 @@ class InlineStyleTests {
)
isRed = false
waitChanges()
waitForChanges()
assertEquals(
expected = "<span>text</span>",
@ -132,7 +132,7 @@ class InlineStyleTests {
repeat(4) {
isRed = !isRed
waitChanges()
waitForChanges()
val expected = if (isRed) {
"<span style=\"color: red;\">text</span>"

161
web/core/src/jsTest/kotlin/TestUtils.kt

@ -1,67 +1,151 @@
package org.jetbrains.compose.web.core.tests
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MonotonicFrameClock
import org.jetbrains.compose.web.renderComposable
import kotlinx.browser.document
import kotlinx.browser.window
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.promise
import kotlinx.dom.clear
import org.w3c.dom.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.time.DurationUnit
import kotlin.time.ExperimentalTime
import kotlin.time.toDuration
/**
* This class provides a set of utils methods to simplify compose-web tests.
* There is no need to create its instances manually.
* @see [runTest]
*/
class TestScope : CoroutineScope by MainScope() {
/**
* It's used as a parent element for the composition.
* It's added into the document's body automatically.
*/
val root = "div".asHtmlElement()
private val testScope = MainScope()
class TestScope : CoroutineScope by testScope {
private val recompositionCompleteEventsChannel = Channel<Unit>()
private val childrenIterator = root.children.asList().listIterator()
val root = "div".asHtmlElement()
init {
document.body!!.appendChild(root)
}
private fun onRecompositionComplete() {
launch {
recompositionCompleteEventsChannel.send(Unit)
}
}
/**
* Cleans up the [root] content.
* Creates a new composition with a given Composable [content].
*/
fun composition(content: @Composable () -> Unit) {
root.clear()
renderComposable(root) {
renderComposable(
root = root,
monotonicFrameClock = TestMonotonicClockImpl(
onRecomposeComplete = this::onRecompositionComplete
)
) {
content()
}
}
private val childrenIterator = root.children.asList().listIterator()
/**
* @return a reference to the next child element of the root.
* Subsequent calls will return next child reference every time.
*/
fun nextChild() = childrenIterator.next() as HTMLElement
/**
* @return a reference to current child.
* Calling this subsequently returns the same reference every time.
*/
fun currentChild() = root.children[childrenIterator.previousIndex()] as HTMLElement
suspend fun waitChanges() {
/**
* Suspends until [root] observes any change to its html.
*/
suspend fun waitForChanges() {
waitForChanges(root)
}
/**
* Suspends until element with [elementId] observes any change to its html.
*/
suspend fun waitForChanges(elementId: String) {
waitForChanges(document.getElementById(elementId) as HTMLElement)
}
/**
* Suspends until [element] observes any change to its html.
*/
suspend fun waitForChanges(element: HTMLElement) {
suspendCoroutine<Unit> { continuation ->
val observer = MutationObserver { mutations, observer ->
continuation.resume(Unit)
observer.disconnect()
}
observer.observe(element, MutationObserverOptions)
}
}
/**
* Suspends until recomposition completes.
*/
suspend fun waitForRecompositionComplete() {
recompositionCompleteEventsChannel.receive()
}
}
internal fun runTest(block: suspend TestScope.() -> Unit): dynamic {
/**
* Use this method to test compose-web components rendered using HTML.
* Declare states and make assertions in [block].
* Use [TestScope.composition] to define the code under test.
*
* For dynamic tests, use [TestScope.waitForRecompositionComplete]
* after changing state's values and before making assertions.
*
* @see [TestScope.composition]
* @see [TestScope.waitForRecompositionComplete]
* @see [TestScope.waitForChanges].
*
* Test example:
* ```
* @Test
* fun textChild() = runTest {
* var textState by mutableStateOf("inner text")
*
* composition {
* Div {
* Text(textState)
* }
* }
* assertEquals("<div>inner text</div>", root.innerHTML)
*
* textState = "new text"
* waitForRecompositionComplete()
*
* assertEquals("<div>new text</div>", root.innerHTML)
* }
* ```
*/
fun runTest(block: suspend TestScope.() -> Unit): dynamic {
val scope = TestScope()
return scope.promise { block(scope) }
}
internal fun runBlockingTest(
block: suspend CoroutineScope.() -> Unit
): dynamic = testScope.promise { this.block() }
internal fun String.asHtmlElement() = document.createElement(this) as HTMLElement
/* Currently, the recompositionRunner relies on AnimationFrame to run the recomposition and
applyChanges. Therefore we can use this method after updating the state and before making
assertions.
If tests get broken, then DefaultMonotonicFrameClock need to be checked if it still
uses window.requestAnimationFrame */
internal suspend fun waitForAnimationFrame() {
suspendCoroutine<Unit> { continuation ->
window.requestAnimationFrame {
continuation.resume(Unit)
}
}
}
fun String.asHtmlElement() = document.createElement(this) as HTMLElement
private object MutationObserverOptions : MutationObserverInit {
override var childList: Boolean? = true
@ -71,16 +155,19 @@ private object MutationObserverOptions : MutationObserverInit {
override var attributeOldValue: Boolean? = true
}
internal suspend fun waitForChanges(elementId: String) {
waitForChanges(document.getElementById(elementId) as HTMLElement)
}
@OptIn(ExperimentalTime::class)
private class TestMonotonicClockImpl(
private val onRecomposeComplete: () -> Unit
) : MonotonicFrameClock {
internal suspend fun waitForChanges(element: HTMLElement) {
suspendCoroutine<Unit> { continuation ->
val observer = MutationObserver { mutations, observer ->
continuation.resume(Unit)
observer.disconnect()
override suspend fun <R> withFrameNanos(
onFrame: (Long) -> R
): R = suspendCoroutine { continuation ->
window.requestAnimationFrame {
val duration = it.toDuration(DurationUnit.MILLISECONDS)
val result = onFrame(duration.inWholeNanoseconds)
continuation.resume(result)
onRecomposeComplete()
}
observer.observe(element, MutationObserverOptions)
}
}

130
web/core/src/jsTest/kotlin/css/FilterTests.kt

@ -0,0 +1,130 @@
/*
* 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.core.tests.css
import org.jetbrains.compose.web.ExperimentalComposeWebApi
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Img
import kotlin.test.Test
import kotlin.test.assertEquals
@ExperimentalComposeWebApi
class FilterTests {
@Test
fun blur() = runTest {
composition {
Img(src = "icon.png", attrs = { style { filter { blur(10.px) } } })
}
assertEquals("blur(10px)", nextChild().style.filter)
}
@Test
fun brightness() = runTest {
composition {
Div({ style { filter { brightness(1.75) } } })
Div({ style { filter { brightness(200.percent) } } })
}
assertEquals("brightness(1.75)", nextChild().style.filter)
assertEquals("brightness(200%)", nextChild().style.filter)
}
@Test
fun contrast() = runTest {
composition {
Div({ style { filter { contrast(2.75) } } })
Div({ style { filter { contrast(177.percent) } } })
}
assertEquals("contrast(2.75)", nextChild().style.filter)
assertEquals("contrast(177%)", nextChild().style.filter)
}
@Test
fun grayscale() = runTest {
composition {
Div({ style { filter { grayscale(0.15) } } })
Div({ style { filter { grayscale(90.percent) } } })
}
assertEquals("grayscale(0.15)", nextChild().style.filter)
assertEquals("grayscale(90%)", nextChild().style.filter)
}
@Test
fun hueRotate() = runTest {
composition {
Div({ style { filter { hueRotate(90.deg) } } })
Div({ style { filter { hueRotate(0.5.turn) } } })
}
assertEquals("hue-rotate(90deg)", nextChild().style.filter)
assertEquals("hue-rotate(0.5turn)", nextChild().style.filter)
}
@Test
fun invert() = runTest {
composition {
Div({ style { filter { invert(0.75) } } })
Div({ style { filter { invert(30.percent) } } })
}
assertEquals("invert(0.75)", nextChild().style.filter)
assertEquals("invert(30%)", nextChild().style.filter)
}
@Test
fun opacity() = runTest {
composition {
Div({ style { filter { opacity(.25) } } })
Div({ style { filter { opacity(30.percent) } } })
}
assertEquals("opacity(0.25)", nextChild().style.filter)
assertEquals("opacity(30%)", nextChild().style.filter)
}
@Test
fun saturate() = runTest {
composition {
Div({ style { filter { saturate(.25) } } })
Div({ style { filter { saturate(20.percent) } } })
}
assertEquals("saturate(0.25)", nextChild().style.filter)
assertEquals("saturate(20%)", nextChild().style.filter)
}
@Test
fun sepia() = runTest {
composition {
Div({ style { filter { sepia(.95) } } })
Div({ style { filter { sepia(80.percent) } } })
}
assertEquals("sepia(0.95)", nextChild().style.filter)
assertEquals("sepia(80%)", nextChild().style.filter)
}
@Test
fun dropShadow() = runTest {
composition {
Div({ style { filter { dropShadow(10.em, 5.px) } } })
Div({ style { filter { dropShadow(7.px, 2.px, 20.px) } } })
Div({ style { filter { dropShadow(7.px, 2.px, Color.yellow) } } })
Div({ style { filter { dropShadow(16.px, 16.px, 10.px, Color.black) } } })
}
assertEquals("drop-shadow(10em 5px)", nextChild().style.filter)
assertEquals("drop-shadow(7px 2px 20px)", nextChild().style.filter)
assertEquals("drop-shadow(yellow 7px 2px)", nextChild().style.filter)
assertEquals("drop-shadow(black 16px 16px 10px)", nextChild().style.filter)
}
}

258
web/core/src/jsTest/kotlin/css/TransformTests.kt

@ -0,0 +1,258 @@
/*
* 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.core.tests.css
import org.jetbrains.compose.web.ExperimentalComposeWebApi
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import kotlin.test.Test
import kotlin.test.assertEquals
@ExperimentalComposeWebApi
class TransformTests {
@Test
fun matrix() = runTest {
composition {
Div({ style { transform { matrix(1, 2, -1, 1, 80, 80) } } })
}
assertEquals("matrix(1, 2, -1, 1, 80, 80)", nextChild().style.transform)
}
@Test
fun matrix3d() = runTest {
composition {
Div({ style { transform { matrix3d(1, 0, 0, 0, 0, 1, 6, 0, 0, 0, 1, 0, 50, 100, 0, 1.1) } } })
}
assertEquals("matrix3d(1, 0, 0, 0, 0, 1, 6, 0, 0, 0, 1, 0, 50, 100, 0, 1.1)", nextChild().style.transform)
}
@Test
fun perspective() = runTest {
composition {
Div({ style { transform { perspective(3.cm) } } })
}
assertEquals("perspective(3cm)", nextChild().style.transform)
}
@Test
fun rotate() = runTest {
composition {
Div({ style { transform { rotate(3.deg) } } })
}
assertEquals("rotate(3deg)", nextChild().style.transform)
}
@Test
fun rotate3d() = runTest {
composition {
Div({ style { transform { rotate3d(1, 1, 0, 2.deg) } } })
}
assertEquals("rotate3d(1, 1, 0, 2deg)", nextChild().style.transform)
}
@Test
fun rotateX() = runTest {
composition {
Div({ style { transform { rotateX(60.deg) } } })
Div({ style { transform { rotateX(-0.25.turn) } } })
Div({ style { transform { rotateX(3.14.rad) } } })
}
assertEquals("rotateX(60deg)", nextChild().style.transform)
assertEquals("rotateX(-0.25turn)", nextChild().style.transform)
assertEquals("rotateX(3.14rad)", nextChild().style.transform)
}
@Test
fun rotateY() = runTest {
composition {
Div({ style { transform { rotateY(60.deg) } } })
Div({ style { transform { rotateY(-0.25.turn) } } })
Div({ style { transform { rotateY(3.14.rad) } } })
}
assertEquals("rotateY(60deg)", nextChild().style.transform)
assertEquals("rotateY(-0.25turn)", nextChild().style.transform)
assertEquals("rotateY(3.14rad)", nextChild().style.transform)
}
@Test
fun rotateZ() = runTest {
composition {
Div({ style { transform { rotateZ(60.deg) } } })
Div({ style { transform { rotateZ(-0.25.turn) } } })
Div({ style { transform { rotateZ(3.14.rad) } } })
}
assertEquals("rotateZ(60deg)", nextChild().style.transform)
assertEquals("rotateZ(-0.25turn)", nextChild().style.transform)
assertEquals("rotateZ(3.14rad)", nextChild().style.transform)
}
@Test
fun scale() = runTest {
composition {
Div({ style { transform { scale(0.6) } } })
Div({ style { transform { scale(0.2, 0.3) } } })
}
assertEquals("scale(0.6)", nextChild().style.transform)
assertEquals("scale(0.2, 0.3)", nextChild().style.transform)
}
@Test
fun scale3d() = runTest {
composition {
Div({ style { transform { scale3d(0.2, 0.3, 0.1) } } })
}
assertEquals("scale3d(0.2, 0.3, 0.1)", nextChild().style.transform)
}
@Test
fun scaleX() = runTest {
composition {
Div({ style { transform { scaleX(0.5) } } })
}
assertEquals("scaleX(0.5)", nextChild().style.transform)
}
@Test
fun scaleY() = runTest {
composition {
Div({ style { transform { scaleY(0.7) } } })
}
assertEquals("scaleY(0.7)", nextChild().style.transform)
}
@Test
fun scaleZ() = runTest {
composition {
Div({ style { transform { scaleZ(0.12) } } })
}
assertEquals("scaleZ(0.12)", nextChild().style.transform)
}
@Test
fun skew() = runTest {
composition {
Div({ style { transform { skew(2.deg) } } })
Div({ style { transform { skew(1.rad, 2.deg) } } })
}
assertEquals("skew(2deg)", nextChild().style.transform)
assertEquals("skew(1rad, 2deg)", nextChild().style.transform)
}
@Test
fun skewX() = runTest {
composition {
Div({ style { transform { skewX(2.deg) } } })
}
assertEquals("skewX(2deg)", nextChild().style.transform)
}
@Test
fun skewY() = runTest {
composition {
Div({ style { transform { skewY(2.rad) } } })
}
assertEquals("skewY(2rad)", nextChild().style.transform)
}
@Test
fun translate() = runTest {
composition {
Div({ style { transform { translate(10.px) } } })
Div({ style { transform { translate(4.percent) } } })
Div({ style { transform { translate(2.percent, 10.px) } } })
Div({ style { transform { translate(10.px, 3.percent) } } })
Div({ style { transform { translate(20.px, 10.px) } } })
Div({ style { transform { translate(5.percent, 8.percent) } } })
}
assertEquals("translate(10px)", nextChild().style.transform)
assertEquals("translate(4%)", nextChild().style.transform)
assertEquals("translate(2%, 10px)", nextChild().style.transform)
assertEquals("translate(10px, 3%)", nextChild().style.transform)
assertEquals("translate(20px, 10px)", nextChild().style.transform)
assertEquals("translate(5%, 8%)", nextChild().style.transform)
}
@Test
fun translate3d() = runTest {
composition {
Div({ style { transform { translate3d(2.percent, 10.px, 1.em) } } })
Div({ style { transform { translate3d(10.px, 3.percent, 2.em) } } })
Div({ style { transform { translate3d(20.px, 10.px, 3.em) } } })
Div({ style { transform { translate3d(5.percent, 8.percent, 4.em) } } })
}
assertEquals("translate3d(2%, 10px, 1em)", nextChild().style.transform)
assertEquals("translate3d(10px, 3%, 2em)", nextChild().style.transform)
assertEquals("translate3d(20px, 10px, 3em)", nextChild().style.transform)
assertEquals("translate3d(5%, 8%, 4em)", nextChild().style.transform)
}
@Test
fun translateX() = runTest {
composition {
Div({ style { transform { translateX(10.px) } } })
Div({ style { transform { translateX(4.percent) } } })
}
assertEquals("translateX(10px)", nextChild().style.transform)
assertEquals("translateX(4%)", nextChild().style.transform)
}
@Test
fun translateY() = runTest {
composition {
Div({ style { transform { translateY(12.px) } } })
Div({ style { transform { translateY(3.percent) } } })
}
assertEquals("translateY(12px)", nextChild().style.transform)
assertEquals("translateY(3%)", nextChild().style.transform)
}
@Test
fun translateZ() = runTest {
composition {
Div({ style { transform { translateZ(7.px) } } })
}
assertEquals("translateZ(7px)", nextChild().style.transform)
}
@Test
fun mutliples() = runTest {
composition {
Div({
style {
transform {
perspective(3.cm)
translate(10.px, 3.px)
rotateY(3.deg)
}
}
})
}
assertEquals("perspective(3cm) translate(10px, 3px) rotateY(3deg)", nextChild().style.transform)
}
}

14
web/core/src/jsTest/kotlin/elements/AttributesTests.kt

@ -170,7 +170,7 @@ class AttributesTests {
assertEquals(null, btn.getAttribute("disabled"))
disabled = true
waitChanges()
waitForChanges()
assertEquals("", btn.getAttribute("disabled"))
}
@ -211,7 +211,7 @@ class AttributesTests {
)
addClassD.value = false
waitChanges()
waitForChanges()
assertEquals(
expected = "c a b",
@ -239,7 +239,7 @@ class AttributesTests {
flag = false
waitChanges()
waitForChanges()
assertEquals("<div b=\"pp\" c=\"cc\"></div>", root.innerHTML)
}
@ -263,7 +263,7 @@ class AttributesTests {
assertEquals("<div>Text set using ref {}</div>", root.innerHTML)
flag = false
waitChanges()
waitForChanges()
assertEquals("", root.innerHTML)
}
@ -290,7 +290,7 @@ class AttributesTests {
assertEquals(false, disposed)
flag = false
waitChanges()
waitForChanges()
assertEquals("", root.innerHTML)
assertEquals(true, disposed)
@ -325,7 +325,7 @@ class AttributesTests {
assertEquals(0, refDisposeCounter)
counter++
waitChanges()
waitForChanges()
assertEquals("2<div></div>", root.innerHTML)
assertEquals(1, refInitCounter)
@ -351,7 +351,7 @@ class AttributesTests {
assertEquals("""<button style="color: red;">Button</button>""", root.innerHTML)
hasValue = true
waitForAnimationFrame()
waitForRecompositionComplete()
assertEquals("""<button style="color: red;" value="buttonValue">Button</button>""", root.innerHTML)
}

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

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

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

@ -4,9 +4,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.getValue
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.waitForAnimationFrame
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.renderComposable
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLTextAreaElement
import kotlin.test.Test
@ -405,9 +406,9 @@ class InputsGenerateCorrectHtmlTests {
@Test
fun textAreaWithAutoComplete() = runTest {
composition {
TextArea({
TextArea(attrs = {
autoComplete(AutoComplete.email)
}, value = "")
})
}
assertEquals("""<textarea autocomplete="email"></textarea>""", root.innerHTML)
}
@ -425,7 +426,7 @@ class InputsGenerateCorrectHtmlTests {
assertEquals("""<form autocomplete="on"></form>""", root.innerHTML)
autoCompleteEnabled = false
waitChanges()
waitForChanges()
assertEquals("""<form autocomplete="off"></form>""", root.innerHTML)
}
@ -451,7 +452,7 @@ class InputsGenerateCorrectHtmlTests {
assertEquals("text", (root.firstChild as HTMLInputElement).value)
state = ""
waitForAnimationFrame()
waitForRecompositionComplete()
assertEquals("", (root.firstChild as HTMLInputElement).value)
}
@ -467,7 +468,7 @@ class InputsGenerateCorrectHtmlTests {
assertEquals("", (root.firstChild as HTMLInputElement).value)
state = "text"
waitForAnimationFrame()
waitForRecompositionComplete()
assertEquals("text", (root.firstChild as HTMLInputElement).value)
}
@ -483,7 +484,7 @@ class InputsGenerateCorrectHtmlTests {
assertEquals("text", (root.firstChild as HTMLTextAreaElement).value)
state = ""
waitForAnimationFrame()
waitForRecompositionComplete()
assertEquals("", (root.firstChild as HTMLTextAreaElement).value)
}
@ -499,8 +500,22 @@ class InputsGenerateCorrectHtmlTests {
assertEquals("", (root.firstChild as HTMLTextAreaElement).value)
state = "text"
waitForAnimationFrame()
waitForRecompositionComplete()
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.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<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)
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
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<String, TestCase>()
fun launchTestCase(testCaseId: String) {
// 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")

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)
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 ?: ""

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

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.provider.MethodSource
import org.openqa.selenium.By
import org.openqa.selenium.JavascriptExecutor
import org.openqa.selenium.WebDriver
import org.openqa.selenium.WebElement
import org.openqa.selenium.chrome.ChromeDriver
import org.openqa.selenium.chrome.ChromeOptions
import org.openqa.selenium.firefox.FirefoxDriver
@ -83,4 +85,23 @@ abstract class BaseIntegrationTests() {
@JvmStatic
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