Browse Source

Merge branch 'master' into samples_against_master

samples_against_master
Shagen Ogandzhanian 3 years ago
parent
commit
ddd0fe0645
  1. 76
      tutorials/Image_And_Icons_Manipulations/README.md
  2. BIN
      tutorials/Image_And_Icons_Manipulations/draw_image_into_canvas.png
  3. 8
      web/core/build.gradle.kts
  4. 2
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/RenderComposable.kt
  5. 7
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/SyntheticEventListener.kt
  6. 4
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StyleBuilder.kt
  7. 200
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/transform.kt
  8. 104
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Base.kt
  9. 6
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt
  10. 1
      web/core/src/jsTest/kotlin/CSSStylesheetTests.kt
  11. 1
      web/core/src/jsTest/kotlin/CSSUnitApiTests.kt
  12. 1
      web/core/src/jsTest/kotlin/ControlledRadioGroupsTests.kt
  13. 1
      web/core/src/jsTest/kotlin/DomSideEffectTests.kt
  14. 2
      web/core/src/jsTest/kotlin/FailingTestCases.kt
  15. 1
      web/core/src/jsTest/kotlin/InlineStyleTests.kt
  16. 13
      web/core/src/jsTest/kotlin/StaticComposableTests.kt
  17. 4
      web/core/src/jsTest/kotlin/css/CSSBackgroundTests.kt
  18. 2
      web/core/src/jsTest/kotlin/css/CSSBorderTests.kt
  19. 4
      web/core/src/jsTest/kotlin/css/CSSBoxTests.kt
  20. 4
      web/core/src/jsTest/kotlin/css/CSSDisplayTests.kt
  21. 2
      web/core/src/jsTest/kotlin/css/CSSFlexTests.kt
  22. 2
      web/core/src/jsTest/kotlin/css/CSSListStyleTests.kt
  23. 4
      web/core/src/jsTest/kotlin/css/CSSMarginTests.kt
  24. 4
      web/core/src/jsTest/kotlin/css/CSSOverflowTests.kt
  25. 4
      web/core/src/jsTest/kotlin/css/CSSPaddingTests.kt
  26. 5
      web/core/src/jsTest/kotlin/css/CSSTextTests.kt
  27. 4
      web/core/src/jsTest/kotlin/css/CSSUiTests.kt
  28. 2
      web/core/src/jsTest/kotlin/css/ColorTests.kt
  29. 2
      web/core/src/jsTest/kotlin/css/FilterTests.kt
  30. 4
      web/core/src/jsTest/kotlin/css/GridTests.kt
  31. 2
      web/core/src/jsTest/kotlin/css/TransformTests.kt
  32. 55
      web/core/src/jsTest/kotlin/elements/AttributesTests.kt
  33. 30
      web/core/src/jsTest/kotlin/elements/ElementsTests.kt
  34. 1
      web/core/src/jsTest/kotlin/elements/EventTests.kt
  35. 3
      web/core/src/jsTest/kotlin/elements/InputsGenerateCorrectHtmlTests.kt
  36. 1
      web/core/src/jsTest/kotlin/elements/TableTests.kt
  37. 4
      web/gradle.properties
  38. 33
      web/internal-web-core-runtime/build.gradle.kts
  39. 4
      web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/ComposeWebInternalApi.kt
  40. 71
      web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/DomApplier.kt
  41. 7
      web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/GlobalSnapshotManager.kt
  42. 5
      web/internal-web-core-runtime/src/jsMain/kotlin/org/jetbrains/compose/web/internal/runtime/JsMicrotasksDispatcher.kt
  43. 2
      web/settings.gradle.kts
  44. 42
      web/test-utils/build.gradle.kts
  45. 4
      web/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/ComposeWebExperimentalTestsApi.kt
  46. 66
      web/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/TestUtils.kt
  47. 82
      web/test-utils/src/jsTest/kotlin/TestsForTestUtils.kt

76
tutorials/Image_And_Icons_Manipulations/README.md

@ -19,7 +19,7 @@ import androidx.compose.ui.window.singleWindowApplication
fun main() = singleWindowApplication { fun main() = singleWindowApplication {
Image( Image(
painter = painterResource("sample.png"), // ImageBitmap painter = painterResource("sample.png"),
contentDescription = "Sample", contentDescription = "Sample",
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
@ -135,70 +135,66 @@ fun loadXmlImageVector(file: File, density: Density): ImageVector =
[XML vector drawable](../../artwork/compose-logo.xml) [XML vector drawable](../../artwork/compose-logo.xml)
## Drawing raw image data using native canvas ## Drawing images using Canvas
You may want to draw raw image data, in which case you can use `Canvas` and` drawIntoCanvas`.
```kotlin ```kotlin
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset 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.Paint
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 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.loadImageBitmap
import androidx.compose.ui.res.loadSvgPainter
import androidx.compose.ui.res.loadXmlImageVector
import androidx.compose.ui.res.useResource import androidx.compose.ui.res.useResource
import androidx.compose.ui.window.singleWindowApplication import androidx.compose.ui.window.singleWindowApplication
import org.jetbrains.skija.Bitmap import org.xml.sax.InputSource
import org.jetbrains.skija.ColorAlphaType
import org.jetbrains.skija.ImageInfo
private val sample = useResource("sample.png", ::loadImageBitmap)
fun main() = singleWindowApplication { 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( Canvas(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
drawIntoCanvas { canvas -> 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
} }
} }
return pixels
} }
``` ```
<img alt="Drawing raw images" src="draw_image_into_canvas.png" height="496" /> [PNG](sample.png)
[SVG](../../artwork/idea-logo.svg)
[XML vector drawable](../../artwork/compose-logo.xml)
## Setting the application window icon ## Setting the application window icon

BIN
tutorials/Image_And_Icons_Manipulations/draw_image_into_canvas.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

8
web/core/build.gradle.kts

@ -30,12 +30,14 @@ kotlin {
val jsMain by getting { val jsMain by getting {
dependencies { dependencies {
implementation(project(":internal-web-core-runtime"))
implementation(kotlin("stdlib-js")) implementation(kotlin("stdlib-js"))
} }
} }
val jsTest by getting { val jsTest by getting {
dependencies { dependencies {
implementation(project(":test-utils"))
implementation(kotlin("test-js")) implementation(kotlin("test-js"))
} }
} }
@ -45,5 +47,11 @@ kotlin {
implementation(compose.desktop.currentOs) 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.CoroutineScope
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.compose.web.internal.runtime.*
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.HTMLBodyElement import org.w3c.dom.HTMLBodyElement
import org.w3c.dom.get import org.w3c.dom.get
@ -22,6 +23,7 @@ import org.w3c.dom.get
* *
* @return the instance of the [Composition] * @return the instance of the [Composition]
*/ */
@OptIn(ComposeWebInternalApi::class)
fun <TElement : Element> renderComposable( fun <TElement : Element> renderComposable(
root: TElement, root: TElement,
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock, 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.INPUT
import org.jetbrains.compose.web.attributes.EventsListenerBuilder.Companion.SELECT import org.jetbrains.compose.web.attributes.EventsListenerBuilder.Companion.SELECT
import org.jetbrains.compose.web.events.* 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.DragEvent
import org.w3c.dom.TouchEvent import org.w3c.dom.TouchEvent
import org.w3c.dom.clipboard.ClipboardEvent import org.w3c.dom.clipboard.ClipboardEvent
import org.w3c.dom.events.* import org.w3c.dom.events.*
@OptIn(ComposeWebInternalApi::class)
open class SyntheticEventListener<T : SyntheticEvent<*>> internal constructor( open class SyntheticEventListener<T : SyntheticEvent<*>> internal constructor(
val event: String, val event: String,
val options: Options, val options: Options,
val listener: (T) -> Unit val listener: (T) -> Unit
) : EventListener { ) : EventListener, NamedEventListener {
override val name: String = event
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun handleEvent(event: Event) { override fun handleEvent(event: Event) {

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

@ -7,6 +7,9 @@
package org.jetbrains.compose.web.css 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 import kotlin.properties.ReadOnlyProperty
/** /**
@ -128,6 +131,7 @@ interface StyleHolder {
val variables: StylePropertyList val variables: StylePropertyList
} }
@OptIn(ComposeWebInternalApi::class)
@Suppress("EqualsOrHashCode") @Suppress("EqualsOrHashCode")
open class StyleBuilderImpl : StyleBuilder, StyleHolder { open class StyleBuilderImpl : StyleBuilder, StyleHolder {
override val properties: MutableStylePropertyList = mutableListOf() override val properties: MutableStylePropertyList = mutableListOf()

200
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, a2: Number, b2: Number, c2: Number, d2: Number,
a3: Number, b3: Number, c3: Number, d3: Number, a3: Number, b3: Number, c3: Number, d3: Number,
a4: Number, b4: Number, c4: Number, d4: Number a4: Number, b4: Number, c4: Number, d4: Number
): Boolean )
fun perspective(d: CSSLengthValue): Boolean fun perspective(d: CSSLengthValue)
fun rotate(a: CSSAngleValue): Boolean fun rotate(a: CSSAngleValue)
fun rotate3d(x: Number, y: Number, z: Number, a: CSSAngleValue): Boolean fun rotate3d(x: Number, y: Number, z: Number, a: CSSAngleValue)
fun rotateX(a: CSSAngleValue): Boolean fun rotateX(a: CSSAngleValue)
fun rotateY(a: CSSAngleValue): Boolean fun rotateY(a: CSSAngleValue)
fun rotateZ(a: CSSAngleValue): Boolean fun rotateZ(a: CSSAngleValue)
fun scale(sx: Number): Boolean fun scale(sx: Number)
fun scale(sx: Number, sy: Number): Boolean fun scale(sx: Number, sy: Number)
fun scale3d(sx: Number, sy: Number, sz: Number): Boolean fun scale3d(sx: Number, sy: Number, sz: Number)
fun scaleX(s: Number): Boolean fun scaleX(s: Number)
fun scaleY(s: Number): Boolean fun scaleY(s: Number)
fun scaleZ(s: Number): Boolean fun scaleZ(s: Number)
fun skew(ax: CSSAngleValue): Boolean fun skew(ax: CSSAngleValue)
fun skew(ax: CSSAngleValue, ay: CSSAngleValue): Boolean fun skew(ax: CSSAngleValue, ay: CSSAngleValue)
fun skewX(a: CSSAngleValue): Boolean fun skewX(a: CSSAngleValue)
fun skewY(a: CSSAngleValue): Boolean fun skewY(a: CSSAngleValue)
fun translate(tx: CSSLengthValue): Boolean fun translate(tx: CSSLengthValue)
fun translate(tx: CSSPercentageValue): Boolean fun translate(tx: CSSPercentageValue)
fun translate(tx: CSSLengthValue, ty: CSSLengthValue): Boolean fun translate(tx: CSSLengthValue, ty: CSSLengthValue)
fun translate(tx: CSSLengthValue, ty: CSSPercentageValue): Boolean fun translate(tx: CSSLengthValue, ty: CSSPercentageValue)
fun translate(tx: CSSPercentageValue, ty: CSSLengthValue): Boolean fun translate(tx: CSSPercentageValue, ty: CSSLengthValue)
fun translate(tx: CSSPercentageValue, ty: CSSPercentageValue): Boolean fun translate(tx: CSSPercentageValue, ty: CSSPercentageValue)
fun translate3d(tx: CSSLengthValue, ty: CSSLengthValue, tz: CSSLengthValue): Boolean fun translate3d(tx: CSSLengthValue, ty: CSSLengthValue, tz: CSSLengthValue)
fun translate3d(tx: CSSLengthValue, ty: CSSPercentageValue, tz: CSSLengthValue): Boolean fun translate3d(tx: CSSLengthValue, ty: CSSPercentageValue, tz: CSSLengthValue)
fun translate3d(tx: CSSPercentageValue, ty: CSSLengthValue, tz: CSSLengthValue): Boolean fun translate3d(tx: CSSPercentageValue, ty: CSSLengthValue, tz: CSSLengthValue)
fun translate3d(tx: CSSPercentageValue, ty: CSSPercentageValue, tz: CSSLengthValue): Boolean fun translate3d(tx: CSSPercentageValue, ty: CSSPercentageValue, tz: CSSLengthValue)
fun translateX(tx: CSSLengthValue): Boolean fun translateX(tx: CSSLengthValue)
fun translateX(tx: CSSPercentageValue): Boolean fun translateX(tx: CSSPercentageValue)
fun translateY(ty: CSSLengthValue): Boolean fun translateY(ty: CSSLengthValue)
fun translateY(ty: CSSPercentageValue): Boolean fun translateY(ty: CSSPercentageValue)
fun translateZ(tz: CSSLengthValue): Boolean fun translateZ(tz: CSSLengthValue)
} }
private class TransformBuilderImplementation : TransformBuilder { private class TransformBuilderImplementation : TransformBuilder {
@ -64,67 +64,133 @@ private class TransformBuilderImplementation : TransformBuilder {
a2: Number, b2: Number, c2: Number, d2: Number, a2: Number, b2: Number, c2: Number, d2: Number,
a3: Number, b3: Number, c3: Number, d3: Number, a3: Number, b3: Number, c3: Number, d3: Number,
a4: Number, b4: Number, c4: Number, d4: 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)" } 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)" }) 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 rotateZ(a: CSSAngleValue) {
override fun rotateY(a: CSSAngleValue) = transformations.add { "rotateY($a)" } 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) {
override fun scale(sx: Number, sy: Number) = transformations.add { "scale($sx, $sy)" } transformations.add { "scale($sx, $sy)" }
override fun scale3d(sx: Number, sy: Number, sz: Number) = }
override fun scale3d(sx: Number, sy: Number, sz: Number) {
transformations.add { "scale3d($sx, $sy, $sz)" } transformations.add { "scale3d($sx, $sy, $sz)" }
}
override fun scaleX(s: Number) = transformations.add { "scaleX($s)" } override fun scaleX(s: Number) {
override fun scaleY(s: Number) = transformations.add { "scaleY($s)" } transformations.add { "scaleX($s)" }
override fun scaleZ(s: Number) = transformations.add { "scaleZ($s)" } }
override fun skew(ax: CSSAngleValue) = transformations.add { "skew($ax)" } override fun scaleY(s: Number) {
override fun skew(ax: CSSAngleValue, ay: CSSAngleValue) = transformations.add { "skew($ax, $ay)" } transformations.add { "scaleY($s)" }
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 scaleZ(s: Number) {
override fun translate(tx: CSSPercentageValue) = transformations.add { "translate($tx)" } 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)" } transformations.add { "translate($tx, $ty)" }
}
override fun translate(tx: CSSLengthValue, ty: CSSPercentageValue) = override fun translate(tx: CSSLengthValue, ty: CSSPercentageValue) {
transformations.add { "translate($tx, $ty)" } transformations.add { "translate($tx, $ty)" }
}
override fun translate(tx: CSSPercentageValue, ty: CSSLengthValue) = override fun translate(tx: CSSPercentageValue, ty: CSSLengthValue) {
transformations.add { "translate($tx, $ty)" } transformations.add { "translate($tx, $ty)" }
}
override fun translate(tx: CSSPercentageValue, ty: CSSPercentageValue) = override fun translate(tx: CSSPercentageValue, ty: CSSPercentageValue) {
transformations.add { "translate($tx, $ty)" } 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)" } 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)" } 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)" } 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)" } transformations.add { "translate3d($tx, $ty, $tz)" }
}
override fun translateX(tx: CSSLengthValue) = transformations.add { "translateX($tx)" } override fun translateX(tx: CSSLengthValue) {
override fun translateX(tx: CSSPercentageValue) = transformations.add { "translateX($tx)" } transformations.add { "translateX($tx)" }
}
override fun translateY(ty: CSSLengthValue) = transformations.add { "translateY($ty)" } override fun translateX(tx: CSSPercentageValue) {
override fun translateY(ty: CSSPercentageValue) = transformations.add { "translateY($ty)" } 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 { override fun toString(): String {
return transformations.joinToString(" ") { it.apply() } return transformations.joinToString(" ") { it.apply() }
@ -132,7 +198,7 @@ private class TransformBuilderImplementation : TransformBuilder {
} }
@ExperimentalComposeWebApi @ExperimentalComposeWebApi
fun StyleBuilder.transform(transformFunction: TransformBuilder.() -> Unit) { fun StyleBuilder.transform(transformContext: TransformBuilder.() -> Unit) {
val transformBuilder = TransformBuilderImplementation() val transformBuilder = TransformBuilderImplementation()
property("transform", transformBuilder.apply(transformFunction).toString()) property("transform", transformBuilder.apply(transformContext).toString())
} }

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

@ -1,32 +1,25 @@
package org.jetbrains.compose.web.dom package org.jetbrains.compose.web.dom
import androidx.compose.runtime.Applier import androidx.compose.runtime.*
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 org.jetbrains.compose.web.attributes.AttrsBuilder import org.jetbrains.compose.web.attributes.AttrsBuilder
import org.jetbrains.compose.web.ExperimentalComposeWebApi 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.Element
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import org.w3c.dom.css.ElementCSSInlineStyle
import org.w3c.dom.svg.SVGElement
@OptIn(ComposeCompilerApi::class) @OptIn(ComposeCompilerApi::class)
@Composable @Composable
@ExplicitGroupsComposable @ExplicitGroupsComposable
inline fun <TScope, T, reified E : Applier<*>> ComposeDomNode( private inline fun <TScope, T> ComposeDomNode(
noinline factory: () -> T, noinline factory: () -> T,
elementScope: TScope, elementScope: TScope,
noinline attrsSkippableUpdate: @Composable SkippableUpdater<T>.() -> Unit, noinline attrsSkippableUpdate: @Composable SkippableUpdater<T>.() -> Unit,
noinline content: (@Composable TScope.() -> Unit)? noinline content: (@Composable TScope.() -> Unit)?
) { ) {
if (currentComposer.applier !is E) error("Invalid applier")
currentComposer.startNode() currentComposer.startNode()
if (currentComposer.inserting) { if (currentComposer.inserting) {
currentComposer.createNode(factory) currentComposer.createNode(factory)
@ -34,9 +27,7 @@ inline fun <TScope, T, reified E : Applier<*>> ComposeDomNode(
currentComposer.useNode() currentComposer.useNode()
} }
SkippableUpdater<T>(currentComposer).apply { attrsSkippableUpdate.invoke(SkippableUpdater(currentComposer))
attrsSkippableUpdate()
}
currentComposer.startReplaceableGroup(0x7ab4aae9) currentComposer.startReplaceableGroup(0x7ab4aae9)
content?.invoke(elementScope) content?.invoke(elementScope)
@ -44,10 +35,49 @@ inline fun <TScope, T, reified E : Applier<*>> ComposeDomNode(
currentComposer.endNode() currentComposer.endNode()
} }
class DisposableEffectHolder<TElement : Element>(
var effect: (DisposableEffectScope.(TElement) -> DisposableEffectResult)? = null
)
@OptIn(ComposeWebInternalApi::class)
private fun DomElementWrapper.updateProperties(applicators: List<Pair<(Element, Any) -> 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<ElementCSSInlineStyle>().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<String, String>) {
node.getAttributeNames().forEach { name ->
if (name == "style") return@forEach
node.removeAttribute(name)
}
attrs.forEach {
node.setAttribute(it.key, it.value)
}
}
@OptIn(ComposeWebInternalApi::class)
@Composable @Composable
fun <TElement : Element> TagElement( fun <TElement : Element> TagElement(
elementBuilder: ElementBuilder<TElement>, elementBuilder: ElementBuilder<TElement>,
@ -55,37 +85,35 @@ fun <TElement : Element> TagElement(
content: (@Composable ElementScope<TElement>.() -> Unit)? content: (@Composable ElementScope<TElement>.() -> Unit)?
) { ) {
val scope = remember { ElementScopeImpl<TElement>() } val scope = remember { ElementScopeImpl<TElement>() }
val refEffect = remember { DisposableEffectHolder<TElement>() } var refEffect: (DisposableEffectScope.(TElement) -> DisposableEffectResult)? = null
ComposeDomNode<ElementScope<TElement>, DomElementWrapper, DomApplier>( ComposeDomNode<ElementScope<TElement>, DomElementWrapper>(
factory = { factory = {
DomElementWrapper(elementBuilder.create() as HTMLElement).also { val node = elementBuilder.create()
scope.element = it.node.unsafeCast<TElement>() scope.element = node
} DomElementWrapper(node)
}, },
attrsSkippableUpdate = { attrsSkippableUpdate = {
val attrsApplied = AttrsBuilder<TElement>().also { val attrsBuilder = AttrsBuilder<TElement>()
if (applyAttrs != null) { applyAttrs?.invoke(attrsBuilder)
it.applyAttrs()
} refEffect = attrsBuilder.refEffect
}
refEffect.effect = attrsApplied.refEffect
val attrsCollected = attrsApplied.collect()
val events = attrsApplied.collectListeners()
update { update {
set(attrsCollected, DomElementWrapper::updateAttrs) set(attrsBuilder.collect(), DomElementWrapper::updateAttrs)
set(events, DomElementWrapper::updateEventListeners) set(attrsBuilder.collectListeners(), DomElementWrapper::updateEventListeners)
set(attrsApplied.propertyUpdates, DomElementWrapper::updateProperties) set(attrsBuilder.propertyUpdates, DomElementWrapper::updateProperties)
set(attrsApplied.styleBuilder, DomElementWrapper::updateStyleDeclarations) set(attrsBuilder.styleBuilder, DomElementWrapper::updateStyleDeclarations)
} }
}, },
elementScope = scope, elementScope = scope,
content = content content = content
) )
refEffect?.let { effect ->
DisposableEffect(null) { DisposableEffect(null) {
refEffect.effect?.invoke(this, scope.element) ?: onDispose {} effect.invoke(this, scope.element)
}
} }
} }

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

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

@ -12,6 +12,7 @@ import org.w3c.dom.HTMLElement
import org.w3c.dom.get import org.w3c.dom.get
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
object AppCSSVariables { object AppCSSVariables {
val width by variable<CSSUnitValue>() val width by variable<CSSUnitValue>()

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

@ -11,6 +11,7 @@ import org.w3c.dom.HTMLElement
import org.w3c.dom.get import org.w3c.dom.get
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
class CSSUnitApiTests { class CSSUnitApiTests {
// TODO: Cover CSS.Q, CSS.khz and CSS.hz after we'll get rid from polyfill // TODO: Cover CSS.Q, CSS.khz and CSS.hz after we'll get rid from polyfill

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.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
import org.jetbrains.compose.web.testutils.*
class ControlledRadioGroupsTests { 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 org.jetbrains.compose.web.renderComposable
import kotlinx.browser.document import kotlinx.browser.document
import kotlinx.dom.clear import kotlinx.dom.clear
import org.jetbrains.compose.web.testutils.*
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals

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

@ -5,9 +5,7 @@
package org.jetbrains.compose.web.core.tests package org.jetbrains.compose.web.core.tests
import androidx.compose.runtime.Composable
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
class FailingTestCases { 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 org.jetbrains.compose.web.dom.Text
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
class InlineStyleTests { class InlineStyleTests {

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

@ -10,6 +10,7 @@ import org.w3c.dom.get
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
import org.jetbrains.compose.web.testutils.*
class StaticComposableTests { class StaticComposableTests {
@Test @Test
@ -184,12 +185,10 @@ class StaticComposableTests {
} }
@Test @Test
fun stylesPosition() { fun stylesPosition() = runTest {
val root = "div".asHtmlElement()
val enumValues = Position.values() val enumValues = Position.values()
renderComposable(
root = root composition {
) {
enumValues.forEach { position -> enumValues.forEach { position ->
Span( Span(
{ {
@ -197,14 +196,14 @@ class StaticComposableTests {
position(position) position(position)
} }
} }
) { } )
} }
} }
enumValues.forEachIndexed { index, position -> enumValues.forEachIndexed { index, position ->
assertEquals( assertEquals(
"position: ${position.value};", "position: ${position.value};",
(root.children[index] as HTMLElement).style.cssText nextChild().style.cssText
) )
} }
} }

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

@ -6,13 +6,11 @@
package org.jetbrains.compose.web.core.tests.css package org.jetbrains.compose.web.core.tests.css
import kotlinx.browser.window import kotlinx.browser.window
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.css.* import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
class CSSBackgroundTests { class CSSBackgroundTests {
@Test @Test

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

@ -5,7 +5,7 @@
package org.jetbrains.compose.web.core.tests.css 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.css.*
import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Div
import kotlin.test.Test import kotlin.test.Test

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

@ -5,11 +5,9 @@
package org.jetbrains.compose.web.core.tests.css package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.core.tests.asHtmlElement import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.css.* import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.renderComposable
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import org.w3c.dom.get import org.w3c.dom.get
import kotlin.test.Test import kotlin.test.Test

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

@ -5,14 +5,12 @@
package org.jetbrains.compose.web.core.tests.css 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.core.tests.values
import org.jetbrains.compose.web.css.DisplayStyle import org.jetbrains.compose.web.css.DisplayStyle
import org.jetbrains.compose.web.css.display import org.jetbrains.compose.web.css.display
import org.jetbrains.compose.web.css.value import org.jetbrains.compose.web.css.value
import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals

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

@ -5,7 +5,7 @@
package org.jetbrains.compose.web.core.tests.css 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.core.tests.values
import org.jetbrains.compose.web.css.* import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Div

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

@ -5,7 +5,7 @@
package org.jetbrains.compose.web.core.tests.css 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.css.*
import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement

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

@ -6,11 +6,9 @@
package org.jetbrains.compose.web.core.tests.css package org.jetbrains.compose.web.core.tests.css
import kotlinx.browser.window 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.css.*
import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals

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

@ -5,11 +5,9 @@
package org.jetbrains.compose.web.core.tests.css 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.css.*
import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals

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

@ -6,11 +6,9 @@
package org.jetbrains.compose.web.core.tests.css package org.jetbrains.compose.web.core.tests.css
import kotlinx.browser.window 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.css.*
import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals

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

@ -5,12 +5,9 @@
package org.jetbrains.compose.web.core.tests.css package org.jetbrains.compose.web.core.tests.css
import kotlinx.browser.window import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.css.* import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals

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

@ -5,11 +5,9 @@
package org.jetbrains.compose.web.core.tests.css 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.css.cursor
import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals

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

@ -5,7 +5,7 @@
package org.jetbrains.compose.web.core.tests.css 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.css.*
import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement

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

@ -6,7 +6,7 @@
package org.jetbrains.compose.web.core.tests.css package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.ExperimentalComposeWebApi 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.css.*
import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Img import org.jetbrains.compose.web.dom.Img

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

@ -5,11 +5,9 @@
package org.jetbrains.compose.web.core.tests.css package org.jetbrains.compose.web.core.tests.css
import androidx.compose.runtime.compositionLocalOf import org.jetbrains.compose.web.testutils.*
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.css.* import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Div
import kotlin.js.Json
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals

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

@ -5,7 +5,7 @@
package org.jetbrains.compose.web.core.tests.css package org.jetbrains.compose.web.core.tests.css
import org.jetbrains.compose.web.ExperimentalComposeWebApi 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.css.*
import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Div
import kotlin.test.Test import kotlin.test.Test

55
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.mutableStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue 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.AttrsBuilder
import org.jetbrains.compose.web.attributes.disabled import org.jetbrains.compose.web.attributes.disabled
import org.jetbrains.compose.web.attributes.forId import org.jetbrains.compose.web.attributes.forId
@ -17,6 +19,7 @@ import org.w3c.dom.HTMLDivElement
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
class AttributesTests { class AttributesTests {
@ -333,26 +336,72 @@ class AttributesTests {
assertEquals(0, refDisposeCounter) 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 = "<div><div id=\"id0\"><p>Key=0</p></div></div>",
actual = root.outerHTML
)
key = 1
waitForRecompositionComplete()
assertEquals(
expected = "<div><div id=\"id1\"><p>Key=1</p></div></div>",
actual = root.outerHTML
)
}
@Test // issue: https://github.com/JetBrains/compose-jb/issues/981 @Test // issue: https://github.com/JetBrains/compose-jb/issues/981
fun attributesUpdateShouldNotCauseInlineStylesCleanUp() = runTest { fun attributesUpdateShouldNotCauseInlineStylesCleanUp() = runTest {
var hasValue by mutableStateOf(false) var hasValue by mutableStateOf(false)
composition { composition {
Button(attrs = { Button(attrs = {
classes("a")
style { style {
color(Color.red) color(Color.red)
} }
if (hasValue) value("buttonValue") if (hasValue) {
classes("b")
value("buttonValue")
}
}) { }) {
Text("Button") Text("Button")
} }
} }
assertEquals("""<button style="color: red;">Button</button>""", root.innerHTML) assertEquals(
expected = "<button class=\"a\" style=\"color: red;\">Button</button>",
actual = nextChild().outerHTML
)
hasValue = true hasValue = true
waitForRecompositionComplete() waitForRecompositionComplete()
assertEquals("""<button style="color: red;" value="buttonValue">Button</button>""", root.innerHTML) assertEquals(
expected = "<button style=\"color: red;\" value=\"buttonValue\" class=\"a b\">Button</button>",
actual = currentChild().outerHTML
)
} }
} }

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

@ -6,9 +6,11 @@
package org.jetbrains.compose.web.core.tests.elements package org.jetbrains.compose.web.core.tests.elements
import androidx.compose.runtime.Composable 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.ExperimentalComposeWebApi
import org.jetbrains.compose.web.attributes.AttrsBuilder 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.jetbrains.compose.web.dom.*
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import org.w3c.dom.get import org.w3c.dom.get
@ -131,4 +133,30 @@ class ElementsTests {
assertEquals("<div><custom id=\"container\">CUSTOM</custom></div>", root.outerHTML) assertEquals("<div><custom id=\"container\">CUSTOM</custom></div>", 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("<div><div>ON</div></div>", nextChild().outerHTML)
}
} }

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.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
import org.jetbrains.compose.web.testutils.*
class EventTests { 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.setValue
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import org.jetbrains.compose.web.attributes.* import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.core.tests.asHtmlElement
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.dom.* import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.renderComposable import org.jetbrains.compose.web.renderComposable
import org.w3c.dom.HTMLInputElement import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLTextAreaElement import org.w3c.dom.HTMLTextAreaElement
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
class InputsGenerateCorrectHtmlTests { 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 org.w3c.dom.HTMLElement
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import org.jetbrains.compose.web.testutils.*
class TableTests { class TableTests {

4
web/gradle.properties

@ -1,6 +1,6 @@
# __LATEST_COMPOSE_RELEASE_VERSION__ # __LATEST_COMPOSE_RELEASE_VERSION__
COMPOSE_CORE_VERSION=0.0.0-master-build316 COMPOSE_CORE_VERSION=1.0.0-alpha4-build321
COMPOSE_WEB_VERSION=1.0.0-alpha1 COMPOSE_WEB_VERSION=1.0.0-alpha4
compose.web.buildSamples=false compose.web.buildSamples=false
compose.web.tests.integration.withFirefox compose.web.tests.integration.withFirefox
compose.web.tests.skip.benchmarks=false compose.web.tests.skip.benchmarks=false

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

71
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 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 kotlinx.dom.clear
import org.w3c.dom.* import org.w3c.dom.*
import org.w3c.dom.css.CSSStyleDeclaration
import org.w3c.dom.events.EventListener
internal class DomApplier( @ComposeWebInternalApi
class DomApplier(
root: DomNodeWrapper root: DomNodeWrapper
) : AbstractApplier<DomNodeWrapper>(root) { ) : AbstractApplier<DomNodeWrapper>(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<SyntheticEventListener<*>>()
fun updateEventListeners(list: List<SyntheticEventListener<*>>) {
val htmlElement = node as? HTMLElement ?: return
currentListeners.forEach { @ComposeWebInternalApi
htmlElement.removeEventListener(it.event, it) interface NamedEventListener : EventListener {
val name: String
} }
currentListeners = list @ComposeWebInternalApi
open class DomNodeWrapper(open val node: Node) {
currentListeners.forEach {
htmlElement.addEventListener(it.event, it)
}
}
fun insert(index: Int, nodeWrapper: DomNodeWrapper) { fun insert(index: Int, nodeWrapper: DomNodeWrapper) {
val length = node.childNodes.length 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<NamedEventListener>()
internal class DomElementWrapper(override val node: HTMLElement): DomNodeWrapper(node) { fun updateEventListeners(list: List<NamedEventListener>) {
private var currentAttrs: Map<String, String>? = null currentListeners.forEach {
node.removeEventListener(it.name, it)
fun updateAttrs(attrs: Map<String, String>) {
currentAttrs?.forEach {
node.removeAttribute(it.key)
}
attrs.forEach {
node.setAttribute(it.key, it.value)
}
currentAttrs = attrs
}
fun updateProperties(list: List<Pair<(Element, Any) -> Unit, Any>>) {
if (node.className.isNotEmpty()) node.className = ""
list.forEach {
it.first(node, it.second)
}
} }
fun updateStyleDeclarations(style: StyleHolder?) { currentListeners = list
node.removeAttribute("style")
style?.properties?.forEach { (name, value) -> currentListeners.forEach {
setProperty(node.style, name, value) node.addEventListener(it.name, it)
}
style?.variables?.forEach { (name, value) ->
setVariable(node.style, name, value)
} }
} }
} }

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.ObserverHandle
import androidx.compose.runtime.snapshots.Snapshot 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.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -16,7 +16,8 @@ import kotlinx.coroutines.launch
* Composition bootstrapping mechanisms for a particular platform/framework should call * Composition bootstrapping mechanisms for a particular platform/framework should call
* [ensureStarted] during setup to initialize periodic global snapshot notifications. * [ensureStarted] during setup to initialize periodic global snapshot notifications.
*/ */
internal object GlobalSnapshotManager { @ComposeWebInternalApi
object GlobalSnapshotManager {
private var started = false private var started = false
private var commitPending = false private var commitPending = false
private var removeWriteObserver: (ObserverHandle)? = null 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.CoroutineDispatcher
import kotlinx.coroutines.Runnable import kotlinx.coroutines.Runnable
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.js.Promise import kotlin.js.Promise
internal class JsMicrotasksDispatcher : CoroutineDispatcher() { @ComposeWebInternalApi
class JsMicrotasksDispatcher : CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) { override fun dispatch(context: CoroutineContext, block: Runnable) {
Promise.resolve(Unit).then { block.run() } Promise.resolve(Unit).then { block.run() }
} }

2
web/settings.gradle.kts

@ -37,6 +37,8 @@ fun module(name: String, path: String) {
module(":web-core", "core") module(":web-core", "core")
module(":web-widgets", "widgets") module(":web-widgets", "widgets")
module(":compose-compiler-integration", "compose-compiler-integration") 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) { if (extra["compose.web.tests.skip.benchmarks"]!!.toString().toBoolean() != true) {
module(":web-benchmark-core", "benchmark-core") 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

66
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.*
import androidx.compose.runtime.MonotonicFrameClock
import org.jetbrains.compose.web.renderComposable
import kotlinx.browser.document import kotlinx.browser.document
import kotlinx.browser.window import kotlinx.browser.window
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.promise
import kotlinx.dom.clear import kotlinx.dom.clear
import org.jetbrains.compose.web.internal.runtime.*
import org.w3c.dom.* import org.w3c.dom.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
@ -23,6 +20,7 @@ import kotlin.time.toDuration
* There is no need to create its instances manually. * There is no need to create its instances manually.
* @see [runTest] * @see [runTest]
*/ */
@ComposeWebExperimentalTestsApi
class TestScope : CoroutineScope by MainScope() { class TestScope : CoroutineScope by MainScope() {
/** /**
@ -31,7 +29,7 @@ class TestScope : CoroutineScope by MainScope() {
*/ */
val root = "div".asHtmlElement() val root = "div".asHtmlElement()
private val recompositionCompleteEventsChannel = Channel<Unit>() private var waitForRecompositionCompleteContinuation: Continuation<Unit>? = null
private val childrenIterator = root.children.asList().listIterator() private val childrenIterator = root.children.asList().listIterator()
init { init {
@ -39,26 +37,56 @@ class TestScope : CoroutineScope by MainScope() {
} }
private fun onRecompositionComplete() { private fun onRecompositionComplete() {
launch { waitForRecompositionCompleteContinuation?.resume(Unit)
recompositionCompleteEventsChannel.send(Unit) waitForRecompositionCompleteContinuation = null
}
} }
/** /**
* Cleans up the [root] content. * Cleans up the [root] content.
* Creates a new composition with a given Composable [content]. * Creates a new composition with a given Composable [content].
*/ */
@ComposeWebExperimentalTestsApi
fun composition(content: @Composable () -> Unit) { fun composition(content: @Composable () -> Unit) {
root.clear() root.clear()
renderComposable( renderTestComposable(root = root) {
root = root, content()
monotonicFrameClock = TestMonotonicClockImpl( }
}
/**
* 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 onRecomposeComplete = this::onRecompositionComplete
) + JsMicrotasksDispatcher()
val recomposer = Recomposer(context)
val composition = ControlledComposition(
applier = DomApplier(DomNodeWrapper(root)),
parent = recomposer
) )
) { composition.setContent @Composable {
content() content()
} }
CoroutineScope(context).launch(start = CoroutineStart.UNDISPATCHED) {
recomposer.runRecomposeAndApplyChanges()
}
return composition
} }
/** /**
@ -104,7 +132,9 @@ class TestScope : CoroutineScope by MainScope() {
* Suspends until recomposition completes. * Suspends until recomposition completes.
*/ */
suspend fun waitForRecompositionComplete() { 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 { fun runTest(block: suspend TestScope.() -> Unit): dynamic {
val scope = TestScope() val scope = TestScope()
return scope.promise { block(scope) } return scope.promise { block(scope) }
} }
@ComposeWebExperimentalTestsApi
fun String.asHtmlElement() = document.createElement(this) as HTMLElement fun String.asHtmlElement() = document.createElement(this) as HTMLElement
private object MutationObserverOptions : MutationObserverInit { 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