diff --git a/tutorials/Image_And_Icons_Manipulations/README.md b/tutorials/Image_And_Icons_Manipulations/README.md
index b5b8d046a0..b34bb292c4 100755
--- a/tutorials/Image_And_Icons_Manipulations/README.md
+++ b/tutorials/Image_And_Icons_Manipulations/README.md
@@ -19,7 +19,7 @@ import androidx.compose.ui.window.singleWindowApplication
fun main() = singleWindowApplication {
Image(
- painter = painterResource("sample.png"), // ImageBitmap
+ painter = painterResource("sample.png"),
contentDescription = "Sample",
modifier = Modifier.fillMaxSize()
)
@@ -135,70 +135,66 @@ fun loadXmlImageVector(file: File, density: Density): ImageVector =
[XML vector drawable](../../artwork/compose-logo.xml)
-## Drawing raw image data using native canvas
-
-You may want to draw raw image data, in which case you can use `Canvas` and` drawIntoCanvas`.
-
+## Drawing images using Canvas
```kotlin
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Paint
-import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.graphics.withSave
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.loadImageBitmap
+import androidx.compose.ui.res.loadSvgPainter
+import androidx.compose.ui.res.loadXmlImageVector
import androidx.compose.ui.res.useResource
import androidx.compose.ui.window.singleWindowApplication
-import org.jetbrains.skija.Bitmap
-import org.jetbrains.skija.ColorAlphaType
-import org.jetbrains.skija.ImageInfo
-
-private val sample = useResource("sample.png", ::loadImageBitmap)
+import org.xml.sax.InputSource
fun main() = singleWindowApplication {
- val bitmap = remember { bitmapFromByteArray(sample.getBytes(), sample.width, sample.height) }
+ val density = LocalDensity.current // to calculate the intrinsic size of vector images (SVG, XML)
+
+ val sample = remember {
+ useResource("sample.png", ::loadImageBitmap)
+ }
+ val ideaLogo = remember {
+ useResource("idea-logo.svg") { loadSvgPainter(it, density) }
+ }
+ val composeLogo = rememberVectorPainter(
+ remember {
+ useResource("compose-logo.xml") { loadXmlImageVector(InputSource(it), density) }
+ }
+ )
+
Canvas(
modifier = Modifier.fillMaxSize()
) {
drawIntoCanvas { canvas ->
- canvas.drawImage(bitmap, Offset.Zero, Paint())
+ canvas.withSave {
+ canvas.drawImage(sample, Offset.Zero, Paint())
+ canvas.translate(sample.width.toFloat(), 0f)
+ with(ideaLogo) {
+ draw(ideaLogo.intrinsicSize)
+ }
+ canvas.translate(ideaLogo.intrinsicSize.width, 0f)
+ with(composeLogo) {
+ draw(Size(100f, 100f))
+ }
+ }
}
}
}
+```
-fun bitmapFromByteArray(pixels: ByteArray, width: Int, height: Int): ImageBitmap {
- val bitmap = Bitmap()
- bitmap.allocPixels(ImageInfo.makeS32(width, height, ColorAlphaType.PREMUL))
- bitmap.installPixels(bitmap.imageInfo, pixels, (width * 4).toLong())
- return bitmap.asImageBitmap()
-}
-
-// creating byte array from BufferedImage
-private fun ImageBitmap.getBytes(): ByteArray {
- val buffer = IntArray(width * height)
- readPixels(buffer)
-
- val pixels = ByteArray(width * height * 4)
-
- var index = 0
- for (y in 0 until height) {
- for (x in 0 until width) {
- val pixel = buffer[y * width + x]
- pixels[index++] = ((pixel and 0xFF)).toByte() // Blue component
- pixels[index++] = (((pixel shr 8) and 0xFF)).toByte() // Green component
- pixels[index++] = (((pixel shr 16) and 0xFF)).toByte() // Red component
- pixels[index++] = (((pixel shr 24) and 0xFF)).toByte() // Alpha component
- }
- }
+[PNG](sample.png)
- return pixels
-}
-```
+[SVG](../../artwork/idea-logo.svg)
-
+[XML vector drawable](../../artwork/compose-logo.xml)
## Setting the application window icon
@@ -261,4 +257,4 @@ fun main() = application {
}
```
-
\ No newline at end of file
+
diff --git a/tutorials/Image_And_Icons_Manipulations/draw_image_into_canvas.png b/tutorials/Image_And_Icons_Manipulations/draw_image_into_canvas.png
deleted file mode 100755
index 370d3a9af2..0000000000
Binary files a/tutorials/Image_And_Icons_Manipulations/draw_image_into_canvas.png and /dev/null differ
diff --git a/web/core/build.gradle.kts b/web/core/build.gradle.kts
index f4659e50fa..6df6e53958 100644
--- a/web/core/build.gradle.kts
+++ b/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")
+ }
+ }
}
}
diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/RenderComposable.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/RenderComposable.kt
index 9640debd9e..978f7f7031 100644
--- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/RenderComposable.kt
+++ b/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 renderComposable(
root: TElement,
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/SyntheticEventListener.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/SyntheticEventListener.kt
index fa7926eaba..66e0f4a482 100644
--- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/SyntheticEventListener.kt
+++ b/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.ComposeWebInternalApi
+import org.jetbrains.compose.web.internal.runtime.NamedEventListener
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> internal constructor(
val event: String,
val options: Options,
val listener: (T) -> Unit
-) : EventListener {
+) : EventListener, NamedEventListener {
+
+ override val name: String = event
@Suppress("UNCHECKED_CAST")
override fun handleEvent(event: Event) {
diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StyleBuilder.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StyleBuilder.kt
index 4020e79e16..595b078474 100644
--- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StyleBuilder.kt
+++ b/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 CSSStyleVariable.value(fallback: TValue? = null)
* property("width", AppCSSVariables.width.value())
* }
*```
- *
+ *
*/
fun variable() =
ReadOnlyProperty> { _, property ->
@@ -128,6 +131,7 @@ interface StyleHolder {
val variables: StylePropertyList
}
+@OptIn(ComposeWebInternalApi::class)
@Suppress("EqualsOrHashCode")
open class StyleBuilderImpl : StyleBuilder, StyleHolder {
override val properties: MutableStylePropertyList = mutableListOf()
diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/transform.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/transform.kt
index 37cdecfe4e..c5e87c67d1 100644
--- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/transform.kt
+++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/transform.kt
@@ -18,39 +18,39 @@ interface TransformBuilder {
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
+ )
+
+ fun perspective(d: CSSLengthValue)
+ fun rotate(a: CSSAngleValue)
+ fun rotate3d(x: Number, y: Number, z: Number, a: CSSAngleValue)
+ fun rotateX(a: CSSAngleValue)
+ fun rotateY(a: CSSAngleValue)
+ fun rotateZ(a: CSSAngleValue)
+ fun scale(sx: Number)
+ fun scale(sx: Number, sy: Number)
+ fun scale3d(sx: Number, sy: Number, sz: Number)
+ fun scaleX(s: Number)
+ fun scaleY(s: Number)
+ fun scaleZ(s: Number)
+ fun skew(ax: CSSAngleValue)
+ fun skew(ax: CSSAngleValue, ay: CSSAngleValue)
+ fun skewX(a: CSSAngleValue)
+ fun skewY(a: CSSAngleValue)
+ fun translate(tx: CSSLengthValue)
+ fun translate(tx: CSSPercentageValue)
+ fun translate(tx: CSSLengthValue, ty: CSSLengthValue)
+ fun translate(tx: CSSLengthValue, ty: CSSPercentageValue)
+ fun translate(tx: CSSPercentageValue, ty: CSSLengthValue)
+ fun translate(tx: CSSPercentageValue, ty: CSSPercentageValue)
+ fun translate3d(tx: CSSLengthValue, ty: CSSLengthValue, tz: CSSLengthValue)
+ fun translate3d(tx: CSSLengthValue, ty: CSSPercentageValue, tz: CSSLengthValue)
+ fun translate3d(tx: CSSPercentageValue, ty: CSSLengthValue, tz: CSSLengthValue)
+ fun translate3d(tx: CSSPercentageValue, ty: CSSPercentageValue, tz: CSSLengthValue)
+ fun translateX(tx: CSSLengthValue)
+ fun translateX(tx: CSSPercentageValue)
+ fun translateY(ty: CSSLengthValue)
+ fun translateY(ty: CSSPercentageValue)
+ fun translateZ(tz: CSSLengthValue)
}
private class TransformBuilderImplementation : TransformBuilder {
@@ -64,67 +64,133 @@ private class TransformBuilderImplementation : TransformBuilder {
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 perspective(d: CSSLengthValue) = transformations.add { "perspective($d)" }
+ override fun rotate(a: CSSAngleValue) {
+ transformations.add { "rotate($a)" }
+ }
- override fun rotate(a: CSSAngleValue) = transformations.add { "rotate($a)" }
- override fun rotate3d(x: Number, y: Number, z: Number, a: CSSAngleValue) =
+ 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 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 rotateZ(a: CSSAngleValue) {
+ transformations.add { "rotateZ($a)" }
+ }
+
+ override fun scale(sx: Number) {
+ transformations.add { "scale($sx)" }
+ }
- 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) =
+ 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 scaleX(s: Number) {
+ transformations.add { "scaleX($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 scaleY(s: Number) {
+ transformations.add { "scaleY($s)" }
+ }
- override fun translate(tx: CSSLengthValue) = transformations.add { "translate($tx)" }
- override fun translate(tx: CSSPercentageValue) = transformations.add { "translate($tx)" }
+ override fun scaleZ(s: Number) {
+ transformations.add { "scaleZ($s)" }
+ }
- override fun translate(tx: CSSLengthValue, ty: CSSLengthValue) =
+ 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) =
+ override fun translate(tx: CSSLengthValue, ty: CSSPercentageValue) {
transformations.add { "translate($tx, $ty)" }
+ }
- override fun translate(tx: CSSPercentageValue, ty: CSSLengthValue) =
+ override fun translate(tx: CSSPercentageValue, ty: CSSLengthValue) {
transformations.add { "translate($tx, $ty)" }
+ }
- override fun translate(tx: CSSPercentageValue, ty: CSSPercentageValue) =
+ override fun translate(tx: CSSPercentageValue, ty: CSSPercentageValue) {
transformations.add { "translate($tx, $ty)" }
+ }
- override fun translate3d(tx: CSSLengthValue, ty: CSSLengthValue, tz: CSSLengthValue) =
+ override fun translate3d(tx: CSSLengthValue, ty: CSSLengthValue, tz: CSSLengthValue) {
transformations.add { "translate3d($tx, $ty, $tz)" }
+ }
- override fun translate3d(tx: CSSLengthValue, ty: CSSPercentageValue, tz: CSSLengthValue) =
+ override fun translate3d(tx: CSSLengthValue, ty: CSSPercentageValue, tz: CSSLengthValue) {
transformations.add { "translate3d($tx, $ty, $tz)" }
+ }
- override fun translate3d(tx: CSSPercentageValue, ty: CSSLengthValue, tz: CSSLengthValue) =
+ override fun translate3d(tx: CSSPercentageValue, ty: CSSLengthValue, tz: CSSLengthValue) {
transformations.add { "translate3d($tx, $ty, $tz)" }
+ }
- override fun translate3d(tx: CSSPercentageValue, ty: CSSPercentageValue, tz: CSSLengthValue) =
+ 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 translateX(tx: CSSLengthValue) {
+ transformations.add { "translateX($tx)" }
+ }
- override fun translateY(ty: CSSLengthValue) = transformations.add { "translateY($ty)" }
- override fun translateY(ty: CSSPercentageValue) = transformations.add { "translateY($ty)" }
+ override fun translateX(tx: CSSPercentageValue) {
+ transformations.add { "translateX($tx)" }
+ }
- override fun translateZ(tz: CSSLengthValue) = transformations.add { "translateZ($tz)" }
+ 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() }
@@ -132,7 +198,7 @@ private class TransformBuilderImplementation : TransformBuilder {
}
@ExperimentalComposeWebApi
-fun StyleBuilder.transform(transformFunction: TransformBuilder.() -> Unit) {
+fun StyleBuilder.transform(transformContext: TransformBuilder.() -> Unit) {
val transformBuilder = TransformBuilderImplementation()
- property("transform", transformBuilder.apply(transformFunction).toString())
+ property("transform", transformBuilder.apply(transformContext).toString())
}
\ No newline at end of file
diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Base.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Base.kt
index 26ce1ca8b6..b00eef957e 100644
--- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Base.kt
+++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Base.kt
@@ -1,32 +1,25 @@
package org.jetbrains.compose.web.dom
-import androidx.compose.runtime.Applier
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.ComposeCompilerApi
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.DisposableEffectResult
-import androidx.compose.runtime.DisposableEffectScope
-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 androidx.compose.runtime.*
import org.jetbrains.compose.web.attributes.AttrsBuilder
import org.jetbrains.compose.web.ExperimentalComposeWebApi
+import org.jetbrains.compose.web.css.StyleHolder
+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
+import org.w3c.dom.css.ElementCSSInlineStyle
+import org.w3c.dom.svg.SVGElement
@OptIn(ComposeCompilerApi::class)
@Composable
@ExplicitGroupsComposable
-inline fun > ComposeDomNode(
+private inline fun ComposeDomNode(
noinline factory: () -> T,
elementScope: TScope,
noinline attrsSkippableUpdate: @Composable SkippableUpdater.() -> Unit,
noinline content: (@Composable TScope.() -> Unit)?
) {
- if (currentComposer.applier !is E) error("Invalid applier")
currentComposer.startNode()
if (currentComposer.inserting) {
currentComposer.createNode(factory)
@@ -34,9 +27,7 @@ inline fun > ComposeDomNode(
currentComposer.useNode()
}
- SkippableUpdater(currentComposer).apply {
- attrsSkippableUpdate()
- }
+ attrsSkippableUpdate.invoke(SkippableUpdater(currentComposer))
currentComposer.startReplaceableGroup(0x7ab4aae9)
content?.invoke(elementScope)
@@ -44,48 +35,85 @@ inline fun > ComposeDomNode(
currentComposer.endNode()
}
-class DisposableEffectHolder(
- var effect: (DisposableEffectScope.(TElement) -> DisposableEffectResult)? = null
-)
+@OptIn(ComposeWebInternalApi::class)
+private fun DomElementWrapper.updateProperties(applicators: List Unit, Any>>) {
+ node.removeAttribute("class")
+
+ applicators.forEach { (applicator, item) ->
+ applicator(node, item)
+ }
+}
+
+@OptIn(ComposeWebInternalApi::class)
+private fun DomElementWrapper.updateStyleDeclarations(styleApplier: StyleHolder) {
+ when (node) {
+ is HTMLElement, is SVGElement -> {
+ node.removeAttribute("style")
+
+ val style = node.unsafeCast().style
+
+ styleApplier.properties.forEach { (name, value) ->
+ style.setProperty(name, value.toString())
+ }
+
+ styleApplier.variables.forEach { (name, value) ->
+ style.setProperty(name, value.toString())
+ }
+ }
+ }
+}
+
+@OptIn(ComposeWebInternalApi::class)
+fun DomElementWrapper.updateAttrs(attrs: Map) {
+ node.getAttributeNames().forEach { name ->
+ if (name == "style") return@forEach
+ node.removeAttribute(name)
+ }
+
+ attrs.forEach {
+ node.setAttribute(it.key, it.value)
+ }
+}
+
+
+@OptIn(ComposeWebInternalApi::class)
@Composable
fun TagElement(
elementBuilder: ElementBuilder,
applyAttrs: (AttrsBuilder.() -> Unit)?,
content: (@Composable ElementScope.() -> Unit)?
) {
- val scope = remember { ElementScopeImpl() }
- val refEffect = remember { DisposableEffectHolder() }
+ val scope = remember { ElementScopeImpl() }
+ var refEffect: (DisposableEffectScope.(TElement) -> DisposableEffectResult)? = null
- ComposeDomNode, DomElementWrapper, DomApplier>(
+ ComposeDomNode, DomElementWrapper>(
factory = {
- DomElementWrapper(elementBuilder.create() as HTMLElement).also {
- scope.element = it.node.unsafeCast()
- }
+ val node = elementBuilder.create()
+ scope.element = node
+ DomElementWrapper(node)
},
attrsSkippableUpdate = {
- val attrsApplied = AttrsBuilder().also {
- if (applyAttrs != null) {
- it.applyAttrs()
- }
- }
- refEffect.effect = attrsApplied.refEffect
- val attrsCollected = attrsApplied.collect()
- val events = attrsApplied.collectListeners()
+ val attrsBuilder = AttrsBuilder()
+ applyAttrs?.invoke(attrsBuilder)
+
+ refEffect = attrsBuilder.refEffect
update {
- set(attrsCollected, DomElementWrapper::updateAttrs)
- set(events, DomElementWrapper::updateEventListeners)
- set(attrsApplied.propertyUpdates, DomElementWrapper::updateProperties)
- set(attrsApplied.styleBuilder, DomElementWrapper::updateStyleDeclarations)
+ set(attrsBuilder.collect(), DomElementWrapper::updateAttrs)
+ set(attrsBuilder.collectListeners(), DomElementWrapper::updateEventListeners)
+ set(attrsBuilder.propertyUpdates, DomElementWrapper::updateProperties)
+ set(attrsBuilder.styleBuilder, DomElementWrapper::updateStyleDeclarations)
}
},
elementScope = scope,
content = content
)
- DisposableEffect(null) {
- refEffect.effect?.invoke(this, scope.element) ?: onDispose {}
+ refEffect?.let { effect ->
+ DisposableEffect(null) {
+ effect.invoke(this, scope.element)
+ }
}
}
diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt
index 644ae0f79e..afab3a5d8c 100644
--- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt
+++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt
@@ -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(
diff --git a/web/core/src/jsTest/kotlin/CSSStylesheetTests.kt b/web/core/src/jsTest/kotlin/CSSStylesheetTests.kt
index 8874c35d08..4fabc6189f 100644
--- a/web/core/src/jsTest/kotlin/CSSStylesheetTests.kt
+++ b/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()
@@ -143,4 +144,4 @@ class CSSVariableTests {
assertEquals("rgb(0, 128, 0)", window.getComputedStyle(el).backgroundColor)
}
}
-}
\ No newline at end of file
+}
diff --git a/web/core/src/jsTest/kotlin/CSSUnitApiTests.kt b/web/core/src/jsTest/kotlin/CSSUnitApiTests.kt
index 20e2788a47..d80dd26290 100644
--- a/web/core/src/jsTest/kotlin/CSSUnitApiTests.kt
+++ b/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)
}
-}
\ No newline at end of file
+}
diff --git a/web/core/src/jsTest/kotlin/ControlledRadioGroupsTests.kt b/web/core/src/jsTest/kotlin/ControlledRadioGroupsTests.kt
index 9597b6f72d..9bf138d794 100644
--- a/web/core/src/jsTest/kotlin/ControlledRadioGroupsTests.kt
+++ b/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 {
diff --git a/web/core/src/jsTest/kotlin/DomSideEffectTests.kt b/web/core/src/jsTest/kotlin/DomSideEffectTests.kt
index d39f3ede06..5c2b69bbfb 100644
--- a/web/core/src/jsTest/kotlin/DomSideEffectTests.kt
+++ b/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
diff --git a/web/core/src/jsTest/kotlin/FailingTestCases.kt b/web/core/src/jsTest/kotlin/FailingTestCases.kt
index a23fc3b022..549da12b15 100644
--- a/web/core/src/jsTest/kotlin/FailingTestCases.kt
+++ b/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 {
diff --git a/web/core/src/jsTest/kotlin/InlineStyleTests.kt b/web/core/src/jsTest/kotlin/InlineStyleTests.kt
index deb1107ab4..7a283bf4bb 100644
--- a/web/core/src/jsTest/kotlin/InlineStyleTests.kt
+++ b/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 {
diff --git a/web/core/src/jsTest/kotlin/StaticComposableTests.kt b/web/core/src/jsTest/kotlin/StaticComposableTests.kt
index 2b1b7785e4..1b5dc868ee 100644
--- a/web/core/src/jsTest/kotlin/StaticComposableTests.kt
+++ b/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
@@ -184,12 +185,10 @@ class StaticComposableTests {
}
@Test
- fun stylesPosition() {
- val root = "div".asHtmlElement()
+ fun stylesPosition() = runTest {
val enumValues = Position.values()
- renderComposable(
- root = root
- ) {
+
+ composition {
enumValues.forEach { position ->
Span(
{
@@ -197,14 +196,14 @@ class StaticComposableTests {
position(position)
}
}
- ) { }
+ )
}
}
enumValues.forEachIndexed { index, position ->
assertEquals(
"position: ${position.value};",
- (root.children[index] as HTMLElement).style.cssText
+ nextChild().style.cssText
)
}
}
diff --git a/web/core/src/jsTest/kotlin/css/CSSBackgroundTests.kt b/web/core/src/jsTest/kotlin/css/CSSBackgroundTests.kt
index 709fea5e50..d989249ed4 100644
--- a/web/core/src/jsTest/kotlin/css/CSSBackgroundTests.kt
+++ b/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)
}
-}
\ No newline at end of file
+}
diff --git a/web/core/src/jsTest/kotlin/css/CSSBorderTests.kt b/web/core/src/jsTest/kotlin/css/CSSBorderTests.kt
index 576a118524..319a717ee1 100644
--- a/web/core/src/jsTest/kotlin/css/CSSBorderTests.kt
+++ b/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)
}
-}
\ No newline at end of file
+}
diff --git a/web/core/src/jsTest/kotlin/css/CSSBoxTests.kt b/web/core/src/jsTest/kotlin/css/CSSBoxTests.kt
index dc2725e265..eb26c1ad35 100644
--- a/web/core/src/jsTest/kotlin/css/CSSBoxTests.kt
+++ b/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)
}
-}
\ No newline at end of file
+}
diff --git a/web/core/src/jsTest/kotlin/css/CSSDisplayTests.kt b/web/core/src/jsTest/kotlin/css/CSSDisplayTests.kt
index 3633eda658..db8bc3f7ea 100644
--- a/web/core/src/jsTest/kotlin/css/CSSDisplayTests.kt
+++ b/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 {
}
}
-}
\ No newline at end of file
+}
diff --git a/web/core/src/jsTest/kotlin/css/CSSFlexTests.kt b/web/core/src/jsTest/kotlin/css/CSSFlexTests.kt
index aabdd2130f..84d1dbfd2a 100644
--- a/web/core/src/jsTest/kotlin/css/CSSFlexTests.kt
+++ b/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 {
}
}
-}
\ No newline at end of file
+}
diff --git a/web/core/src/jsTest/kotlin/css/CSSListStyleTests.kt b/web/core/src/jsTest/kotlin/css/CSSListStyleTests.kt
index 27d6e043d7..22c2464070 100644
--- a/web/core/src/jsTest/kotlin/css/CSSListStyleTests.kt
+++ b/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)
}
-}
\ No newline at end of file
+}
diff --git a/web/core/src/jsTest/kotlin/css/CSSMarginTests.kt b/web/core/src/jsTest/kotlin/css/CSSMarginTests.kt
index 21922d78e1..364c41a0a1 100644
--- a/web/core/src/jsTest/kotlin/css/CSSMarginTests.kt
+++ b/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")
}
-}
\ No newline at end of file
+}
diff --git a/web/core/src/jsTest/kotlin/css/CSSOverflowTests.kt b/web/core/src/jsTest/kotlin/css/CSSOverflowTests.kt
index 80157345e7..113567cbb9 100644
--- a/web/core/src/jsTest/kotlin/css/CSSOverflowTests.kt
+++ b/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 {
}
-}
\ No newline at end of file
+}
diff --git a/web/core/src/jsTest/kotlin/css/CSSPaddingTests.kt b/web/core/src/jsTest/kotlin/css/CSSPaddingTests.kt
index a86c12a0c1..c94a6eed8d 100644
--- a/web/core/src/jsTest/kotlin/css/CSSPaddingTests.kt
+++ b/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")
}
-}
\ No newline at end of file
+}
diff --git a/web/core/src/jsTest/kotlin/css/CSSTextTests.kt b/web/core/src/jsTest/kotlin/css/CSSTextTests.kt
index efae748b17..91f241470b 100644
--- a/web/core/src/jsTest/kotlin/css/CSSTextTests.kt
+++ b/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)
}
-}
\ No newline at end of file
+}
diff --git a/web/core/src/jsTest/kotlin/css/CSSUiTests.kt b/web/core/src/jsTest/kotlin/css/CSSUiTests.kt
index dcde84b40b..4b38f1ef1c 100644
--- a/web/core/src/jsTest/kotlin/css/CSSUiTests.kt
+++ b/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)
}
-}
\ No newline at end of file
+}
diff --git a/web/core/src/jsTest/kotlin/css/ColorTests.kt b/web/core/src/jsTest/kotlin/css/ColorTests.kt
index 09635f149d..f57581150b 100644
--- a/web/core/src/jsTest/kotlin/css/ColorTests.kt
+++ b/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
diff --git a/web/core/src/jsTest/kotlin/css/FilterTests.kt b/web/core/src/jsTest/kotlin/css/FilterTests.kt
index 4a2f1a98aa..89dae0b3f4 100644
--- a/web/core/src/jsTest/kotlin/css/FilterTests.kt
+++ b/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)
}
-}
\ No newline at end of file
+}
diff --git a/web/core/src/jsTest/kotlin/css/GridTests.kt b/web/core/src/jsTest/kotlin/css/GridTests.kt
index fa1340985c..48bae5779c 100644
--- a/web/core/src/jsTest/kotlin/css/GridTests.kt
+++ b/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)
}
-}
\ No newline at end of file
+}
diff --git a/web/core/src/jsTest/kotlin/css/TransformTests.kt b/web/core/src/jsTest/kotlin/css/TransformTests.kt
index ce671d8181..b2a0d0a609 100644
--- a/web/core/src/jsTest/kotlin/css/TransformTests.kt
+++ b/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)
}
-}
\ No newline at end of file
+}
diff --git a/web/core/src/jsTest/kotlin/elements/AttributesTests.kt b/web/core/src/jsTest/kotlin/elements/AttributesTests.kt
index 15bb55a7e1..ce55090ac2 100644
--- a/web/core/src/jsTest/kotlin/elements/AttributesTests.kt
+++ b/web/core/src/jsTest/kotlin/elements/AttributesTests.kt
@@ -3,6 +3,8 @@ package org.jetbrains.compose.web.core.tests
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
+import kotlinx.browser.document
+import kotlinx.dom.clear
import org.jetbrains.compose.web.attributes.AttrsBuilder
import org.jetbrains.compose.web.attributes.disabled
import org.jetbrains.compose.web.attributes.forId
@@ -17,6 +19,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 {
@@ -333,26 +336,72 @@ class AttributesTests {
assertEquals(0, refDisposeCounter)
}
+ @Test
+ fun disposableRefEffectWithChangingKey() = runTest {
+ var key by mutableStateOf(0)
+
+ composition {
+ val readKey = key // read key here to recompose an entire scope
+ Div(
+ attrs = {
+ id("id$readKey")
+ }
+ ) {
+ DisposableRefEffect(readKey) {
+ val p = document.createElement("p").also { it.innerHTML = "Key=$readKey" }
+ it.appendChild(p)
+
+ onDispose {
+ it.clear()
+ }
+ }
+ }
+ }
+
+ assertEquals(
+ expected = "",
+ actual = root.outerHTML
+ )
+
+ key = 1
+ waitForRecompositionComplete()
+
+ assertEquals(
+ expected = "",
+ actual = root.outerHTML
+ )
+ }
+
@Test // issue: https://github.com/JetBrains/compose-jb/issues/981
fun attributesUpdateShouldNotCauseInlineStylesCleanUp() = runTest {
var hasValue by mutableStateOf(false)
composition {
Button(attrs = {
+ classes("a")
style {
color(Color.red)
}
- if (hasValue) value("buttonValue")
+ if (hasValue) {
+ classes("b")
+ value("buttonValue")
+ }
}) {
Text("Button")
}
}
- assertEquals("""""", root.innerHTML)
+ assertEquals(
+ expected = "",
+ actual = nextChild().outerHTML
+ )
hasValue = true
waitForRecompositionComplete()
- assertEquals("""""", root.innerHTML)
+ assertEquals(
+ expected = "",
+ actual = currentChild().outerHTML
+ )
}
}
diff --git a/web/core/src/jsTest/kotlin/elements/ElementsTests.kt b/web/core/src/jsTest/kotlin/elements/ElementsTests.kt
index 1b8feab268..19d8b3f6a1 100644
--- a/web/core/src/jsTest/kotlin/elements/ElementsTests.kt
+++ b/web/core/src/jsTest/kotlin/elements/ElementsTests.kt
@@ -6,9 +6,11 @@
package org.jetbrains.compose.web.core.tests.elements
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import kotlinx.browser.document
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
@@ -131,4 +133,30 @@ class ElementsTests {
assertEquals("CUSTOM
", root.outerHTML)
}
+
+ @Test
+ fun elementBuilderShouldBeCalledOnce() = runTest {
+ var counter = 0
+ var flag = false
+
+ composition {
+ TagElement({
+ counter++
+ document.createElement("div")
+ }, null,
+ if (flag) {
+ { Div() { Text("ON") } }
+ } else null
+ )
+
+ }
+
+ assertEquals(1, counter, )
+
+ flag = true
+ waitForRecompositionComplete()
+
+ assertEquals(1, counter)
+ assertEquals("", nextChild().outerHTML)
+ }
}
diff --git a/web/core/src/jsTest/kotlin/elements/EventTests.kt b/web/core/src/jsTest/kotlin/elements/EventTests.kt
index eda84323d2..d432b6e8d4 100644
--- a/web/core/src/jsTest/kotlin/elements/EventTests.kt
+++ b/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 {
diff --git a/web/core/src/jsTest/kotlin/elements/InputsGenerateCorrectHtmlTests.kt b/web/core/src/jsTest/kotlin/elements/InputsGenerateCorrectHtmlTests.kt
index 9e66107903..f9ab963eda 100644
--- a/web/core/src/jsTest/kotlin/elements/InputsGenerateCorrectHtmlTests.kt
+++ b/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 {
diff --git a/web/core/src/jsTest/kotlin/elements/TableTests.kt b/web/core/src/jsTest/kotlin/elements/TableTests.kt
index edcc3ad4db..bfeeb9297d 100644
--- a/web/core/src/jsTest/kotlin/elements/TableTests.kt
+++ b/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 {
diff --git a/web/gradle.properties b/web/gradle.properties
index a73618b9a8..2047134e94 100644
--- a/web/gradle.properties
+++ b/web/gradle.properties
@@ -1,6 +1,6 @@
# __LATEST_COMPOSE_RELEASE_VERSION__
-COMPOSE_CORE_VERSION=0.0.0-master-build316
-COMPOSE_WEB_VERSION=1.0.0-alpha1
+COMPOSE_CORE_VERSION=1.0.0-alpha4-build321
+COMPOSE_WEB_VERSION=1.0.0-alpha4
compose.web.buildSamples=false
compose.web.tests.integration.withFirefox
compose.web.tests.skip.benchmarks=false
diff --git a/web/internal-web-core-runtime/build.gradle.kts b/web/internal-web-core-runtime/build.gradle.kts
new file mode 100644
index 0000000000..442c4e127c
--- /dev/null
+++ b/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"))
+ }
+ }
+ }
+}
diff --git a/web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/ComposeWebInternalApi.kt b/web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/ComposeWebInternalApi.kt
new file mode 100644
index 0000000000..a2c6a4cecc
--- /dev/null
+++ b/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
diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/DomApplier.kt b/web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/DomApplier.kt
similarity index 52%
rename from web/core/src/jsMain/kotlin/org/jetbrains/compose/web/DomApplier.kt
rename to web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/DomApplier.kt
index 59ec4db962..bef7baf0cb 100644
--- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/DomApplier.kt
+++ b/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(root) {
@@ -34,28 +33,14 @@ 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>()
-
- fun updateEventListeners(list: List>) {
- val htmlElement = node as? HTMLElement ?: return
-
- currentListeners.forEach {
- htmlElement.removeEventListener(it.event, it)
- }
- currentListeners = list
+@ComposeWebInternalApi
+interface NamedEventListener : EventListener {
+ val name: String
+}
- currentListeners.forEach {
- htmlElement.addEventListener(it.event, it)
- }
- }
+@ComposeWebInternalApi
+open class DomNodeWrapper(open val node: Node) {
fun insert(index: Int, nodeWrapper: DomNodeWrapper) {
val length = node.childNodes.length
@@ -88,37 +73,19 @@ internal open class DomNodeWrapper(open val node: Node) {
}
}
+@ComposeWebInternalApi
+class DomElementWrapper(override val node: Element): DomNodeWrapper(node) {
+ private var currentListeners = emptyList()
-internal class DomElementWrapper(override val node: HTMLElement): DomNodeWrapper(node) {
- private var currentAttrs: Map? = null
-
- fun updateAttrs(attrs: Map) {
- currentAttrs?.forEach {
- node.removeAttribute(it.key)
- }
-
- attrs.forEach {
- node.setAttribute(it.key, it.value)
- }
- currentAttrs = attrs
- }
-
- fun updateProperties(list: List Unit, Any>>) {
- if (node.className.isNotEmpty()) node.className = ""
-
- list.forEach {
- it.first(node, it.second)
+ fun updateEventListeners(list: List) {
+ currentListeners.forEach {
+ node.removeEventListener(it.name, it)
}
- }
- fun updateStyleDeclarations(style: StyleHolder?) {
- node.removeAttribute("style")
+ currentListeners = list
- style?.properties?.forEach { (name, value) ->
- setProperty(node.style, name, value)
- }
- style?.variables?.forEach { (name, value) ->
- setVariable(node.style, name, value)
+ currentListeners.forEach {
+ node.addEventListener(it.name, it)
}
}
}
diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/GlobalSnapshotManager.kt b/web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/GlobalSnapshotManager.kt
similarity index 92%
rename from web/core/src/jsMain/kotlin/org/jetbrains/compose/web/GlobalSnapshotManager.kt
rename to web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/GlobalSnapshotManager.kt
index 647db56fc5..71183b7576 100644
--- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/GlobalSnapshotManager.kt
+++ b/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
diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/JsMicrotasksDispatcher.kt b/web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/JsMicrotasksDispatcher.kt
similarity index 68%
rename from web/core/src/jsMain/kotlin/org/jetbrains/compose/web/JsMicrotasksDispatcher.kt
rename to web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/JsMicrotasksDispatcher.kt
index 93b382e3b0..dd64b039a5 100644
--- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/JsMicrotasksDispatcher.kt
+++ b/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() }
}
diff --git a/web/settings.gradle.kts b/web/settings.gradle.kts
index 968b43b02b..5f8c875589 100644
--- a/web/settings.gradle.kts
+++ b/web/settings.gradle.kts
@@ -30,13 +30,15 @@ fun module(name: String, path: String) {
if (!projectDir.exists()) {
throw AssertionError("file $projectDir does not exist")
}
- project(name).projectDir = projectDir
+ project(name).projectDir = projectDir
}
module(":web-core", "core")
module(":web-widgets", "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")
diff --git a/web/test-utils/build.gradle.kts b/web/test-utils/build.gradle.kts
new file mode 100644
index 0000000000..6bf70113a6
--- /dev/null
+++ b/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"))
+ }
+ }
+ }
+}
diff --git a/web/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/ComposeWebExperimentalTestsApi.kt b/web/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/ComposeWebExperimentalTestsApi.kt
new file mode 100644
index 0000000000..a4221c81c5
--- /dev/null
+++ b/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
diff --git a/web/core/src/jsTest/kotlin/TestUtils.kt b/web/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/TestUtils.kt
similarity index 70%
rename from web/core/src/jsTest/kotlin/TestUtils.kt
rename to web/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/TestUtils.kt
index 5a0f554b62..0ae538a87e 100644
--- a/web/core/src/jsTest/kotlin/TestUtils.kt
+++ b/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()
+ private var waitForRecompositionCompleteContinuation: Continuation? = 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 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 { 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 {
diff --git a/web/test-utils/src/jsTest/kotlin/TestsForTestUtils.kt b/web/test-utils/src/jsTest/kotlin/TestsForTestUtils.kt
new file mode 100644
index 0000000000..a8c1cd50a7
--- /dev/null
+++ b/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("", 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("Hello World!
", root.outerHTML)
+ }
+}