diff --git a/html/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/TestUtils.kt b/html/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/TestUtils.kt
index 38440fcf5bb..4d8fea0c08e 100644
--- a/html/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/TestUtils.kt
+++ b/html/test-utils/src/jsMain/kotlin/org/jetbrains/compose/web/testutils/TestUtils.kt
@@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.MonotonicFrameClock
import kotlinx.browser.document
import kotlinx.browser.window
+import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.promise
@@ -84,12 +85,16 @@ class TestScope : CoroutineScope by MainScope() {
* Suspends until [element] observes any change to its html.
*/
suspend fun waitForChanges(element: HTMLElement = root) {
- suspendCoroutine { continuation ->
+ suspendCancellableCoroutine { continuation ->
val observer = MutationObserver { _, observer ->
continuation.resume(Unit)
observer.disconnect()
}
observer.observe(element, MutationObserverOptions)
+
+ continuation.invokeOnCancellation {
+ observer.disconnect()
+ }
}
}
@@ -97,8 +102,14 @@ class TestScope : CoroutineScope by MainScope() {
* Suspends until recomposition completes.
*/
suspend fun waitForRecompositionComplete() {
- suspendCoroutine { continuation ->
+ suspendCancellableCoroutine { continuation ->
waitForRecompositionCompleteContinuation = continuation
+
+ continuation.invokeOnCancellation {
+ if (waitForRecompositionCompleteContinuation === continuation) {
+ waitForRecompositionCompleteContinuation = null
+ }
+ }
}
}
}
diff --git a/html/test-utils/src/jsTest/kotlin/TestsForTestUtils.kt b/html/test-utils/src/jsTest/kotlin/TestsForTestUtils.kt
index a8c1cd50a73..b3b27ae8a89 100644
--- a/html/test-utils/src/jsTest/kotlin/TestsForTestUtils.kt
+++ b/html/test-utils/src/jsTest/kotlin/TestsForTestUtils.kt
@@ -3,6 +3,8 @@ import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.currentRecomposeScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.withTimeout
import org.jetbrains.compose.web.testutils.ComposeWebExperimentalTestsApi
import org.jetbrains.compose.web.testutils.runTest
import kotlin.test.Test
@@ -79,4 +81,54 @@ class TestsForTestUtils {
assertEquals(true, waitForChangesContinued)
assertEquals("Hello World!
", root.outerHTML)
}
+
+ @Test
+ fun waitForChanges_cancels_with_timeout() = runTest {
+
+ var cancelled = false
+
+ val job = launch {
+ try {
+ withTimeout(1000) {
+ waitForChanges(root)
+ }
+ } catch (t: TimeoutCancellationException) {
+ cancelled = true
+ throw t
+ }
+ }
+
+ delay(100) // to check that `waitForChanges` is suspended after delay
+ assertEquals(false, cancelled)
+
+ delay(1000) // to check that `waitForChanges` is cancelled after timeout
+ assertEquals(true, cancelled)
+
+ job.join()
+ }
+
+ @Test
+ fun waitForRecompositionComplete_cancels_with_timeout() = runTest {
+
+ var cancelled = false
+
+ val job = launch {
+ try {
+ withTimeout(1000) {
+ waitForRecompositionComplete()
+ }
+ } catch (t: TimeoutCancellationException) {
+ cancelled = true
+ throw t
+ }
+ }
+
+ delay(100) // to check that `waitForRecompositionComplete` is suspended after delay
+ assertEquals(false, cancelled)
+
+ delay(1000) // to check that `waitForRecompositionComplete` is cancelled after timeout
+ assertEquals(true, cancelled)
+
+ job.join()
+ }
}