recovered
", composeHtmlToString(key = "failed-composition") { P { Text("recovered") } }) + } + + @Test + fun failedSideEffectStillDisposesEveryObserver() { + val events = mutableListOfbefore
after
", + html, + ) + assertEquals(1, creations) + assertEquals(1, bodyExecutions) + } + } + } + + @Test + fun rememberingMovableContentWithoutInvokingItDoesNotFail() { + var creations = 0 + val html = composeHtmlToString { + remember { + creations++ + movableContentOf { error("unused movable content must not execute") } + } + Div { Text("ordinary content") } + } + assertEquals("recovered
", composeHtmlToString(key = "failed-movable") { + P { Text("recovered") } + }) + } + + @Test + fun movableContentHashesIgnoreParentsAndRestoreOuterHash() { + val hashes = mutableListOfafter
", composeHtmlToString(content = content)) - assertEquals("after
", composeHtmlToString(key = "movable", content = content)) + assertEquals("after
", composeHtmlToString(content = content)) + assertEquals("after
", composeHtmlToString(key = "movable", content = content)) } @Test @@ -135,22 +134,15 @@ class DirectStringRenderingTest { } @Test - fun capturedCompositionLocalsAndCompositionContextsRemainSupported() { - val language = staticCompositionLocalOf { "en" } - val content: @Composable () -> Unit = { - CompositionLocalProvider(language provides "de") { - val captured = currentCompositionLocalContext - rememberCompositionContext() - CompositionLocalProvider(language provides "fr") { - CompositionLocalProvider(captured) { - P { Text(language.current) } - } - } + fun compositionContextCaptureFailsExplicitly() { + for (renderKey in listOf(null, "captured-locals")) { + assertFailsWithde
", composeHtmlToString(content = content)) - repeat(2) { - assertEquals("de
", composeHtmlToString(key = "captured-locals", content = content)) + assertFailsWithrecovered
", composeHtmlToString(key = renderKey) { P { Text("recovered") } }) } } diff --git a/html/eap/core/src/jvmMain/kotlin/org/jetbrains/compose/web/ComposeHtmlToStream.kt b/html/eap/core/src/jvmMain/kotlin/org/jetbrains/compose/web/ComposeHtmlToStream.kt index 0088d60a64..5e02f64867 100644 --- a/html/eap/core/src/jvmMain/kotlin/org/jetbrains/compose/web/ComposeHtmlToStream.kt +++ b/html/eap/core/src/jvmMain/kotlin/org/jetbrains/compose/web/ComposeHtmlToStream.kt @@ -19,9 +19,9 @@ import java.io.Writer * intact. Elements requiring content validation are emitted only after validation succeeds. * A write can exceed [chunkSize]. No empty writes are made. * - * Composition reuse, effects, validation, and failure handling follow [composeHtmlToStream] - * with a String sink. Keyed renders are not thread-safe. Output already written cannot be - * retracted on failure, and a failed keyed composition is disposed. + * Storage reuse, effects, validation, and failure handling follow [composeHtmlToStream] + * with a String sink. Keyed renders are not thread-safe, even with different keys. + * Output already written cannot be retracted on failure, and failed rendering storage is reset. */ fun composeHtmlToStream( writer: Writer, @@ -38,11 +38,9 @@ fun composeHtmlToStream( content() } } - if (key == null) { - composeHtmlString(hydratable = hydratable, chunkSize = chunkSize, chunkSink = chunkSink, content = validatedContent) - } else { - composeReusableHtmlString(key, hydratable, chunkSize = chunkSize, chunkSink = chunkSink, content = validatedContent) - } + composeHtmlString( + hydratable = hydratable, key = key, chunkSink = chunkSink, chunkSize = chunkSize, content = validatedContent, + ) } /** Creates a Writer sink that reuses character storage across chunks within one render. */ diff --git a/html/eap/core/src/jvmTest/kotlin/org/jetbrains/compose/web/dom/ComposeHtmlToWriterTest.kt b/html/eap/core/src/jvmTest/kotlin/org/jetbrains/compose/web/dom/ComposeHtmlToWriterTest.kt index 6d9f82668e..0b5481091a 100644 --- a/html/eap/core/src/jvmTest/kotlin/org/jetbrains/compose/web/dom/ComposeHtmlToWriterTest.kt +++ b/html/eap/core/src/jvmTest/kotlin/org/jetbrains/compose/web/dom/ComposeHtmlToWriterTest.kt @@ -11,8 +11,10 @@ import java.io.IOException import java.io.OutputStreamWriter import java.io.Writer import kotlinx.browser.dom.Element +import org.jetbrains.compose.web.HtmlRenderer import org.jetbrains.compose.web.composeHtmlToStream import org.jetbrains.compose.web.composeHtmlToString +import org.jetbrains.compose.web.htmlWriterSink import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -146,28 +148,26 @@ class ComposeHtmlToWriterTest { @Test fun shrinksOldLargeBuffersAfterSmallRendersButReusesRepeatedLargeBuffers() { - val key = "writer-buffer-capacity" + val renderer = HtmlRenderer() + fun render(text: String, writer: Writer? = null): String = renderer.render( + hydratable = false, + requireHtmlDocumentRoot = false, + chunkSink = writer?.let(::htmlWriterSink), + chunkSize = 2048, + prefix = "", + ) { Text(text) } val large = "α".repeat(1024 * 1024) - composeHtmlToString(key = key) { Text(large) } - val output = keyedOutput(key) - val largeCapacity = output.capacity() + render(large) + val largeCapacity = renderer.bufferCapacity assertTrue(largeCapacity >= large.length) - composeHtmlToString(key = key) { Text(large) } - assertEquals(largeCapacity, output.capacity()) - assertEquals("small", composeHtmlToString(key = key) { Text("small") }) - assertTrue(output.capacity() <= 65536) + render(large) + assertEquals(largeCapacity, renderer.bufferCapacity) + assertEquals("small", render("small")) + assertTrue(renderer.bufferCapacity <= 65536) - composeHtmlToString(key = key) { Text(large) } - assertTrue(output.capacity() >= large.length) - composeHtmlToStream(RecordingWriter(), key = key) { Text("small") } - assertTrue(output.capacity() <= 65536) - } - - private fun keyedOutput(key: String): StringBuilder { - val field = Class.forName("org.jetbrains.compose.web.ComposeHtmlToStringKt") - .getDeclaredField("reusableHtmlCompositions").apply { isAccessible = true } - val renderer = (field.get(null) as Map<*, *>)[key]!! - val output = renderer.javaClass.getDeclaredField("output").apply { isAccessible = true } - return output.get(renderer) as StringBuilder + render(large) + assertTrue(renderer.bufferCapacity >= large.length) + render("small", RecordingWriter()) + assertTrue(renderer.bufferCapacity <= 65536) } }