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. 99
      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. 6
      web/core/src/jsTest/kotlin/DomSideEffectTests.kt
  22. 8
      web/core/src/jsTest/kotlin/InlineStyleTests.kt
  23. 157
      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 * [imageviewer](examples/imageviewer) - Image Viewer application for Android and Desktop
* [issues](examples/issues) - GitHub issue tracker with an adaptive UI and ktor-client * [issues](examples/issues) - GitHub issue tracker with an adaptive UI and ktor-client
* [game](examples/falling-balls) - Simple game * [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 * [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 * [notepad](examples/notepad) - Notepad, using the new experimental Composable Window API
* [todoapp](examples/todoapp) - TODO items tracker with persistence and multiple screens * [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 { MaterialTheme {
DesktopTheme { 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 package org.jetbrains.compose.videoplayer
import androidx.compose.desktop.SwingPanel
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCompositionContext
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.SwingPanel
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import uk.co.caprica.vlcj.factory.discovery.NativeDiscovery 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 uk.co.caprica.vlcj.player.component.EmbeddedMediaPlayerComponent
import java.util.*
@Composable @Composable
internal actual fun VideoPlayerImpl(url: String, width: Int, height: Int) { internal actual fun VideoPlayerImpl(url: String, width: Int, height: Int) {
println("Video player for $url") println("Video player for $url")
NativeDiscovery().discover() NativeDiscovery().discover()
// Doesn't work on macOS, see https://github.com/caprica/vlcj/issues/887 for suggestions. val mediaPlayerComponent = remember {
val mediaPlayerComponent = remember { EmbeddedMediaPlayerComponent() } // see https://github.com/caprica/vlcj/issues/887#issuecomment-503288294 for why we're using CallbackMediaPlayerComponent for macOS.
if (isMacOS()) {
CallbackMediaPlayerComponent()
} else {
EmbeddedMediaPlayerComponent()
}
}
SideEffect { SideEffect {
val ok = mediaPlayerComponent.mediaPlayer().media().play(url) val ok = mediaPlayerComponent.mediaPlayer().media().play(url)
println("play gave $ok") 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. In this example Text/TextField context menus will be extended with two additional items.
## Context menu for an arbitary area ## Context menu for an arbitrary area
There is a possibility to create a context menu for an arbitary application window area. This is implemented using ContextMenuArea API that is 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. similar to ContextMenuDataProvider.
```kotlin ```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 return duration
} }
@Test // add1kItems overrides default `repeat` value (was - 5, now - 3) to avoid getting swallowed on CI @Test // add1kItems overrides default `repeat` value (was - 5, now - 2) to avoid getting swallowed on CI
fun add1kItems() = runBenchmark(name = "add1000Items", repeat = 3) { fun add1kItems() = runBenchmark(name = "add1000Items", repeat = 2) {
addNItems(1000) addNItems(1000)
} }

1
web/build.gradle.kts

@ -37,6 +37,7 @@ subprojects {
pluginManager.withPlugin("kotlin-multiplatform") { pluginManager.withPlugin("kotlin-multiplatform") {
val printTestBundleSize by tasks.registering { val printTestBundleSize by tasks.registering {
dependsOn(tasks.named("jsTest"))
doLast { doLast {
val bundlePath = buildDir.resolve( val bundlePath = buildDir.resolve(
"compileSync/test/testDevelopmentExecutable/kotlin/${rootProject.name}-${project.name}-test.js" "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.setProperty
import org.jetbrains.compose.web.dom.setVariable import org.jetbrains.compose.web.dom.setVariable
import kotlinx.dom.clear import kotlinx.dom.clear
import org.w3c.dom.Element import org.w3c.dom.*
import org.w3c.dom.HTMLElement
import org.w3c.dom.Node
import org.w3c.dom.get
internal class DomApplier( internal class DomApplier(
root: DomNodeWrapper root: DomNodeWrapper
@ -109,7 +106,9 @@ internal class DomElementWrapper(override val node: HTMLElement): DomNodeWrapper
fun updateProperties(list: List<Pair<(Element, Any) -> Unit, Any>>) { fun updateProperties(list: List<Pair<(Element, Any) -> Unit, Any>>) {
if (node.className.isNotEmpty()) node.className = "" if (node.className.isNotEmpty()) node.className = ""
list.forEach { it.first(node, it.second) } list.forEach {
it.first(node, it.second)
}
} }
fun updateStyleDeclarations(style: StyleHolder?) { fun updateStyleDeclarations(style: StyleHolder?) {

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.Composable
import androidx.compose.runtime.Composition import androidx.compose.runtime.Composition
import androidx.compose.runtime.ControlledComposition import androidx.compose.runtime.ControlledComposition
import androidx.compose.runtime.MonotonicFrameClock
import androidx.compose.runtime.DefaultMonotonicFrameClock import androidx.compose.runtime.DefaultMonotonicFrameClock
import androidx.compose.runtime.Recomposer import androidx.compose.runtime.Recomposer
import org.jetbrains.compose.web.dom.DOMScope import org.jetbrains.compose.web.dom.DOMScope
@ -23,11 +24,12 @@ import org.w3c.dom.get
*/ */
fun <TElement : Element> renderComposable( fun <TElement : Element> renderComposable(
root: TElement, root: TElement,
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
content: @Composable DOMScope<TElement>.() -> Unit content: @Composable DOMScope<TElement>.() -> Unit
): Composition { ): Composition {
GlobalSnapshotManager.ensureStarted() GlobalSnapshotManager.ensureStarted()
val context = DefaultMonotonicFrameClock + JsMicrotasksDispatcher() val context = monotonicFrameClock + JsMicrotasksDispatcher()
val recomposer = Recomposer(context) val recomposer = Recomposer(context)
val composition = ControlledComposition( val composition = ControlledComposition(
applier = DomApplier(DomNodeWrapper(root)), 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 package org.jetbrains.compose.web.attributes
import org.jetbrains.compose.web.attributes.builders.saveControlledInputState
import org.jetbrains.compose.web.events.SyntheticSubmitEvent import org.jetbrains.compose.web.events.SyntheticSubmitEvent
import org.w3c.dom.HTMLAnchorElement import org.w3c.dom.HTMLAnchorElement
import org.w3c.dom.HTMLButtonElement import org.w3c.dom.HTMLButtonElement
@ -127,9 +128,6 @@ fun AttrsBuilder<HTMLInputElement>.autoFocus() =
fun AttrsBuilder<HTMLInputElement>.capture(value: String) = fun AttrsBuilder<HTMLInputElement>.capture(value: String) =
attr("capture", value) // type: file only attr("capture", value) // type: file only
fun AttrsBuilder<HTMLInputElement>.checked() =
attr("checked", "") // radio, checkbox
fun AttrsBuilder<HTMLInputElement>.dirName(value: String) = fun AttrsBuilder<HTMLInputElement>.dirName(value: String) =
attr("dirname", value) // text, search attr("dirname", value) // text, search
@ -202,14 +200,6 @@ fun AttrsBuilder<HTMLInputElement>.src(value: String) =
fun AttrsBuilder<HTMLInputElement>.step(value: Number) = fun AttrsBuilder<HTMLInputElement>.step(value: Number) =
attr("step", value.toString()) // numeric types only attr("step", value.toString()) // numeric types only
fun AttrsBuilder<HTMLInputElement>.valueAttr(value: String) =
attr("value", value)
fun AttrsBuilder<HTMLInputElement>.value(value: String): AttrsBuilder<HTMLInputElement> {
prop(setInputValue, value)
return this
}
/* Option attributes */ /* Option attributes */
fun AttrsBuilder<HTMLOptionElement>.value(value: String) = fun AttrsBuilder<HTMLOptionElement>.value(value: String) =
@ -299,11 +289,6 @@ fun AttrsBuilder<HTMLTextAreaElement>.rows(value: Int) =
fun AttrsBuilder<HTMLTextAreaElement>.wrap(value: TextAreaWrap) = fun AttrsBuilder<HTMLTextAreaElement>.wrap(value: TextAreaWrap) =
attr("wrap", value.str) attr("wrap", value.str)
fun AttrsBuilder<HTMLTextAreaElement>.value(value: String): AttrsBuilder<HTMLTextAreaElement> {
prop(setInputValue, value)
return this
}
/* Img attributes */ /* Img attributes */
fun AttrsBuilder<HTMLImageElement>.src(value: String): AttrsBuilder<HTMLImageElement> = fun AttrsBuilder<HTMLImageElement>.src(value: String): AttrsBuilder<HTMLImageElement> =
@ -312,8 +297,19 @@ fun AttrsBuilder<HTMLImageElement>.src(value: String): AttrsBuilder<HTMLImageEle
fun AttrsBuilder<HTMLImageElement>.alt(value: String): AttrsBuilder<HTMLImageElement> = fun AttrsBuilder<HTMLImageElement>.alt(value: String): AttrsBuilder<HTMLImageElement> =
attr("alt", value) attr("alt", value)
private val setInputValue: (HTMLInputElement, String) -> Unit = { e, v ->
internal val setInputValue: (HTMLInputElement, String) -> Unit = { e, v ->
e.value = v e.value = v
saveControlledInputState(e, v)
}
internal val setTextAreaDefaultValue: (HTMLTextAreaElement, String) -> Unit = { e, v ->
e.innerText = v
}
internal val setCheckedValue: (HTMLInputElement, Boolean) -> Unit = { e, v ->
e.checked = v
saveControlledInputState(e, v)
} }
/* Img attributes */ /* Img attributes */

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

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

61
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/InputAttrsBuilder.kt

@ -12,10 +12,62 @@ import org.jetbrains.compose.web.events.SyntheticInputEvent
import org.jetbrains.compose.web.events.SyntheticSelectEvent import org.jetbrains.compose.web.events.SyntheticSelectEvent
import org.w3c.dom.HTMLInputElement import org.w3c.dom.HTMLInputElement
/**
* An extension of [AttrsBuilder].
* This class provides a set of methods specific for [Input] element:
*
* [value] - sets the current input's value.
* [defaultValue] - sets the default input's value.
*
* [checked] - sets the current checked/unchecked state of a checkbox or a radio.
* [defaultChecked] - sets the default checked state of a checkbox or a radio.
*
* [onInvalid] - adds invalid` event listener
* [onInput] - adds `input` event listener
* [onChange] - adds `change` event listener
* [onBeforeInput] - add `beforeinput` event listener
* [onSelect] - add `select` event listener
*/
class InputAttrsBuilder<ValueType>( class InputAttrsBuilder<ValueType>(
val inputType: InputType<ValueType> val inputType: InputType<ValueType>
) : AttrsBuilder<HTMLInputElement>() { ) : AttrsBuilder<HTMLInputElement>() {
fun value(value: String): InputAttrsBuilder<ValueType> {
when (inputType) {
InputType.Checkbox,
InputType.Radio,
InputType.Hidden,
InputType.Submit -> attr("value", value)
else -> prop(setInputValue, value)
}
return this
}
fun value(value: Number): InputAttrsBuilder<ValueType> {
value(value.toString())
return this
}
fun checked(checked: Boolean): InputAttrsBuilder<ValueType> {
prop(setCheckedValue, checked)
return this
}
fun defaultChecked(): InputAttrsBuilder<ValueType> {
attr("checked", "")
return this
}
fun defaultValue(value: String): InputAttrsBuilder<ValueType> {
attr("value", value)
return this
}
fun defaultValue(value: Number): InputAttrsBuilder<ValueType> {
attr("value", value.toString())
return this
}
fun onInvalid( fun onInvalid(
options: Options = Options.DEFAULT, options: Options = Options.DEFAULT,
listener: (SyntheticEvent<HTMLInputElement>) -> Unit listener: (SyntheticEvent<HTMLInputElement>) -> Unit
@ -51,3 +103,12 @@ class InputAttrsBuilder<ValueType>(
listeners.add(SelectEventListener(options, listener)) listeners.add(SelectEventListener(options, listener))
} }
} }
internal external interface JsWeakMap {
fun delete(key: Any)
fun get(key: Any): Any?
fun has(key: Any): Boolean
fun set(key: Any, value: Any): JsWeakMap
}

74
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/InternalControlledInputUtils.kt

@ -0,0 +1,74 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.web.attributes.builders
import androidx.compose.runtime.Composable
import androidx.compose.runtime.NonRestartableComposable
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.dom.ElementScope
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLTextAreaElement
private val controlledInputsValuesWeakMap: JsWeakMap = js("new WeakMap();").unsafeCast<JsWeakMap>()
internal fun restoreControlledInputState(type: InputType<*>, inputElement: HTMLInputElement) {
if (controlledInputsValuesWeakMap.has(inputElement)) {
if (type == InputType.Radio) {
controlledRadioGroups[inputElement.name]?.forEach { radio ->
radio.checked = controlledInputsValuesWeakMap.get(radio).toString().toBoolean()
}
inputElement.checked = controlledInputsValuesWeakMap.get(inputElement).toString().toBoolean()
return
}
if (type == InputType.Checkbox) {
inputElement.checked = controlledInputsValuesWeakMap.get(inputElement).toString().toBoolean()
} else {
inputElement.value = controlledInputsValuesWeakMap.get(inputElement).toString()
}
}
}
internal fun restoreControlledTextAreaState(element: HTMLTextAreaElement) {
if (controlledInputsValuesWeakMap.has(element)) {
element.value = controlledInputsValuesWeakMap.get(element).toString()
}
}
internal fun <V : Any> saveControlledInputState(element: HTMLElement, value: V) {
controlledInputsValuesWeakMap.set(element, value)
if (element is HTMLInputElement) {
updateRadioGroupIfNeeded(element)
}
}
// internal only for testing purposes. It actually should be private.
internal val controlledRadioGroups = mutableMapOf<String, MutableSet<HTMLInputElement>>()
private fun updateRadioGroupIfNeeded(element: HTMLInputElement) {
if (element.type == "radio" && element.name.isNotEmpty()) {
if (!controlledRadioGroups.containsKey(element.name)) {
controlledRadioGroups[element.name] = mutableSetOf()
}
controlledRadioGroups[element.name]!!.add(element)
}
}
@Composable
@NonRestartableComposable
internal fun ElementScope<HTMLInputElement>.DisposeRadioGroupEffect() {
DisposableRefEffect { ref ->
onDispose {
controlledRadioGroups[ref.name]?.remove(ref)
if (controlledRadioGroups[ref.name]?.isEmpty() == true) {
controlledRadioGroups.remove(ref.name)
}
}
}
}

10
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/TextAreaAttrsBuilder.kt

@ -13,6 +13,16 @@ import org.w3c.dom.HTMLTextAreaElement
class TextAreaAttrsBuilder : AttrsBuilder<HTMLTextAreaElement>() { class TextAreaAttrsBuilder : AttrsBuilder<HTMLTextAreaElement>() {
fun value(value: String): AttrsBuilder<HTMLTextAreaElement> {
prop(setInputValue, value)
return this
}
fun defaultValue(value: String): AttrsBuilder<HTMLTextAreaElement> {
prop(setTextAreaDefaultValue, value)
return this
}
fun onInput( fun onInput(
options: Options = Options.DEFAULT, options: Options = Options.DEFAULT,
listener: (SyntheticInputEvent<String, HTMLTextAreaElement>) -> Unit listener: (SyntheticInputEvent<String, HTMLTextAreaElement>) -> Unit

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

99
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt

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

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

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

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

@ -0,0 +1,108 @@
package org.jetbrains.compose.web.core.tests
import androidx.compose.runtime.*
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.attributes.builders.controlledRadioGroups
import org.jetbrains.compose.web.attributes.name
import org.jetbrains.compose.web.dom.RadioInput
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class ControlledRadioGroupsTests {
@Test
fun controlledRadioGroupGetsUpdated() = runTest {
var countOfRadio by mutableStateOf(0)
composition {
repeat(countOfRadio) {
key (it) {
RadioInput(checked = false) {
id("r$it")
name("group1")
}
}
}
}
assertEquals(0, controlledRadioGroups.size)
countOfRadio = 5
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)
}
}

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

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

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

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

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

@ -1,68 +1,152 @@
package org.jetbrains.compose.web.core.tests package org.jetbrains.compose.web.core.tests
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MonotonicFrameClock
import org.jetbrains.compose.web.renderComposable import org.jetbrains.compose.web.renderComposable
import kotlinx.browser.document import kotlinx.browser.document
import kotlinx.browser.window import kotlinx.browser.window
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.promise import kotlinx.coroutines.promise
import kotlinx.dom.clear import kotlinx.dom.clear
import org.w3c.dom.* import org.w3c.dom.*
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine 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() private val recompositionCompleteEventsChannel = Channel<Unit>()
private val childrenIterator = root.children.asList().listIterator()
class TestScope : CoroutineScope by testScope {
val root = "div".asHtmlElement()
init { init {
document.body!!.appendChild(root) 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) { fun composition(content: @Composable () -> Unit) {
root.clear() root.clear()
renderComposable(root) {
renderComposable(
root = root,
monotonicFrameClock = TestMonotonicClockImpl(
onRecomposeComplete = this::onRecompositionComplete
)
) {
content() 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 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 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) waitForChanges(root)
} }
}
internal fun runTest(block: suspend TestScope.() -> Unit): dynamic { /**
val scope = TestScope() * Suspends until element with [elementId] observes any change to its html.
return scope.promise { block(scope) } */
suspend fun waitForChanges(elementId: String) {
waitForChanges(document.getElementById(elementId) as HTMLElement)
} }
internal fun runBlockingTest( /**
block: suspend CoroutineScope.() -> Unit * Suspends until [element] observes any change to its html.
): dynamic = testScope.promise { this.block() } */
suspend fun waitForChanges(element: HTMLElement) {
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 -> suspendCoroutine<Unit> { continuation ->
window.requestAnimationFrame { val observer = MutationObserver { mutations, observer ->
continuation.resume(Unit) continuation.resume(Unit)
observer.disconnect()
}
observer.observe(element, MutationObserverOptions)
}
}
/**
* Suspends until recomposition completes.
*/
suspend fun waitForRecompositionComplete() {
recompositionCompleteEventsChannel.receive()
} }
} }
/**
* 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) }
} }
fun String.asHtmlElement() = document.createElement(this) as HTMLElement
private object MutationObserverOptions : MutationObserverInit { private object MutationObserverOptions : MutationObserverInit {
override var childList: Boolean? = true override var childList: Boolean? = true
override var attributes: Boolean? = true override var attributes: Boolean? = true
@ -71,16 +155,19 @@ private object MutationObserverOptions : MutationObserverInit {
override var attributeOldValue: Boolean? = true override var attributeOldValue: Boolean? = true
} }
internal suspend fun waitForChanges(elementId: String) { @OptIn(ExperimentalTime::class)
waitForChanges(document.getElementById(elementId) as HTMLElement) private class TestMonotonicClockImpl(
} private val onRecomposeComplete: () -> Unit
) : MonotonicFrameClock {
internal suspend fun waitForChanges(element: HTMLElement) { override suspend fun <R> withFrameNanos(
suspendCoroutine<Unit> { continuation -> onFrame: (Long) -> R
val observer = MutationObserver { mutations, observer -> ): R = suspendCoroutine { continuation ->
continuation.resume(Unit) window.requestAnimationFrame {
observer.disconnect() 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")) assertEquals(null, btn.getAttribute("disabled"))
disabled = true disabled = true
waitChanges() waitForChanges()
assertEquals("", btn.getAttribute("disabled")) assertEquals("", btn.getAttribute("disabled"))
} }
@ -211,7 +211,7 @@ class AttributesTests {
) )
addClassD.value = false addClassD.value = false
waitChanges() waitForChanges()
assertEquals( assertEquals(
expected = "c a b", expected = "c a b",
@ -239,7 +239,7 @@ class AttributesTests {
flag = false flag = false
waitChanges() waitForChanges()
assertEquals("<div b=\"pp\" c=\"cc\"></div>", root.innerHTML) assertEquals("<div b=\"pp\" c=\"cc\"></div>", root.innerHTML)
} }
@ -263,7 +263,7 @@ class AttributesTests {
assertEquals("<div>Text set using ref {}</div>", root.innerHTML) assertEquals("<div>Text set using ref {}</div>", root.innerHTML)
flag = false flag = false
waitChanges() waitForChanges()
assertEquals("", root.innerHTML) assertEquals("", root.innerHTML)
} }
@ -290,7 +290,7 @@ class AttributesTests {
assertEquals(false, disposed) assertEquals(false, disposed)
flag = false flag = false
waitChanges() waitForChanges()
assertEquals("", root.innerHTML) assertEquals("", root.innerHTML)
assertEquals(true, disposed) assertEquals(true, disposed)
@ -325,7 +325,7 @@ class AttributesTests {
assertEquals(0, refDisposeCounter) assertEquals(0, refDisposeCounter)
counter++ counter++
waitChanges() waitForChanges()
assertEquals("2<div></div>", root.innerHTML) assertEquals("2<div></div>", root.innerHTML)
assertEquals(1, refInitCounter) assertEquals(1, refInitCounter)
@ -351,7 +351,7 @@ class AttributesTests {
assertEquals("""<button style="color: red;">Button</button>""", root.innerHTML) assertEquals("""<button style="color: red;">Button</button>""", root.innerHTML)
hasValue = true hasValue = true
waitForAnimationFrame() waitForRecompositionComplete()
assertEquals("""<button style="color: red;" value="buttonValue">Button</button>""", root.innerHTML) 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 { composition {
TextArea( TextArea(
{
onInput { handled = true }
},
value = "" value = ""
) ) {
onInput { handled = true }
}
} }
val radio = root.firstChild as HTMLTextAreaElement 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.setValue
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import org.jetbrains.compose.web.attributes.* import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.core.tests.asHtmlElement
import org.jetbrains.compose.web.core.tests.runTest import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.core.tests.waitForAnimationFrame
import org.jetbrains.compose.web.dom.* import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.renderComposable
import org.w3c.dom.HTMLInputElement import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLTextAreaElement import org.w3c.dom.HTMLTextAreaElement
import kotlin.test.Test import kotlin.test.Test
@ -405,9 +406,9 @@ class InputsGenerateCorrectHtmlTests {
@Test @Test
fun textAreaWithAutoComplete() = runTest { fun textAreaWithAutoComplete() = runTest {
composition { composition {
TextArea({ TextArea(attrs = {
autoComplete(AutoComplete.email) autoComplete(AutoComplete.email)
}, value = "") })
} }
assertEquals("""<textarea autocomplete="email"></textarea>""", root.innerHTML) assertEquals("""<textarea autocomplete="email"></textarea>""", root.innerHTML)
} }
@ -425,7 +426,7 @@ class InputsGenerateCorrectHtmlTests {
assertEquals("""<form autocomplete="on"></form>""", root.innerHTML) assertEquals("""<form autocomplete="on"></form>""", root.innerHTML)
autoCompleteEnabled = false autoCompleteEnabled = false
waitChanges() waitForChanges()
assertEquals("""<form autocomplete="off"></form>""", root.innerHTML) assertEquals("""<form autocomplete="off"></form>""", root.innerHTML)
} }
@ -451,7 +452,7 @@ class InputsGenerateCorrectHtmlTests {
assertEquals("text", (root.firstChild as HTMLInputElement).value) assertEquals("text", (root.firstChild as HTMLInputElement).value)
state = "" state = ""
waitForAnimationFrame() waitForRecompositionComplete()
assertEquals("", (root.firstChild as HTMLInputElement).value) assertEquals("", (root.firstChild as HTMLInputElement).value)
} }
@ -467,7 +468,7 @@ class InputsGenerateCorrectHtmlTests {
assertEquals("", (root.firstChild as HTMLInputElement).value) assertEquals("", (root.firstChild as HTMLInputElement).value)
state = "text" state = "text"
waitForAnimationFrame() waitForRecompositionComplete()
assertEquals("text", (root.firstChild as HTMLInputElement).value) assertEquals("text", (root.firstChild as HTMLInputElement).value)
} }
@ -483,7 +484,7 @@ class InputsGenerateCorrectHtmlTests {
assertEquals("text", (root.firstChild as HTMLTextAreaElement).value) assertEquals("text", (root.firstChild as HTMLTextAreaElement).value)
state = "" state = ""
waitForAnimationFrame() waitForRecompositionComplete()
assertEquals("", (root.firstChild as HTMLTextAreaElement).value) assertEquals("", (root.firstChild as HTMLTextAreaElement).value)
} }
@ -499,8 +500,22 @@ class InputsGenerateCorrectHtmlTests {
assertEquals("", (root.firstChild as HTMLTextAreaElement).value) assertEquals("", (root.firstChild as HTMLTextAreaElement).value)
state = "text" state = "text"
waitForAnimationFrame() waitForRecompositionComplete()
assertEquals("text", (root.firstChild as HTMLTextAreaElement).value) assertEquals("text", (root.firstChild as HTMLTextAreaElement).value)
} }
@Test
fun textAreaWithDefaultValueAndWithoutIt() {
val root = "div".asHtmlElement()
renderComposable(root = root) {
TextArea()
TextArea {
defaultValue("not-empty-default-value")
}
}
assertEquals("<textarea></textarea><textarea>not-empty-default-value</textarea>", root.innerHTML)
}
} }

52
web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/Sample.kt

@ -8,27 +8,18 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import org.jetbrains.compose.web.attributes.Draggable
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.attributes.name
import org.jetbrains.compose.web.css.selectors.className import org.jetbrains.compose.web.css.selectors.className
import org.jetbrains.compose.web.css.selectors.hover import org.jetbrains.compose.web.css.selectors.hover
import org.jetbrains.compose.web.css.selectors.plus import org.jetbrains.compose.web.css.selectors.plus
import org.jetbrains.compose.web.dom.A
import org.jetbrains.compose.web.dom.Button
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Input
import org.jetbrains.compose.web.dom.Style
import org.jetbrains.compose.web.dom.Text
import org.jetbrains.compose.web.dom.TextArea
import org.jetbrains.compose.web.renderComposableInBody import org.jetbrains.compose.web.renderComposableInBody
import org.jetbrains.compose.web.sample.tests.launchTestCase import org.jetbrains.compose.web.sample.tests.launchTestCase
import kotlinx.browser.window import kotlinx.browser.window
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.compose.web.attributes.value import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.css.* import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.*
import org.w3c.dom.url.URLSearchParams import org.w3c.dom.url.URLSearchParams
class State { class State {
@ -143,6 +134,45 @@ fun main() {
println("renderComposable") println("renderComposable")
val counter = remember { mutableStateOf(0) } val counter = remember { mutableStateOf(0) }
CheckboxInput(checked = false) {
onInput {
println("Checkbox input = ${it.value}")
}
onChange {
println("Checkbox onChange = ${it.value}")
}
}
var emailState by remember { mutableStateOf("") }
var rangeState by remember { mutableStateOf<Number>(10) }
TextInput(value = emailState) {
onInput {
println("Typed value = ${it.value}")
emailState = it.value
}
}
NumberInput(value = 10) {
onBeforeInput { println(("number onBeforeInput = ${it.value}")) }
onInput { println(("number onInput = ${it.value}")) }
onChange { println(("number onChange = ${it.value}")) }
}
RangeInput(rangeState) {
onBeforeInput { println(("RangeInput onBeforeInput = ${it.value}")) }
onInput {
println(("RangeInput onInput = ${it.value}"))
rangeState = it.value ?: 0
}
}
MonthInput(value = "2021-10") {
onInput {
println("Month = ${it.value}")
}
}
CounterApp(counter) CounterApp(counter)
val inputValue = remember { mutableStateOf("") } val inputValue = remember { mutableStateOf("") }

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

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

412
web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/ControlledInputsTests.kt

@ -0,0 +1,412 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package androidx.compose.web.sample.tests
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.attributes.name
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.sample.tests.TestText
import org.jetbrains.compose.web.sample.tests.testCase
class ControlledInputsTests {
val textInputHardcodedValueShouldNotChange by testCase {
var onInputText by remember { mutableStateOf("None") }
P { TestText(onInputText) }
Div {
TextInput(value = "hardcoded", attrs = {
id("textInput")
onInput {
onInputText = it.value
}
})
}
}
val textInputMutableValueShouldGetOverridden by testCase {
var onInputText by remember { mutableStateOf("InitialValue") }
P { TestText(onInputText) }
Div {
TextInput(value = onInputText, attrs = {
id("textInput")
onInput {
onInputText = "OVERRIDDEN VALUE"
}
})
}
}
val textInputMutableValueShouldChange by testCase {
var onInputText by remember { mutableStateOf("InitialValue") }
P { TestText(onInputText) }
Div {
TextInput(value = onInputText, attrs = {
id("textInput")
onInput {
onInputText = it.value
}
})
}
}
val textAreaHardcodedValueShouldNotChange by testCase {
var onInputText by remember { mutableStateOf("None") }
P { TestText(onInputText) }
Div {
TextArea(value = "hardcoded", attrs = {
id("textArea")
onInput {
onInputText = it.value
}
})
}
}
val textAreaMutableValueShouldGetOverridden by testCase {
var onInputText by remember { mutableStateOf("InitialValue") }
P { TestText(onInputText) }
Div {
TextArea(value = onInputText, attrs = {
id("textArea")
onInput {
onInputText = "OVERRIDDEN VALUE"
}
})
}
}
val textAreaMutableValueShouldChange by testCase {
var onInputText by remember { mutableStateOf("InitialValue") }
P { TestText(onInputText) }
Div {
TextArea(value = onInputText, attrs = {
id("textArea")
onInput {
onInputText = it.value
}
})
}
}
val checkBoxHardcodedNeverChanges by testCase {
var checkClicked by remember { mutableStateOf(false) }
P { TestText(checkClicked.toString()) }
Div {
CheckboxInput(checked = false) {
id("checkbox")
onInput {
checkClicked = it.value
}
}
}
}
val checkBoxMutableValueChanges by testCase {
var checked by remember { mutableStateOf(false) }
P { TestText(checked.toString()) }
Div {
CheckboxInput(checked = checked) {
id("checkbox")
onInput {
checked = it.value
}
}
}
}
val checkBoxDefaultCheckedChangesDoesntAffectState by testCase {
var checked by remember { mutableStateOf(true) }
P { TestText(checked.toString()) }
Div {
Input(type = InputType.Checkbox) {
id("checkboxMirror")
if (checked) defaultChecked()
}
Input(type = InputType.Checkbox) {
id("checkboxMain")
checked(checked)
onInput { checked = it.value }
}
}
}
val radioHardcodedNeverChanges by testCase {
Div {
RadioInput(checked = true) {
id("radio1")
name("group1")
}
RadioInput(checked = false) {
id("radio2")
name("group1")
}
}
}
val radioMutableCheckedChanges by testCase {
var checked by remember { mutableStateOf(0) }
TestText("Checked - $checked")
Div {
RadioInput(checked = checked == 1) {
id("radio1")
name("group1")
onInput { checked = 1 }
}
RadioInput(checked = checked == 2) {
id("radio2")
name("group1")
onInput { checked = 2 }
}
}
}
val numberHardcodedNeverChanges by testCase {
var typedValue by remember { mutableStateOf("None") }
TestText(value = typedValue)
NumberInput(value = 5, min = 0, max = 100) {
id("numberInput")
onInput {
typedValue = it.value.toString()
}
}
}
val numberMutableChanges by testCase {
var value by remember { mutableStateOf(5) }
TestText(value = value.toString())
NumberInput(value = value, min = 0, max = 100) {
id("numberInput")
onInput {
value = it.value!!.toInt()
}
}
}
val rangeHardcodedNeverChanges by testCase {
var typedValue by remember { mutableStateOf("None") }
TestText(value = typedValue)
RangeInput(value = 21) {
id("rangeInput")
onInput {
typedValue = it.value.toString()
}
}
}
val rangeMutableChanges by testCase {
var value by remember { mutableStateOf(10) }
TestText(value = value.toString())
RangeInput(value = value) {
id("rangeInput")
onInput {
value = it.value!!.toInt()
}
}
}
val emailHardcodedNeverChanges by testCase {
var typedValue by remember { mutableStateOf("None") }
TestText(value = typedValue)
EmailInput(value = "a@a.abc") {
id("emailInput")
onInput {
typedValue = it.value
}
}
}
val emailMutableChanges by testCase {
var value by remember { mutableStateOf("") }
TestText(value = value)
EmailInput(value = value) {
id("emailInput")
onInput {
value = it.value
}
}
}
val passwordHardcodedNeverChanges by testCase {
var typeValue by remember { mutableStateOf("None") }
TestText(value = typeValue)
PasswordInput(value = "123456") {
id("passwordInput")
onInput {
typeValue = it.value
}
}
}
val passwordMutableChanges by testCase {
var value by remember { mutableStateOf("") }
TestText(value = value)
EmailInput(value = value) {
id("passwordInput")
onInput {
value = it.value
}
}
}
val searchHardcodedNeverChanges by testCase {
var typeValue by remember { mutableStateOf("None") }
TestText(value = typeValue)
SearchInput(value = "hardcoded") {
id("searchInput")
onInput {
typeValue = it.value
}
}
}
val searchMutableChanges by testCase {
var typeValue by remember { mutableStateOf("") }
TestText(value = typeValue)
SearchInput(value = typeValue) {
id("searchInput")
onInput {
typeValue = it.value
}
}
}
val telHardcodedNeverChanges by testCase {
var typedValue by remember { mutableStateOf("None") }
TestText(value = typedValue)
TelInput(value = "123456") {
id("telInput")
onInput {
typedValue = it.value
}
}
}
val telMutableChanges by testCase {
var value by remember { mutableStateOf("") }
TestText(value = value)
TelInput(value = value) {
id("telInput")
onInput {
value = it.value
}
}
}
val urlHardcodedNeverChanges by testCase {
var typedValue by remember { mutableStateOf("None") }
TestText(value = typedValue)
UrlInput(value = "www.site.com") {
id("urlInput")
onInput {
typedValue = it.value
}
}
}
val urlMutableChanges by testCase {
var value by remember { mutableStateOf("") }
TestText(value = value)
UrlInput(value = value) {
id("urlInput")
onInput {
value = it.value
}
}
}
val hardcodedDateInputNeverChanges by testCase {
var inputValue by remember { mutableStateOf("None") }
TestText(inputValue)
DateInput(value = "") {
id("dateInput")
onInput {
inputValue = "onInput Caught"
}
}
}
val mutableDateInputChanges by testCase {
var inputValue by remember { mutableStateOf("") }
TestText(inputValue)
DateInput(value = inputValue) {
id("dateInput")
onInput {
inputValue = it.value
}
}
}
val hardcodedTimeNeverChanges by testCase {
var typedValue by remember { mutableStateOf("None") }
TestText(typedValue)
TimeInput(value = "14:00") {
id("time")
onInput {
typedValue = "onInput Caught"
}
}
}
val mutableTimeChanges by testCase {
var value by remember { mutableStateOf("") }
TestText(value)
TimeInput(value = value) {
id("time")
onInput {
value = it.value
}
}
}
}

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

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

190
web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/UncontrolledInputsTests.kt

@ -0,0 +1,190 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package androidx.compose.web.sample.tests
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.attributes.name
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.sample.tests.TestText
import org.jetbrains.compose.web.sample.tests.testCase
class UncontrolledInputsTests {
val textInputDefaultValueRemainsTheSameButValueCanBeChanged by testCase {
var inputValue by remember { mutableStateOf("") }
Input(type = InputType.Text) {
id("textInput")
defaultValue("defaultInputValue")
attr("data-input-value", inputValue)
onInput {
inputValue = it.value
}
}
}
val textAreaDefaultValueRemainsTheSameButValueCanBeChanged by testCase {
var inputValue by remember { mutableStateOf("") }
TextArea {
id("textArea")
defaultValue("defaultTextAreaValue")
attr("data-text-area-value", inputValue)
onInput {
inputValue = it.value
}
}
}
val checkBoxDefaultCheckedRemainsTheSameButCheckedCanBeChanged by testCase {
var checkedValue by remember { mutableStateOf(true) }
Input(type = InputType.Checkbox) {
id("checkbox")
defaultChecked()
value("checkbox-value")
attr("data-checkbox", checkedValue.toString())
onInput {
checkedValue = it.value
}
}
}
val radioDefaultCheckedRemainsTheSameButCheckedCanBeChanged by testCase {
var checkedValue by remember { mutableStateOf("") }
Input(type = InputType.Radio) {
id("radio1")
defaultChecked()
value("radio-value1")
name("radiogroup")
attr("data-radio", checkedValue)
onInput {
checkedValue = "radio-value1"
}
}
Input(type = InputType.Radio) {
id("radio2")
value("radio-value2")
name("radiogroup")
attr("data-radio", checkedValue)
onInput {
checkedValue = "radio-value2"
}
}
}
val numberDefaultValueRemainsTheSameButValueCanBeChanged by testCase {
var typedValue by remember { mutableStateOf("None") }
TestText(value = "Value = $typedValue")
Input(type = InputType.Number) {
id("numberInput")
defaultValue(11)
onInput {
typedValue = it.value.toString()
}
}
}
val rangeDefaultValueRemainsTheSameButValueCanBeChanged by testCase {
var typedValue by remember { mutableStateOf("None") }
TestText(value = "Value = $typedValue")
Input(type = InputType.Range) {
id("rangeInput")
defaultValue(7)
onInput {
typedValue = it.value.toString()
}
}
}
val emailDefaultValueRemainsTheSameButValueCanBeChanged by testCase {
var typedValue by remember { mutableStateOf("None") }
TestText(value = "Value = $typedValue")
Input(type = InputType.Email) {
id("emailInput")
defaultValue("a@a.abc")
onInput {
typedValue = it.value
}
}
}
val passwordDefaultValueRemainsTheSameButValueCanBeChanged by testCase {
var typedValue by remember { mutableStateOf("None") }
TestText(value = "Value = $typedValue")
Input(type = InputType.Password) {
id("passwordInput")
defaultValue("1111")
onInput {
typedValue = it.value
}
}
}
val searchDefaultValueRemainsTheSameButValueCanBeChanged by testCase {
var typedValue by remember { mutableStateOf("None") }
TestText(value = "Value = $typedValue")
Input(type = InputType.Search) {
id("searchInput")
defaultValue("kotlin")
onInput {
typedValue = it.value
}
}
}
val telDefaultValueRemainsTheSameButValueCanBeChanged by testCase {
var typedValue by remember { mutableStateOf("None") }
TestText(value = typedValue)
Input(type = InputType.Tel) {
id("telInput")
defaultValue("123123")
onInput {
typedValue = it.value
}
}
}
val urlDefaultValueRemainsTheSameButValueCanBeChanged by testCase {
var typedValue by remember { mutableStateOf("None") }
TestText(value = typedValue)
Input(type = InputType.Url) {
id("urlInput")
defaultValue("www.site.com")
onInput {
typedValue = it.value
}
}
}
}

507
web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/ControlledInputsTests.kt

@ -0,0 +1,507 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.web.tests.integration
import org.jetbrains.compose.web.tests.integration.common.BaseIntegrationTests
import org.jetbrains.compose.web.tests.integration.common.ResolveDrivers
import org.jetbrains.compose.web.tests.integration.common.openTestPage
import org.jetbrains.compose.web.tests.integration.common.waitTextToBe
import org.junit.jupiter.api.Assumptions
import org.openqa.selenium.By
import org.openqa.selenium.Keys
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver
class ControlledInputsTests : BaseIntegrationTests() {
@ResolveDrivers
fun textInputHardcodedValueShouldNotChange(driver: WebDriver) {
driver.openTestPage("textInputHardcodedValueShouldNotChange")
driver.waitTextToBe(value = "None")
val controlledTextInput = driver.findElement(By.id("textInput"))
controlledTextInput.sendKeys("A")
driver.waitTextToBe(value = "hardcodedA")
controlledTextInput.sendKeys("B")
driver.waitTextToBe(value = "hardcodedB")
controlledTextInput.sendKeys("C")
driver.waitTextToBe(value = "hardcodedC")
check(controlledTextInput.getAttribute("value") == "hardcoded")
}
@ResolveDrivers
fun textInputMutableValueShouldGetOverridden(driver: WebDriver) {
driver.openTestPage("textInputMutableValueShouldGetOverridden")
driver.waitTextToBe(value = "InitialValue")
val controlledTextInput = driver.findElement(By.id("textInput"))
controlledTextInput.sendKeys("ABC")
driver.waitTextToBe(value = "OVERRIDDEN VALUE")
check(controlledTextInput.getAttribute("value") == "OVERRIDDEN VALUE")
}
@ResolveDrivers
fun textInputMutableValueShouldChange(driver: WebDriver) {
driver.openTestPage("textInputMutableValueShouldChange")
driver.waitTextToBe(value = "InitialValue")
val controlledTextInput = driver.findElement(By.id("textInput"))
controlledTextInput.sendKeys("A")
driver.waitTextToBe(value = "InitialValueA")
controlledTextInput.sendKeys("B")
driver.waitTextToBe(value = "InitialValueAB")
controlledTextInput.sendKeys("C")
driver.waitTextToBe(value = "InitialValueABC")
check(controlledTextInput.getAttribute("value") == "InitialValueABC")
}
@ResolveDrivers
fun textAreaHardcodedValueShouldNotChange(driver: WebDriver) {
driver.openTestPage("textAreaHardcodedValueShouldNotChange")
driver.waitTextToBe(value = "None")
val controlledTextArea = driver.findElement(By.id("textArea"))
controlledTextArea.sendKeys("A")
driver.waitTextToBe(value = "hardcodedA")
controlledTextArea.sendKeys("B")
driver.waitTextToBe(value = "hardcodedB")
controlledTextArea.sendKeys("C")
driver.waitTextToBe(value = "hardcodedC")
check(controlledTextArea.getAttribute("value") == "hardcoded")
}
@ResolveDrivers
fun textAreaMutableValueShouldGetOverridden(driver: WebDriver) {
driver.openTestPage("textAreaMutableValueShouldGetOverridden")
driver.waitTextToBe(value = "InitialValue")
val controlledTextArea = driver.findElement(By.id("textArea"))
controlledTextArea.sendKeys("ABC")
driver.waitTextToBe(value = "OVERRIDDEN VALUE")
check(controlledTextArea.getAttribute("value") == "OVERRIDDEN VALUE")
}
@ResolveDrivers
fun textAreaMutableValueShouldChange(driver: WebDriver) {
driver.openTestPage("textAreaMutableValueShouldChange")
driver.waitTextToBe(value = "InitialValue")
val controlledTextArea = driver.findElement(By.id("textArea"))
controlledTextArea.sendKeys("A")
driver.waitTextToBe(value = "InitialValueA")
controlledTextArea.sendKeys("B")
driver.waitTextToBe(value = "InitialValueAB")
controlledTextArea.sendKeys("C")
driver.waitTextToBe(value = "InitialValueABC")
check(controlledTextArea.getAttribute("value") == "InitialValueABC")
}
@ResolveDrivers
fun checkBoxHardcodedNeverChanges(driver: WebDriver) {
driver.openTestPage("checkBoxHardcodedNeverChanges")
driver.waitTextToBe(value = "false")
val checkbox = driver.findElement(By.id("checkbox"))
check(!checkbox.isSelected)
checkbox.click()
driver.waitTextToBe(value = "true") // input received but ignored
check(!checkbox.isSelected)
}
@ResolveDrivers
fun checkBoxMutableValueChanges(driver: WebDriver) {
driver.openTestPage("checkBoxMutableValueChanges")
driver.waitTextToBe(value = "false")
val checkbox = driver.findElement(By.id("checkbox"))
check(!checkbox.isSelected)
checkbox.click()
driver.waitTextToBe(value = "true")
check(checkbox.isSelected)
}
@ResolveDrivers
fun checkBoxDefaultCheckedChangesDoesntAffectState(driver: WebDriver) {
driver.openTestPage("checkBoxDefaultCheckedChangesDoesntAffectState")
driver.waitTextToBe(value = "true")
val mainCheckbox = driver.findElement(By.id("checkboxMain"))
val mirrorCheckbox = driver.findElement(By.id("checkboxMirror"))
check(mainCheckbox.isSelected)
check(mirrorCheckbox.isSelected)
mirrorCheckbox.click()
driver.waitTextToBe(value = "true")
check(!mirrorCheckbox.isSelected)
check(mainCheckbox.isSelected)
mainCheckbox.click()
driver.waitTextToBe(value = "false")
check(!mainCheckbox.isSelected)
check(!mirrorCheckbox.isSelected)
mainCheckbox.click()
driver.waitTextToBe(value = "true")
check(mainCheckbox.isSelected)
check(!mirrorCheckbox.isSelected)
}
@ResolveDrivers
fun radioHardcodedNeverChanges(driver: WebDriver) {
driver.openTestPage("radioHardcodedNeverChanges")
val radio1 = driver.findElement(By.id("radio1"))
val radio2 = driver.findElement(By.id("radio2"))
check(radio1.isSelected)
check(!radio2.isSelected)
check(radio1.getAttribute("name") == radio2.getAttribute("name"))
check(radio1.getAttribute("name") == "group1")
check(radio2.getAttribute("name") == "group1")
radio2.click()
check(radio1.isSelected)
check(!radio2.isSelected)
}
@ResolveDrivers
fun radioMutableCheckedChanges(driver: WebDriver) {
driver.openTestPage("radioMutableCheckedChanges")
driver.waitTextToBe(value = "Checked - 0")
val radio1 = driver.findElement(By.id("radio1"))
val radio2 = driver.findElement(By.id("radio2"))
check(!radio1.isSelected)
check(!radio2.isSelected)
radio2.click()
driver.waitTextToBe(value = "Checked - 2")
check(!radio1.isSelected)
check(radio2.isSelected)
radio1.click()
driver.waitTextToBe(value = "Checked - 1")
check(radio1.isSelected)
check(!radio2.isSelected)
}
@ResolveDrivers
fun numberHardcodedNeverChanges(driver: WebDriver) {
driver.openTestPage("numberHardcodedNeverChanges")
driver.waitTextToBe(value = "None")
val numberInput = driver.findElement(By.id("numberInput"))
check(numberInput.getAttribute("value") == "5")
numberInput.sendKeys("1")
driver.waitTextToBe(value = "51")
check(numberInput.getAttribute("value") == "5")
}
@ResolveDrivers
fun numberMutableChanges(driver: WebDriver) {
driver.openTestPage("numberMutableChanges")
driver.waitTextToBe(value = "5")
val numberInput = driver.findElement(By.id("numberInput"))
check(numberInput.getAttribute("value") == "5")
numberInput.sendKeys("1")
driver.waitTextToBe(value = "51")
check(numberInput.getAttribute("value") == "51")
}
@ResolveDrivers
fun rangeHardcodedNeverChanges(driver: WebDriver) {
driver.openTestPage("rangeHardcodedNeverChanges")
driver.waitTextToBe(value = "None")
val numberInput = driver.findElement(By.id("rangeInput"))
check(numberInput.getAttribute("value") == "21")
numberInput.sendKeys(Keys.ARROW_RIGHT)
driver.waitTextToBe(value = "22")
check(numberInput.getAttribute("value") == "21")
numberInput.sendKeys(Keys.ARROW_RIGHT)
driver.waitTextToBe(value = "22")
check(numberInput.getAttribute("value") == "21")
}
@ResolveDrivers
fun rangeMutableChanges(driver: WebDriver) {
driver.openTestPage("rangeMutableChanges")
driver.waitTextToBe(value = "10")
val numberInput = driver.findElement(By.id("rangeInput"))
check(numberInput.getAttribute("value") == "10")
numberInput.sendKeys(Keys.ARROW_RIGHT)
driver.waitTextToBe(value = "11")
check(numberInput.getAttribute("value") == "11")
numberInput.sendKeys(Keys.ARROW_RIGHT)
driver.waitTextToBe(value = "12")
check(numberInput.getAttribute("value") == "12")
}
@ResolveDrivers
fun emailHardcodedNeverChanges(driver: WebDriver) {
driver.openTestPage("emailHardcodedNeverChanges")
driver.waitTextToBe(value = "None")
val emailInput = driver.findElement(By.id("emailInput"))
check(emailInput.getAttribute("value") == "a@a.abc")
emailInput.sendKeys("@")
driver.waitTextToBe(value = "a@a.abc@")
check(emailInput.getAttribute("value") == "a@a.abc")
}
@ResolveDrivers
fun emailMutableChanges(driver: WebDriver) {
driver.openTestPage("emailMutableChanges")
driver.waitTextToBe(value = "")
val emailInput = driver.findElement(By.id("emailInput"))
check(emailInput.getAttribute("value") == "")
emailInput.sendKeys("a")
driver.waitTextToBe(value = "a")
check(emailInput.getAttribute("value") == "a")
}
@ResolveDrivers
fun passwordHardcodedNeverChanges(driver: WebDriver) {
driver.openTestPage("passwordHardcodedNeverChanges")
driver.waitTextToBe(value = "None")
val passwordInput = driver.findElement(By.id("passwordInput"))
check(passwordInput.getAttribute("value") == "123456")
passwordInput.sendKeys("a")
driver.waitTextToBe(value = "123456a")
check(passwordInput.getAttribute("value") == "123456")
}
@ResolveDrivers
fun passwordMutableChanges(driver: WebDriver) {
driver.openTestPage("passwordMutableChanges")
driver.waitTextToBe(value = "")
val passwordInput = driver.findElement(By.id("passwordInput"))
check(passwordInput.getAttribute("value") == "")
passwordInput.sendKeys("a")
driver.waitTextToBe(value = "a")
check(passwordInput.getAttribute("value") == "a")
}
@ResolveDrivers
fun searchHardcodedNeverChanges(driver: WebDriver) {
driver.openTestPage("searchHardcodedNeverChanges")
driver.waitTextToBe(value = "None")
val searchInput = driver.findElement(By.id("searchInput"))
check(searchInput.getAttribute("value") == "hardcoded")
searchInput.sendKeys("a")
driver.waitTextToBe(value = "hardcodeda")
check(searchInput.getAttribute("value") == "hardcoded")
}
@ResolveDrivers
fun searchMutableChanges(driver: WebDriver) {
driver.openTestPage("searchMutableChanges")
driver.waitTextToBe(value = "")
val searchInput = driver.findElement(By.id("searchInput"))
check(searchInput.getAttribute("value") == "")
searchInput.sendKeys("a")
driver.waitTextToBe(value = "a")
check(searchInput.getAttribute("value") == "a")
}
@ResolveDrivers
fun telHardcodedNeverChanges(driver: WebDriver) {
driver.openTestPage("telHardcodedNeverChanges")
driver.waitTextToBe(value = "None")
val telInput = driver.findElement(By.id("telInput"))
check(telInput.getAttribute("value") == "123456")
telInput.sendKeys("7")
driver.waitTextToBe(value = "1234567")
check(telInput.getAttribute("value") == "123456")
}
@ResolveDrivers
fun telMutableChanges(driver: WebDriver) {
driver.openTestPage("telMutableChanges")
driver.waitTextToBe(value = "")
val telInput = driver.findElement(By.id("telInput"))
check(telInput.getAttribute("value") == "")
telInput.sendKeys("1")
driver.waitTextToBe(value = "1")
check(telInput.getAttribute("value") == "1")
}
@ResolveDrivers
fun urlHardcodedNeverChanges(driver: WebDriver) {
driver.openTestPage("urlHardcodedNeverChanges")
driver.waitTextToBe(value = "None")
val urlInput = driver.findElement(By.id("urlInput"))
check(urlInput.getAttribute("value") == "www.site.com")
urlInput.sendKeys("a")
driver.waitTextToBe(value = "www.site.coma")
check(urlInput.getAttribute("value") == "www.site.com")
}
@ResolveDrivers
fun urlMutableChanges(driver: WebDriver) {
driver.openTestPage("urlMutableChanges")
driver.waitTextToBe(value = "")
val urlInput = driver.findElement(By.id("urlInput"))
check(urlInput.getAttribute("value") == "")
urlInput.sendKeys("w")
driver.waitTextToBe(value = "w")
check(urlInput.getAttribute("value") == "w")
}
@ResolveDrivers
fun hardcodedDateInputNeverChanges(driver: WebDriver) {
driver.openTestPage("hardcodedDateInputNeverChanges")
driver.waitTextToBe(value = "None")
val dateInput = driver.findElement(By.id("dateInput"))
check(dateInput.getAttribute("value") == "")
driver.sendKeysForDateInput(dateInput, 2021, 10, 22)
driver.waitTextToBe(value = "onInput Caught")
check(dateInput.getAttribute("value") == "")
}
@ResolveDrivers
fun mutableDateInputChanges(driver: WebDriver) {
// We skip chrome, since for some reason `sendKeys` doesn't work as expected when used for Controlled Input in Chrome
Assumptions.assumeTrue(
driver !is ChromeDriver,
"chrome driver doesn't work properly when using sendKeys on Controlled Input"
)
driver.openTestPage("mutableDateInputChanges")
driver.waitTextToBe(value = "")
val dateInput = driver.findElement(By.id("dateInput"))
check(dateInput.getAttribute("value") == "")
driver.sendKeysForDateInput(dateInput, 2021, 10, 22)
driver.waitTextToBe(value = "2021-10-22")
check(dateInput.getAttribute("value") == "2021-10-22")
}
@ResolveDrivers
fun hardcodedTimeNeverChanges(driver: WebDriver) {
driver.openTestPage("hardcodedTimeNeverChanges")
driver.waitTextToBe(value = "None")
val timeInput = driver.findElement(By.id("time"))
check(timeInput.getAttribute("value") == "14:00")
timeInput.sendKeys("18:31")
driver.waitTextToBe(value = "onInput Caught")
check(timeInput.getAttribute("value") == "14:00")
}
@ResolveDrivers
fun mutableTimeChanges(driver: WebDriver) {
// We skip chrome, since for some reason `sendKeys` doesn't work as expected when used for Controlled Input in Chrome
Assumptions.assumeTrue(
driver !is ChromeDriver,
"chrome driver doesn't work properly when using sendKeys on Controlled Input"
)
driver.openTestPage("mutableTimeChanges")
driver.waitTextToBe(value = "")
val timeInput = driver.findElement(By.id("time"))
check(timeInput.getAttribute("value") == "")
timeInput.sendKeys("18:31")
driver.waitTextToBe(value = "18:31")
check(timeInput.getAttribute("value") == "18:31")
}
@ResolveDrivers
fun timeInputSendKeysOnChromeFailingTest(driver: WebDriver) {
Assumptions.assumeTrue(
driver is ChromeDriver,
"this a `failing test for Chrome only` to catch when issue with sendKeys is resolved"
)
driver.openTestPage("mutableTimeChanges")
driver.waitTextToBe(value = "")
val timeInput = driver.findElement(By.id("time"))
timeInput.sendKeys("18:31")
driver.waitTextToBe(value = "18:03") // it should be 18:31, but this is a failing test, so wrong value is expected
}
}

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

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

208
web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/UncontrolledInputsTests.kt

@ -0,0 +1,208 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.web.tests.integration
import org.jetbrains.compose.web.tests.integration.common.BaseIntegrationTests
import org.jetbrains.compose.web.tests.integration.common.ResolveDrivers
import org.jetbrains.compose.web.tests.integration.common.openTestPage
import org.jetbrains.compose.web.tests.integration.common.waitTextToBe
import org.openqa.selenium.By
import org.openqa.selenium.Keys
import org.openqa.selenium.WebDriver
class UncontrolledInputsTests : BaseIntegrationTests() {
@ResolveDrivers
fun textInputDefaultValueRemainsTheSameButValueCanBeChanged(driver: WebDriver) {
driver.openTestPage("textInputDefaultValueRemainsTheSameButValueCanBeChanged")
val input = driver.findElement(By.id("textInput"))
check(input.getAttribute("value") == "defaultInputValue")
input.sendKeys("-TypedText")
val inputHtml = driver.outerHtmlOfElementWithId("textInput")
check(inputHtml.contains("value=\"defaultInputValue\""))
check(input.getAttribute("value") == "defaultInputValue-TypedText") // this checks the `value` property of the input
check(input.getAttribute("data-input-value") == "defaultInputValue-TypedText")
}
@ResolveDrivers
fun textAreaDefaultValueRemainsTheSameButValueCanBeChanged(driver: WebDriver) {
driver.openTestPage("textAreaDefaultValueRemainsTheSameButValueCanBeChanged")
val textArea = driver.findElement(By.id("textArea"))
check(textArea.getAttribute("value") == "defaultTextAreaValue")
textArea.sendKeys("-TypedText")
val innerTextOfTextArea = driver.outerHtmlOfElementWithId("textArea")
check(innerTextOfTextArea.contains(">defaultTextAreaValue</")) // inner text keeps default value
check(textArea.getAttribute("value") == "defaultTextAreaValue-TypedText") // this checks the `value` property of the textarea
check(textArea.getAttribute("data-text-area-value") == "defaultTextAreaValue-TypedText")
}
@ResolveDrivers
fun checkBoxDefaultCheckedRemainsTheSameButCheckedCanBeChanged(driver: WebDriver) {
driver.openTestPage("checkBoxDefaultCheckedRemainsTheSameButCheckedCanBeChanged")
val checkbox = driver.findElement(By.id("checkbox"))
val innerTextOfCheckbox1 = driver.outerHtmlOfElementWithId("checkbox")
check(innerTextOfCheckbox1.contains("checked"))
check(checkbox.getAttribute("value") == "checkbox-value")
check(checkbox.getAttribute("data-checkbox") == "true")
check(checkbox.isSelected)
checkbox.click()
val innerTextOfCheckbox2 = driver.outerHtmlOfElementWithId("checkbox")
check(innerTextOfCheckbox2.contains("checked"))
check(checkbox.getAttribute("value") == "checkbox-value")
check(checkbox.getAttribute("data-checkbox") == "false")
check(!checkbox.isSelected)
}
@ResolveDrivers
fun radioDefaultCheckedRemainsTheSameButCheckedCanBeChanged(driver: WebDriver) {
driver.openTestPage("radioDefaultCheckedRemainsTheSameButCheckedCanBeChanged")
val radio1 = driver.findElement(By.id("radio1"))
val radio2 = driver.findElement(By.id("radio2"))
check(radio1.isSelected)
check(!radio2.isSelected)
check(driver.outerHtmlOfElementWithId("radio1").contains("checked"))
check(!driver.outerHtmlOfElementWithId("radio2").contains("checked"))
radio2.click()
check(!radio1.isSelected)
check(radio2.isSelected)
check(driver.outerHtmlOfElementWithId("radio1").contains("checked"))
check(!driver.outerHtmlOfElementWithId("radio2").contains("checked"))
}
@ResolveDrivers
fun numberDefaultValueRemainsTheSameButValueCanBeChanged(driver: WebDriver) {
driver.openTestPage("numberDefaultValueRemainsTheSameButValueCanBeChanged")
driver.waitTextToBe(value = "Value = None")
val numberInput = driver.findElement(By.id("numberInput"))
check(numberInput.getAttribute("value") == "11")
numberInput.sendKeys("5")
driver.waitTextToBe(value = "Value = 511")
check(numberInput.getAttribute("value") == "511")
check(driver.outerHtmlOfElementWithId("numberInput").contains("value=\"11\""))
}
@ResolveDrivers
fun rangeDefaultValueRemainsTheSameButValueCanBeChanged(driver: WebDriver) {
driver.openTestPage("rangeDefaultValueRemainsTheSameButValueCanBeChanged")
driver.waitTextToBe(value = "Value = None")
val numberInput = driver.findElement(By.id("rangeInput"))
check(numberInput.getAttribute("value") == "7")
numberInput.sendKeys(Keys.ARROW_RIGHT)
driver.waitTextToBe(value = "Value = 8")
numberInput.sendKeys(Keys.ARROW_RIGHT)
driver.waitTextToBe(value = "Value = 9")
check(numberInput.getAttribute("value") == "9")
check(driver.outerHtmlOfElementWithId("rangeInput").contains("value=\"7\""))
}
@ResolveDrivers
fun emailDefaultValueRemainsTheSameButValueCanBeChanged(driver: WebDriver) {
driver.openTestPage("emailDefaultValueRemainsTheSameButValueCanBeChanged")
driver.waitTextToBe(value = "Value = None")
val emailInput = driver.findElement(By.id("emailInput"))
check(emailInput.getAttribute("value") == "a@a.abc")
emailInput.clear()
emailInput.sendKeys("u@u.com")
driver.waitTextToBe(value = "Value = u@u.com")
check(emailInput.getAttribute("value") == "u@u.com")
check(driver.outerHtmlOfElementWithId("emailInput").contains("value=\"a@a.abc\""))
}
@ResolveDrivers
fun passwordDefaultValueRemainsTheSameButValueCanBeChanged(driver: WebDriver) {
driver.openTestPage("passwordDefaultValueRemainsTheSameButValueCanBeChanged")
driver.waitTextToBe(value = "Value = None")
val passwordInput = driver.findElement(By.id("passwordInput"))
check(passwordInput.getAttribute("value") == "1111")
passwordInput.clear()
passwordInput.sendKeys("a")
driver.waitTextToBe(value = "Value = a")
check(passwordInput.getAttribute("value") == "a")
check(driver.outerHtmlOfElementWithId("passwordInput").contains("value=\"1111\""))
}
@ResolveDrivers
fun searchDefaultValueRemainsTheSameButValueCanBeChanged(driver: WebDriver) {
driver.openTestPage("searchDefaultValueRemainsTheSameButValueCanBeChanged")
driver.waitTextToBe(value = "Value = None")
val searchInput = driver.findElement(By.id("searchInput"))
check(searchInput.getAttribute("value") == "kotlin")
searchInput.clear()
searchInput.sendKeys("j")
driver.waitTextToBe(value = "Value = j")
check(searchInput.getAttribute("value") == "j")
check(driver.outerHtmlOfElementWithId("searchInput").contains("value=\"kotlin\""))
}
@ResolveDrivers
fun telDefaultValueRemainsTheSameButValueCanBeChanged(driver: WebDriver) {
driver.openTestPage("telDefaultValueRemainsTheSameButValueCanBeChanged")
driver.waitTextToBe(value = "None")
val telInput = driver.findElement(By.id("telInput"))
check(telInput.getAttribute("value") == "123123")
telInput.clear()
telInput.sendKeys("987654321")
driver.waitTextToBe(value = "987654321")
check(telInput.getAttribute("value") == "987654321")
check(driver.outerHtmlOfElementWithId("telInput").contains("value=\"123123\""))
}
@ResolveDrivers
fun urlDefaultValueRemainsTheSameButValueCanBeChanged(driver: WebDriver) {
driver.openTestPage("urlDefaultValueRemainsTheSameButValueCanBeChanged")
driver.waitTextToBe(value = "None")
val urlInput = driver.findElement(By.id("urlInput"))
check(urlInput.getAttribute("value") == "www.site.com")
urlInput.clear()
urlInput.sendKeys("google.com")
driver.waitTextToBe(value = "google.com")
check(urlInput.getAttribute("value") == "google.com")
check(driver.outerHtmlOfElementWithId("urlInput").contains("value=\"www.site.com\""))
}
}

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

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

Loading…
Cancel
Save