Browse Source

web: add test-utils module (#1101)

* web: move DomApplier, GlobalSnapshotManager, JsMicrotasksDispatcher to dedicated module

This will allow reusing them in test-utils module.

* web: move TestUtils.kt to a dedicated module

Use test-utils module in web-core as a dependency

Co-authored-by: Oleksandr Karpovich <oleksandr.karpovich@jetbrains.com>
sync/2021-08-26
Oleksandr Karpovich 3 years ago committed by GitHub
parent
commit
e8e02771a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      web/core/build.gradle.kts
  2. 2
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/RenderComposable.kt
  3. 7
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/SyntheticEventListener.kt
  4. 18
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StyleBuilder.kt
  5. 6
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Base.kt
  6. 6
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt
  7. 3
      web/core/src/jsTest/kotlin/CSSStylesheetTests.kt
  8. 3
      web/core/src/jsTest/kotlin/CSSUnitApiTests.kt
  9. 1
      web/core/src/jsTest/kotlin/ControlledRadioGroupsTests.kt
  10. 1
      web/core/src/jsTest/kotlin/DomSideEffectTests.kt
  11. 2
      web/core/src/jsTest/kotlin/FailingTestCases.kt
  12. 1
      web/core/src/jsTest/kotlin/InlineStyleTests.kt
  13. 1
      web/core/src/jsTest/kotlin/StaticComposableTests.kt
  14. 6
      web/core/src/jsTest/kotlin/css/CSSBackgroundTests.kt
  15. 4
      web/core/src/jsTest/kotlin/css/CSSBorderTests.kt
  16. 6
      web/core/src/jsTest/kotlin/css/CSSBoxTests.kt
  17. 6
      web/core/src/jsTest/kotlin/css/CSSDisplayTests.kt
  18. 4
      web/core/src/jsTest/kotlin/css/CSSFlexTests.kt
  19. 4
      web/core/src/jsTest/kotlin/css/CSSListStyleTests.kt
  20. 6
      web/core/src/jsTest/kotlin/css/CSSMarginTests.kt
  21. 6
      web/core/src/jsTest/kotlin/css/CSSOverflowTests.kt
  22. 6
      web/core/src/jsTest/kotlin/css/CSSPaddingTests.kt
  23. 7
      web/core/src/jsTest/kotlin/css/CSSTextTests.kt
  24. 6
      web/core/src/jsTest/kotlin/css/CSSUiTests.kt
  25. 2
      web/core/src/jsTest/kotlin/css/ColorTests.kt
  26. 4
      web/core/src/jsTest/kotlin/css/FilterTests.kt
  27. 6
      web/core/src/jsTest/kotlin/css/GridTests.kt
  28. 4
      web/core/src/jsTest/kotlin/css/TransformTests.kt
  29. 19
      web/core/src/jsTest/kotlin/elements/AttributesTests.kt
  30. 2
      web/core/src/jsTest/kotlin/elements/ElementsTests.kt
  31. 1
      web/core/src/jsTest/kotlin/elements/EventTests.kt
  32. 3
      web/core/src/jsTest/kotlin/elements/InputsGenerateCorrectHtmlTests.kt
  33. 1
      web/core/src/jsTest/kotlin/elements/TableTests.kt
  34. 33
      web/internal-web-core-runtime/build.gradle.kts
  35. 4
      web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/ComposeWebInternalApi.kt
  36. 53
      web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/DomApplier.kt
  37. 7
      web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/GlobalSnapshotManager.kt
  38. 5
      web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/JsMicrotasksDispatcher.kt
  39. 4
      web/settings.gradle.kts
  40. 42
      web/test-utils/build.gradle.kts
  41. 4
      web/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/ComposeWebExperimentalTestsApi.kt
  42. 70
      web/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/TestUtils.kt
  43. 82
      web/test-utils/src/jsTest/kotlin/TestsForTestUtils.kt

8
web/core/build.gradle.kts

@ -30,12 +30,14 @@ kotlin {
val jsMain by getting {
dependencies {
implementation(project(":internal-web-core-runtime"))
implementation(kotlin("stdlib-js"))
}
}
val jsTest by getting {
dependencies {
implementation(project(":test-utils"))
implementation(kotlin("test-js"))
}
}
@ -45,5 +47,11 @@ kotlin {
implementation(compose.desktop.currentOs)
}
}
all {
languageSettings {
useExperimentalAnnotation("org.jetbrains.compose.web.testutils.ComposeWebExperimentalTestsApi")
}
}
}
}

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

@ -11,6 +11,7 @@ import kotlinx.browser.document
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.launch
import org.jetbrains.compose.web.internal.runtime.*
import org.w3c.dom.Element
import org.w3c.dom.HTMLBodyElement
import org.w3c.dom.get
@ -22,6 +23,7 @@ import org.w3c.dom.get
*
* @return the instance of the [Composition]
*/
@OptIn(ComposeWebInternalApi::class)
fun <TElement : Element> renderComposable(
root: TElement,
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,

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

@ -9,16 +9,21 @@ import org.jetbrains.compose.web.attributes.EventsListenerBuilder.Companion.CHAN
import org.jetbrains.compose.web.attributes.EventsListenerBuilder.Companion.INPUT
import org.jetbrains.compose.web.attributes.EventsListenerBuilder.Companion.SELECT
import org.jetbrains.compose.web.events.*
import org.jetbrains.compose.web.internal.runtime.DomNodeWrapper
import org.jetbrains.compose.web.internal.runtime.ComposeWebInternalApi
import org.w3c.dom.DragEvent
import org.w3c.dom.TouchEvent
import org.w3c.dom.clipboard.ClipboardEvent
import org.w3c.dom.events.*
@OptIn(ComposeWebInternalApi::class)
open class SyntheticEventListener<T : SyntheticEvent<*>> internal constructor(
val event: String,
val options: Options,
val listener: (T) -> Unit
) : EventListener {
) : EventListener, DomNodeWrapper.NamedEventListener {
override val name: String = event
@Suppress("UNCHECKED_CAST")
override fun handleEvent(event: Event) {

18
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StyleBuilder.kt

@ -7,6 +7,9 @@
package org.jetbrains.compose.web.css
import org.jetbrains.compose.web.internal.runtime.DomElementWrapper
import org.jetbrains.compose.web.internal.runtime.ComposeWebInternalApi
import org.w3c.dom.css.CSSStyleDeclaration
import kotlin.properties.ReadOnlyProperty
/**
@ -116,7 +119,7 @@ fun <TValue> CSSStyleVariable<TValue>.value(fallback: TValue? = null)
* property("width", AppCSSVariables.width.value())
* }
*```
*
*
*/
fun <TValue : StylePropertyValue> variable() =
ReadOnlyProperty<Any?, CSSStyleVariable<TValue>> { _, property ->
@ -128,8 +131,9 @@ interface StyleHolder {
val variables: StylePropertyList
}
@OptIn(ComposeWebInternalApi::class)
@Suppress("EqualsOrHashCode")
open class StyleBuilderImpl : StyleBuilder, StyleHolder {
open class StyleBuilderImpl : StyleBuilder, StyleHolder, DomElementWrapper.StyleDeclarationsApplier {
override val properties: MutableStylePropertyList = mutableListOf()
override val variables: MutableStylePropertyList = mutableListOf()
@ -153,6 +157,16 @@ open class StyleBuilderImpl : StyleBuilder, StyleHolder {
properties.addAll(sb.properties)
variables.addAll(sb.variables)
}
override fun applyToNodeStyle(nodeStyle: CSSStyleDeclaration) {
properties.forEach { (name, value) ->
nodeStyle.setProperty(name, value.toString())
}
variables.forEach { (name, value) ->
nodeStyle.setProperty(name, value.toString())
}
}
}
data class StylePropertyDeclaration(

6
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Base.kt

@ -10,10 +10,11 @@ import androidx.compose.runtime.ExplicitGroupsComposable
import androidx.compose.runtime.SkippableUpdater
import androidx.compose.runtime.currentComposer
import androidx.compose.runtime.remember
import org.jetbrains.compose.web.DomApplier
import org.jetbrains.compose.web.DomElementWrapper
import org.jetbrains.compose.web.attributes.AttrsBuilder
import org.jetbrains.compose.web.ExperimentalComposeWebApi
import org.jetbrains.compose.web.internal.runtime.DomApplier
import org.jetbrains.compose.web.internal.runtime.DomElementWrapper
import org.jetbrains.compose.web.internal.runtime.ComposeWebInternalApi
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement
@ -48,6 +49,7 @@ class DisposableEffectHolder<TElement : Element>(
var effect: (DisposableEffectScope.(TElement) -> DisposableEffectResult)? = null
)
@OptIn(ComposeWebInternalApi::class)
@Composable
fun <TElement : Element> TagElement(
elementBuilder: ElementBuilder<TElement>,

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

@ -4,14 +4,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.ComposeNode
import androidx.compose.runtime.remember
import androidx.compose.web.attributes.SelectAttrsBuilder
import org.jetbrains.compose.web.DomApplier
import org.jetbrains.compose.web.DomNodeWrapper
import kotlinx.browser.document
import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.attributes.builders.*
import org.jetbrains.compose.web.css.CSSRuleDeclarationList
import org.jetbrains.compose.web.css.StyleSheetBuilder
import org.jetbrains.compose.web.css.StyleSheetBuilderImpl
import org.jetbrains.compose.web.internal.runtime.DomApplier
import org.jetbrains.compose.web.internal.runtime.DomNodeWrapper
import org.jetbrains.compose.web.internal.runtime.ComposeWebInternalApi
import org.w3c.dom.Element
import org.w3c.dom.HTMLAnchorElement
import org.w3c.dom.HTMLAreaElement
@ -414,6 +415,7 @@ fun Source(
)
}
@OptIn(ComposeWebInternalApi::class)
@Composable
fun Text(value: String) {
ComposeNode<DomNodeWrapper, DomApplier>(

3
web/core/src/jsTest/kotlin/CSSStylesheetTests.kt

@ -12,6 +12,7 @@ import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
object AppCSSVariables {
val width by variable<CSSUnitValue>()
@ -143,4 +144,4 @@ class CSSVariableTests {
assertEquals("rgb(0, 128, 0)", window.getComputedStyle(el).backgroundColor)
}
}
}
}

3
web/core/src/jsTest/kotlin/CSSUnitApiTests.kt

@ -11,6 +11,7 @@ import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
class CSSUnitApiTests {
// TODO: Cover CSS.Q, CSS.khz and CSS.hz after we'll get rid from polyfill
@ -525,4 +526,4 @@ class CSSUnitApiTests {
assertEquals("5px", (root.children[0] as HTMLElement).style.left)
assertEquals("8px", (root.children[0] as HTMLElement).style.top)
}
}
}

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

@ -8,6 +8,7 @@ import org.jetbrains.compose.web.dom.RadioInput
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import org.jetbrains.compose.web.testutils.*
class ControlledRadioGroupsTests {

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

@ -5,6 +5,7 @@ import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.renderComposable
import kotlinx.browser.document
import kotlinx.dom.clear
import org.jetbrains.compose.web.testutils.*
import kotlin.test.Test
import kotlin.test.assertEquals

2
web/core/src/jsTest/kotlin/FailingTestCases.kt

@ -5,9 +5,7 @@
package org.jetbrains.compose.web.core.tests
import androidx.compose.runtime.Composable
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class FailingTestCases {

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

@ -9,6 +9,7 @@ import org.jetbrains.compose.web.dom.Span
import org.jetbrains.compose.web.dom.Text
import kotlin.test.Test
import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
class InlineStyleTests {

1
web/core/src/jsTest/kotlin/StaticComposableTests.kt

@ -10,6 +10,7 @@ import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import org.jetbrains.compose.web.testutils.*
class StaticComposableTests {
@Test

6
web/core/src/jsTest/kotlin/css/CSSBackgroundTests.kt

@ -6,13 +6,11 @@
package org.jetbrains.compose.web.core.tests.css
import kotlinx.browser.window
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
class CSSBackgroundTests {
@Test
@ -187,4 +185,4 @@ class CSSBackgroundTests {
assertEquals("no-repeat", window.getComputedStyle(nextChild()).backgroundRepeat)
}
}
}

4
web/core/src/jsTest/kotlin/css/CSSBorderTests.kt

@ -5,7 +5,7 @@
package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import kotlin.test.Test
@ -66,4 +66,4 @@ class CSSBorderTests {
assertEquals("3px 5px 4px", (nextChild()).style.borderWidth)
assertEquals("3px 5px 4px 2px", (nextChild()).style.borderWidth)
}
}
}

6
web/core/src/jsTest/kotlin/css/CSSBoxTests.kt

@ -5,11 +5,9 @@
package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.core.tests.asHtmlElement
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.renderComposable
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test
@ -266,4 +264,4 @@ class CSSBoxTests {
assertEquals("max-content", (nextChild()).style.maxHeight)
assertEquals("min-content", (nextChild()).style.maxHeight)
}
}
}

6
web/core/src/jsTest/kotlin/css/CSSDisplayTests.kt

@ -5,14 +5,12 @@
package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.core.tests.values
import org.jetbrains.compose.web.css.DisplayStyle
import org.jetbrains.compose.web.css.display
import org.jetbrains.compose.web.css.value
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertEquals
@ -42,4 +40,4 @@ class CSSDisplayTests {
}
}
}
}

4
web/core/src/jsTest/kotlin/css/CSSFlexTests.kt

@ -5,7 +5,7 @@
package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.core.tests.values
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
@ -378,4 +378,4 @@ class CSSFlexTests {
}
}
}
}

4
web/core/src/jsTest/kotlin/css/CSSListStyleTests.kt

@ -5,7 +5,7 @@
package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
@ -105,4 +105,4 @@ class CSSListStyleTests {
assertEquals("georgian", (currentChild()).style.listStyleType)
}
}
}

6
web/core/src/jsTest/kotlin/css/CSSMarginTests.kt

@ -6,11 +6,9 @@
package org.jetbrains.compose.web.core.tests.css
import kotlinx.browser.window
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertEquals
@ -139,4 +137,4 @@ class CSSMarginTests {
assertEquals("1px", el.marginLeft, "marginLeft")
}
}
}

6
web/core/src/jsTest/kotlin/css/CSSOverflowTests.kt

@ -5,11 +5,9 @@
package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertEquals
@ -104,4 +102,4 @@ class CSSOverflowTests {
}
}
}

6
web/core/src/jsTest/kotlin/css/CSSPaddingTests.kt

@ -6,11 +6,9 @@
package org.jetbrains.compose.web.core.tests.css
import kotlinx.browser.window
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertEquals
@ -139,4 +137,4 @@ class CSSPaddingTests {
assertEquals("1px", el.paddingLeft, "paddingLeft")
}
}
}

7
web/core/src/jsTest/kotlin/css/CSSTextTests.kt

@ -5,12 +5,9 @@
package org.jetbrains.compose.web.core.tests.css
import kotlinx.browser.window
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertEquals
@ -397,4 +394,4 @@ class CSSTextTests {
assertEquals("break-spaces", (nextChild()).style.whiteSpace)
}
}
}

6
web/core/src/jsTest/kotlin/css/CSSUiTests.kt

@ -5,11 +5,9 @@
package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.cursor
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertEquals
@ -44,4 +42,4 @@ class CSSUiTests {
assertEquals("url(\"hand.cur\"), pointer", (nextChild()).style.cursor)
assertEquals("url(\"cursor2.png\") 2 2, pointer", (nextChild()).style.cursor)
}
}
}

2
web/core/src/jsTest/kotlin/css/ColorTests.kt

@ -5,7 +5,7 @@
package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement

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

@ -6,7 +6,7 @@
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.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Img
@ -127,4 +127,4 @@ class FilterTests {
assertEquals("drop-shadow(black 16px 16px 10px)", nextChild().style.filter)
}
}
}

6
web/core/src/jsTest/kotlin/css/GridTests.kt

@ -5,11 +5,9 @@
package org.jetbrains.compose.web.core.tests.css
import androidx.compose.runtime.compositionLocalOf
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import kotlin.js.Json
import kotlin.test.Test
import kotlin.test.assertEquals
@ -648,4 +646,4 @@ class GridAutoFlowTests {
assertEquals("dense", nextChild().style.asDynamic().gridAutoFlow)
assertEquals("row", nextChild().style.asDynamic().gridAutoFlow)
}
}
}

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

@ -5,7 +5,7 @@
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.testutils.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import kotlin.test.Test
@ -255,4 +255,4 @@ class TransformTests {
assertEquals("perspective(3cm) translate(10px, 3px) rotateY(3deg)", nextChild().style.transform)
}
}
}

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

@ -17,6 +17,7 @@ import org.w3c.dom.HTMLDivElement
import org.w3c.dom.HTMLElement
import kotlin.test.Test
import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
class AttributesTests {
@ -348,11 +349,25 @@ class AttributesTests {
}
}
assertEquals("""<button style="color: red;">Button</button>""", root.innerHTML)
assertEquals(
expected = "color: red;",
actual = (root.firstChild as HTMLButtonElement).getAttribute("style")
)
assertEquals(
expected = null,
actual = (root.firstChild as HTMLButtonElement).getAttribute("value")
)
hasValue = true
waitForRecompositionComplete()
assertEquals("""<button style="color: red;" value="buttonValue">Button</button>""", root.innerHTML)
assertEquals(
expected = "color: red;",
actual = (root.firstChild as HTMLButtonElement).getAttribute("style")
)
assertEquals(
expected = "buttonValue",
actual = (root.firstChild as HTMLButtonElement).getAttribute("value")
)
}
}

2
web/core/src/jsTest/kotlin/elements/ElementsTests.kt

@ -8,7 +8,7 @@ package org.jetbrains.compose.web.core.tests.elements
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.ExperimentalComposeWebApi
import org.jetbrains.compose.web.attributes.AttrsBuilder
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.dom.*
import org.w3c.dom.HTMLElement
import org.w3c.dom.get

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

@ -13,6 +13,7 @@ import org.w3c.dom.events.MouseEvent
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import org.jetbrains.compose.web.testutils.*
class EventTests {

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

@ -4,14 +4,13 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.getValue
import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.core.tests.asHtmlElement
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.renderComposable
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLTextAreaElement
import kotlin.test.Test
import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
class InputsGenerateCorrectHtmlTests {

1
web/core/src/jsTest/kotlin/elements/TableTests.kt

@ -19,6 +19,7 @@ import org.jetbrains.compose.web.dom.Tr
import org.w3c.dom.HTMLElement
import kotlin.test.Test
import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
class TableTests {

33
web/internal-web-core-runtime/build.gradle.kts

@ -0,0 +1,33 @@
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
}
kotlin {
js(IR) {
browser() {
testTask {
testLogging.showStandardStreams = true
useKarma {
useChromeHeadless()
useFirefox()
}
}
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(compose.runtime)
implementation(kotlin("stdlib-common"))
}
}
val jsMain by getting {
dependencies {
implementation(kotlin("stdlib-js"))
}
}
}
}

4
web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/ComposeWebInternalApi.kt

@ -0,0 +1,4 @@
package org.jetbrains.compose.web.internal.runtime
@RequiresOptIn("This API is internal and is likely to change in the future.")
annotation class ComposeWebInternalApi

53
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/DomApplier.kt → web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/DomApplier.kt

@ -1,14 +1,13 @@
package org.jetbrains.compose.web
package org.jetbrains.compose.web.internal.runtime
import androidx.compose.runtime.AbstractApplier
import org.jetbrains.compose.web.attributes.SyntheticEventListener
import org.jetbrains.compose.web.css.StyleHolder
import org.jetbrains.compose.web.dom.setProperty
import org.jetbrains.compose.web.dom.setVariable
import kotlinx.dom.clear
import org.w3c.dom.*
import org.w3c.dom.css.CSSStyleDeclaration
import org.w3c.dom.events.EventListener
internal class DomApplier(
@ComposeWebInternalApi
class DomApplier(
root: DomNodeWrapper
) : AbstractApplier<DomNodeWrapper>(root) {
@ -34,26 +33,28 @@ internal class DomApplier(
}
}
external interface EventListenerOptions {
var once: Boolean
var passive: Boolean
var capture: Boolean
}
internal open class DomNodeWrapper(open val node: Node) {
private var currentListeners = emptyList<SyntheticEventListener<*>>()
@ComposeWebInternalApi
open class DomNodeWrapper(open val node: Node) {
@ComposeWebInternalApi
interface NamedEventListener : EventListener {
val name: String
}
fun updateEventListeners(list: List<SyntheticEventListener<*>>) {
private var currentListeners = emptyList<NamedEventListener>()
fun updateEventListeners(list: List<NamedEventListener>) {
val htmlElement = node as? HTMLElement ?: return
currentListeners.forEach {
htmlElement.removeEventListener(it.event, it)
htmlElement.removeEventListener(it.name, it)
}
currentListeners = list
currentListeners.forEach {
htmlElement.addEventListener(it.event, it)
htmlElement.addEventListener(it.name, it)
}
}
@ -88,8 +89,8 @@ internal open class DomNodeWrapper(open val node: Node) {
}
}
internal class DomElementWrapper(override val node: HTMLElement): DomNodeWrapper(node) {
@ComposeWebInternalApi
class DomElementWrapper(override val node: HTMLElement): DomNodeWrapper(node) {
private var currentAttrs: Map<String, String>? = null
fun updateAttrs(attrs: Map<String, String>) {
@ -111,14 +112,14 @@ internal class DomElementWrapper(override val node: HTMLElement): DomNodeWrapper
}
}
fun updateStyleDeclarations(style: StyleHolder?) {
node.removeAttribute("style")
@ComposeWebInternalApi
fun interface StyleDeclarationsApplier {
@ComposeWebInternalApi
fun applyToNodeStyle(nodeStyle: CSSStyleDeclaration)
}
style?.properties?.forEach { (name, value) ->
setProperty(node.style, name, value)
}
style?.variables?.forEach { (name, value) ->
setVariable(node.style, name, value)
}
fun updateStyleDeclarations(styleApplier: StyleDeclarationsApplier) {
node.removeAttribute("style")
styleApplier.applyToNodeStyle(node.style)
}
}

7
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/GlobalSnapshotManager.kt → web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/GlobalSnapshotManager.kt

@ -1,8 +1,8 @@
package org.jetbrains.compose.web
package org.jetbrains.compose.web.internal.runtime
import androidx.compose.runtime.snapshots.ObserverHandle
import androidx.compose.runtime.snapshots.Snapshot
import org.jetbrains.compose.web.GlobalSnapshotManager.ensureStarted
import org.jetbrains.compose.web.internal.runtime.GlobalSnapshotManager.ensureStarted
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
@ -16,7 +16,8 @@ import kotlinx.coroutines.launch
* Composition bootstrapping mechanisms for a particular platform/framework should call
* [ensureStarted] during setup to initialize periodic global snapshot notifications.
*/
internal object GlobalSnapshotManager {
@ComposeWebInternalApi
object GlobalSnapshotManager {
private var started = false
private var commitPending = false
private var removeWriteObserver: (ObserverHandle)? = null

5
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/JsMicrotasksDispatcher.kt → web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/JsMicrotasksDispatcher.kt

@ -1,11 +1,12 @@
package org.jetbrains.compose.web
package org.jetbrains.compose.web.internal.runtime
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Runnable
import kotlin.coroutines.CoroutineContext
import kotlin.js.Promise
internal class JsMicrotasksDispatcher : CoroutineDispatcher() {
@ComposeWebInternalApi
class JsMicrotasksDispatcher : CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {
Promise.resolve(Unit).then { block.run() }
}

4
web/settings.gradle.kts

@ -30,7 +30,7 @@ fun module(name: String, path: String) {
if (!projectDir.exists()) {
throw AssertionError("file $projectDir does not exist")
}
project(name).projectDir = projectDir
project(name).projectDir = projectDir
}
@ -39,6 +39,8 @@ module(":web-widgets", "widgets")
module(":web-integration-core", "integration-core")
module(":web-integration-widgets", "integration-widgets")
module(":compose-compiler-integration", "compose-compiler-integration")
module(":internal-web-core-runtime", "internal-web-core-runtime")
module(":test-utils", "test-utils")
if (extra["compose.web.tests.skip.benchmarks"]!!.toString().toBoolean() != true) {
module(":web-benchmark-core", "benchmark-core")

42
web/test-utils/build.gradle.kts

@ -0,0 +1,42 @@
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
}
repositories {
mavenCentral()
}
kotlin {
js(IR) {
browser() {
testTask {
testLogging.showStandardStreams = true
useKarma {
useChromeHeadless()
useFirefox()
}
}
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(kotlin("stdlib-common"))
}
}
val jsMain by getting {
dependencies {
implementation(project(":internal-web-core-runtime"))
implementation(kotlin("stdlib-js"))
}
}
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
}
}
}
}

4
web/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/ComposeWebExperimentalTestsApi.kt

@ -0,0 +1,4 @@
package org.jetbrains.compose.web.testutils
@RequiresOptIn("This API is experimental and is likely to change in the future.")
annotation class ComposeWebExperimentalTestsApi

70
web/core/src/jsTest/kotlin/TestUtils.kt → web/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/TestUtils.kt

@ -1,17 +1,14 @@
package org.jetbrains.compose.web.core.tests
package org.jetbrains.compose.web.testutils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MonotonicFrameClock
import org.jetbrains.compose.web.renderComposable
import androidx.compose.runtime.*
import kotlinx.browser.document
import kotlinx.browser.window
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.promise
import kotlinx.dom.clear
import org.jetbrains.compose.web.internal.runtime.*
import org.w3c.dom.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.time.DurationUnit
@ -23,6 +20,7 @@ import kotlin.time.toDuration
* There is no need to create its instances manually.
* @see [runTest]
*/
@ComposeWebExperimentalTestsApi
class TestScope : CoroutineScope by MainScope() {
/**
@ -31,7 +29,7 @@ class TestScope : CoroutineScope by MainScope() {
*/
val root = "div".asHtmlElement()
private val recompositionCompleteEventsChannel = Channel<Unit>()
private var waitForRecompositionCompleteContinuation: Continuation<Unit>? = null
private val childrenIterator = root.children.asList().listIterator()
init {
@ -39,28 +37,58 @@ class TestScope : CoroutineScope by MainScope() {
}
private fun onRecompositionComplete() {
launch {
recompositionCompleteEventsChannel.send(Unit)
}
waitForRecompositionCompleteContinuation?.resume(Unit)
waitForRecompositionCompleteContinuation = null
}
/**
* Cleans up the [root] content.
* Creates a new composition with a given Composable [content].
*/
@ComposeWebExperimentalTestsApi
fun composition(content: @Composable () -> Unit) {
root.clear()
renderComposable(
root = root,
monotonicFrameClock = TestMonotonicClockImpl(
onRecomposeComplete = this::onRecompositionComplete
)
) {
renderTestComposable(root = root) {
content()
}
}
/**
* Use this method to test the composition mounted at [root]
*
* @param root - the [Element] that will be the root of the DOM tree managed by Compose
* @param content - the Composable lambda that defines the composition content
*
* @return the instance of the [Composition]
*/
@OptIn(ComposeWebInternalApi::class)
@ComposeWebExperimentalTestsApi
fun <TElement : Element> renderTestComposable(
root: TElement,
content: @Composable () -> Unit
): Composition {
GlobalSnapshotManager.ensureStarted()
val context = TestMonotonicClockImpl(
onRecomposeComplete = this::onRecompositionComplete
) + JsMicrotasksDispatcher()
val recomposer = Recomposer(context)
val composition = ControlledComposition(
applier = DomApplier(DomNodeWrapper(root)),
parent = recomposer
)
composition.setContent @Composable {
content()
}
CoroutineScope(context).launch(start = CoroutineStart.UNDISPATCHED) {
recomposer.runRecomposeAndApplyChanges()
}
return composition
}
/**
* @return a reference to the next child element of the root.
* Subsequent calls will return next child reference every time.
@ -104,7 +132,9 @@ class TestScope : CoroutineScope by MainScope() {
* Suspends until recomposition completes.
*/
suspend fun waitForRecompositionComplete() {
recompositionCompleteEventsChannel.receive()
suspendCoroutine<Unit> { continuation ->
waitForRecompositionCompleteContinuation = continuation
}
}
}
@ -140,11 +170,13 @@ class TestScope : CoroutineScope by MainScope() {
* }
* ```
*/
@ComposeWebExperimentalTestsApi
fun runTest(block: suspend TestScope.() -> Unit): dynamic {
val scope = TestScope()
return scope.promise { block(scope) }
}
@ComposeWebExperimentalTestsApi
fun String.asHtmlElement() = document.createElement(this) as HTMLElement
private object MutationObserverOptions : MutationObserverInit {

82
web/test-utils/src/jsTest/kotlin/TestsForTestUtils.kt

@ -0,0 +1,82 @@
import androidx.compose.runtime.RecomposeScope
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.currentRecomposeScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jetbrains.compose.web.testutils.ComposeWebExperimentalTestsApi
import org.jetbrains.compose.web.testutils.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
@OptIn(ComposeWebExperimentalTestsApi::class)
class TestsForTestUtils {
@Test
fun waitForRecompositionComplete_suspends_and_continues_properly() = runTest {
var recomposeScope: RecomposeScope? = null
composition {
recomposeScope = currentRecomposeScope
}
delay(100) // to let the initial composition complete
var waitForRecompositionCompleteContinued = false
val job = launch {
waitForRecompositionComplete()
waitForRecompositionCompleteContinued = true
}
delay(100) // to check that `waitForRecompositionComplete` is suspended after delay
assertEquals(false, waitForRecompositionCompleteContinued)
delay(100)
// we made no changes during 100 ms, so `waitForRecompositionComplete` should remain suspended
assertEquals(false, waitForRecompositionCompleteContinued)
recomposeScope!!.invalidate() // force recomposition
job.join()
assertEquals(true, waitForRecompositionCompleteContinued)
}
@Test
fun waitForChanges_suspends_and_continues_properly() = runTest {
var waitForChangesContinued = false
var recomposeScope: RecomposeScope? = null
var showText = ""
composition {
recomposeScope = currentRecomposeScope
SideEffect {
root.innerText = showText
}
}
assertEquals("<div></div>", root.outerHTML)
val job = launch {
waitForChanges(root)
waitForChangesContinued = true
}
delay(100) // to check that `waitForChanges` is suspended after delay
assertEquals(false, waitForChangesContinued)
// force recomposition and check that `waitForChanges` remains suspended as no changes occurred
recomposeScope!!.invalidate()
waitForRecompositionComplete()
assertEquals(false, waitForChangesContinued)
// Make changes and check that `waitForChanges` continues
showText = "Hello World!"
recomposeScope!!.invalidate()
waitForRecompositionComplete()
job.join()
assertEquals(true, waitForChangesContinued)
assertEquals("<div>Hello World!</div>", root.outerHTML)
}
}
Loading…
Cancel
Save