()
+ repeat(3) { request ->
+ val html = composeHtmlToString(key = "direct-effects") {
+ state.value = "$request"
+ val value = remember { ++remembered }
+ CompositionLocalProvider(local provides "local-$request") {
+ Div {
+ Text("${local.current}:${state.value}:$value")
+ DisposableEffect(Unit) {
+ effects.add("enter $request")
+ onDispose { effects.add("dispose $request") }
+ }
+ SideEffect { effects.add("side $request") }
+ LaunchedEffect(Unit) { launched = true }
+ }
+ }
+ }
+ assertEquals("local-$request:$request:${request + 1}
", html)
+ assertEquals("outside", state.value)
+ }
+ assertEquals((0..2).flatMap { listOf("enter $it", "side $it", "dispose $it") }, effects)
+ assertFalse(launched)
+ }
+
+ @Test
+ fun movableContentHasTheExistingOneShotRendererLimitation() {
+ val movable = movableContentOf { Span { Text("moved") } }
+ val content: @Composable () -> Unit = { Div { movable() }; P { Text("after") } }
+ // The cancelled SSR recomposer has never inserted deferred movable content.
+ assertEquals("after
", composeHtmlToString(content = content))
+ assertEquals("after
", composeHtmlToString(key = "movable", content = content))
+ }
+
+ @Test
+ fun rejectsInvalidHtmlAndRecoversAfterFailure() {
+ val invalid: List<@Composable () -> Unit> = listOf(
+ { Div { Text("bad\u0000text") } },
+ { Div({ attr("data-value", "bad\u0000attribute") }) },
+ { TagElement("script", null) { Text("") } },
+ { TagElement("style", null) { Span { Text("child") } } },
+ { Table { Tr { Td { Text("missing tbody") } } } },
+ )
+ for (content in invalid) {
+ assertFailsWith { composeHtmlToString(content = content) }
+ assertFailsWith { composeHtmlToString(key = "direct-failure", content = content) }
+ assertEquals("ok
", composeHtmlToString(key = "direct-failure") { Div { Text("ok") } })
+ }
+ }
+
+ @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) }
+ }
+ }
+ }
+ }
+ assertEquals("de
", composeHtmlToString(content = content))
+ repeat(2) {
+ assertEquals("de
", composeHtmlToString(key = "captured-locals", content = content))
+ }
+ }
+
+ @Test
+ fun nestedRenderRestoresContextAndSameKeyReentryIsRejected() {
+ assertEquals("outer<span>inner</span>tail
",
+ composeHtmlToString(key = "outer") {
+ Div {
+ Text("outer")
+ Text(composeHtmlToString(key = "inner") { Span { Text("inner") } })
+ Text("tail")
+ }
+ })
+ assertFailsWith {
+ composeHtmlToString(key = "reentrant") {
+ composeHtmlToString(key = "reentrant") { Text("bad") }
+ }
+ }
+ assertEquals("ok", composeHtmlToString(key = "reentrant") { Text("ok") })
+ }
+
+}