From 9f15599add1ee327647cc4eefab46fe529935e29 Mon Sep 17 00:00:00 2001 From: Adit lal Date: Wed, 8 Jul 2026 12:37:20 +0530 Subject: [PATCH 1/3] test(verify-claims): broaden Compose reference coverage --- buildSrc/src/main/kotlin/ClaimParser.kt | 20 +++++++++++----- .../src/main/kotlin/GenerateClaimTests.kt | 23 +++++++++++++++---- buildSrc/src/test/kotlin/ClaimParserTest.kt | 21 ++++++++++++++++- .../references/accessibility.md | 12 ++++++++++ skills/compose-expert/references/animation.md | 7 +++++- .../references/composition-locals.md | 6 +++-- .../references/lists-scrolling.md | 15 ++++++++++++ .../references/state-management.md | 7 ++++-- .../references/theming-material3.md | 3 ++- 9 files changed, 97 insertions(+), 17 deletions(-) diff --git a/buildSrc/src/main/kotlin/ClaimParser.kt b/buildSrc/src/main/kotlin/ClaimParser.kt index de18d05..e629f0c 100644 --- a/buildSrc/src/main/kotlin/ClaimParser.kt +++ b/buildSrc/src/main/kotlin/ClaimParser.kt @@ -1,4 +1,4 @@ -data class Assertion(val prop: String, val dp: Int) +data class Assertion(val prop: String, val value: String) data class Claim(val name: String, val subjectSource: String, val asserts: List) data class CompileBlock(val name: String, val subjectSource: String) @@ -7,8 +7,9 @@ object ClaimParser { private val compileFenceOpen = Regex("""^```kotlin\s+compile\s*$""") private val fenceClose = Regex("""^```\s*$""") private val nameLine = Regex("""^//\s*name:\s*([a-z0-9-]+)\s*$""") - private val assertLine = Regex("""^//\s*assert:\s*(width|height)\s*=\s*(\d+)\.dp\s*$""") - private val allowedProps = setOf("width", "height") + private val dpAssertLine = Regex("""^//\s*assert:\s*(width|height)\s*=\s*(\d+)\.dp\s*$""") + private val stringAssertLine = Regex("""^//\s*assert:\s*(text|has-click-action)\s*=\s*"([^"]+)"\s*$""") + private val allowedProps = setOf("width", "height", "text", "has-click-action") fun parse(markdown: String, sourceFile: String): List { val lines = markdown.lines() @@ -63,14 +64,21 @@ object ClaimParser { if (!seenNames.add(name)) err("duplicate name '$name'") val asserts = body.mapNotNull { line -> - assertLine.matchEntire(line.trim())?.let { m -> Assertion(m.groupValues[1], m.groupValues[2].toInt()) } + val trimmed = line.trim() + dpAssertLine.matchEntire(trimmed)?.let { m -> + Assertion(m.groupValues[1], m.groupValues[2]) + } ?: stringAssertLine.matchEntire(trimmed)?.let { m -> + Assertion(m.groupValues[1], m.groupValues[2]) + } + } + if (asserts.isEmpty()) { + err("no '// assert: = N.dp' or '// assert: = \"...\"' lines") } - if (asserts.isEmpty()) err("no '// assert: = N.dp' lines") asserts.forEach { if (it.prop !in allowedProps) err("unsupported assert prop '${it.prop}'") } val subject = body.filterNot { l -> val t = l.trim() - nameLine.matches(t) || assertLine.matches(t) + nameLine.matches(t) || dpAssertLine.matches(t) || stringAssertLine.matches(t) }.joinToString("\n").trim() if (!subject.contains(Regex("""@Composable\s+fun\s+Subject\s*\("""))) { err("no '@Composable fun Subject()' found") diff --git a/buildSrc/src/main/kotlin/GenerateClaimTests.kt b/buildSrc/src/main/kotlin/GenerateClaimTests.kt index 947068a..38ffa46 100644 --- a/buildSrc/src/main/kotlin/GenerateClaimTests.kt +++ b/buildSrc/src/main/kotlin/GenerateClaimTests.kt @@ -21,10 +21,7 @@ abstract class GenerateClaimTests : DefaultTask() { out.deleteRecursively(); out.mkdirs() val tests = claims.joinToString("\n\n") { c -> - val asserts = c.asserts.joinToString("\n") { a -> - val fn = if (a.prop == "width") "assertWidthIsEqualTo" else "assertHeightIsEqualTo" - " rule.onRoot().$fn(${a.dp}.dp)" - } + val asserts = c.asserts.joinToString("\n") { a -> emitAssertion(a) } """ @Test fun `${c.name}`() { rule.setContent { ClaimSubjects.`${c.name}`() } @@ -87,11 +84,27 @@ abstract class GenerateClaimTests : DefaultTask() { require(duplicate == null) { "duplicate kotlin $marker block name '$duplicate'" } } + private fun emitAssertion(assertion: Assertion): String = + when (assertion.prop) { + "width" -> " rule.onRoot().assertWidthIsEqualTo(${assertion.value}.dp)" + "height" -> " rule.onRoot().assertHeightIsEqualTo(${assertion.value}.dp)" + "text" -> " rule.onNodeWithText(\"${assertion.value.escapeKotlinString()}\").assertExists()" + "has-click-action" -> + " rule.onNodeWithText(\"${assertion.value.escapeKotlinString()}\").assertHasClickAction()" + else -> error("unsupported assertion prop '${assertion.prop}'") + } + + private fun String.escapeKotlinString(): String = + replace("\\", "\\\\").replace("\"", "\\\"") + private fun generatedImports(): String = """ import androidx.compose.foundation.background import androidx.compose.foundation.clickable + import androidx.compose.foundation.lazy.LazyColumn + import androidx.compose.foundation.lazy.items import androidx.compose.foundation.layout.* + import androidx.compose.animation.AnimatedVisibility import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier @@ -99,6 +112,8 @@ abstract class GenerateClaimTests : DefaultTask() { import androidx.compose.ui.unit.dp import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onRoot + import androidx.compose.ui.test.onNodeWithText + import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertWidthIsEqualTo import androidx.compose.ui.test.assertHeightIsEqualTo import org.junit.Rule diff --git a/buildSrc/src/test/kotlin/ClaimParserTest.kt b/buildSrc/src/test/kotlin/ClaimParserTest.kt index cd89a04..8c6c11e 100644 --- a/buildSrc/src/test/kotlin/ClaimParserTest.kt +++ b/buildSrc/src/test/kotlin/ClaimParserTest.kt @@ -26,10 +26,29 @@ class ClaimParserTest { assertEquals(1, claims.size) val c = claims.single() assertEquals("size-before-padding", c.name) - assertEquals(listOf("width" to 100, "height" to 100), c.asserts.map { it.prop to it.dp }) + assertEquals(listOf("width" to "100", "height" to "100"), c.asserts.map { it.prop to it.value }) assert(c.subjectSource.contains("fun Subject()")) } + @Test fun parsesTextAndClickActionAssertions() { + val md = """ + ```kotlin verify + // name: button-semantics + @Composable fun Subject() { + Button(onClick = {}) { Text("Save") } + } + // assert: text = "Save" + // assert: has-click-action = "Save" + ``` + """.trimIndent() + + val claim = ClaimParser.parse(md, "x.md").single() + assertEquals( + listOf("text" to "Save", "has-click-action" to "Save"), + claim.asserts.map { it.prop to it.value }, + ) + } + @Test fun rejectsMissingName() { val md = "```kotlin verify\n@Composable fun Subject() {}\n// assert: width = 1.dp\n```" val ex = assertThrows(IllegalStateException::class.java) { ClaimParser.parse(md, "x.md") } diff --git a/skills/compose-expert/references/accessibility.md b/skills/compose-expert/references/accessibility.md index 4d210ba..b8137b2 100644 --- a/skills/compose-expert/references/accessibility.md +++ b/skills/compose-expert/references/accessibility.md @@ -19,6 +19,18 @@ Box( } ``` +Executable smoke check for built-in button semantics: + +```kotlin verify +// name: button-text-exposes-click-action +// assert: text = "Click me" +// assert: has-click-action = "Click me" +@Composable +fun Subject() { + Button(onClick = { }) { Text("Click me") } +} +``` + **Source**: `androidx/compose/ui/semantics/` --- diff --git a/skills/compose-expert/references/animation.md b/skills/compose-expert/references/animation.md index 40a36ac..6ee4634 100644 --- a/skills/compose-expert/references/animation.md +++ b/skills/compose-expert/references/animation.md @@ -33,7 +33,11 @@ Each automatically handles coroutines and recomposition. Use the `label` paramet Controls appear/disappear animations with enter and exit transitions. -```kotlin +```kotlin verify +// name: animated-visibility-renders-visible-content +// assert: text = "Hello!" +@Composable +fun Subject() { var visible by remember { mutableStateOf(true) } AnimatedVisibility(visible = visible) { @@ -42,6 +46,7 @@ AnimatedVisibility(visible = visible) { // Trigger Button(onClick = { visible = !visible }) { Text("Toggle") } +} ``` ### Enter/Exit Transitions diff --git a/skills/compose-expert/references/composition-locals.md b/skills/compose-expert/references/composition-locals.md index 10df9aa..8f12242 100644 --- a/skills/compose-expert/references/composition-locals.md +++ b/skills/compose-expert/references/composition-locals.md @@ -6,11 +6,13 @@ CompositionLocals provide a way to pass data implicitly down the composition tre A CompositionLocal is a slot in the composition that holds a value accessible to any descendant composable without explicit parameter passing. Values are provided using `CompositionLocalProvider` and accessed via `current`. -```kotlin +```kotlin verify +// name: composition-local-provider-overrides-default +// assert: text = "Dark" val localAppTheme = compositionLocalOf { "Light" } @Composable -fun MyScreen() { +fun Subject() { CompositionLocalProvider(localAppTheme provides "Dark") { DescendantComposable() // Can access "Dark" via localAppTheme.current } diff --git a/skills/compose-expert/references/lists-scrolling.md b/skills/compose-expert/references/lists-scrolling.md index b5ea079..26110ec 100644 --- a/skills/compose-expert/references/lists-scrolling.md +++ b/skills/compose-expert/references/lists-scrolling.md @@ -67,6 +67,21 @@ LazyColumn { } ``` +Executable smoke check for the count overload: + +```kotlin verify +// name: lazy-column-count-renders-visible-items +// assert: text = "Item 0" +@Composable +fun Subject() { + LazyColumn { + items(3) { index -> + Text("Item $index") + } + } +} +``` + ### `itemsIndexed` — With Index ```kotlin LazyColumn { diff --git a/skills/compose-expert/references/state-management.md b/skills/compose-expert/references/state-management.md index 7389a5d..f9089d1 100644 --- a/skills/compose-expert/references/state-management.md +++ b/skills/compose-expert/references/state-management.md @@ -29,9 +29,12 @@ Both associate state with a composition key, but differ in persistence scope. - Lost on process death, configuration changes, back navigation - Best for UI state: selection, expanded/collapsed, scroll position -```kotlin +```kotlin verify +// name: remember-counter-renders-clickable-state +// assert: text = "Count: 0" +// assert: has-click-action = "Count: 0" @Composable -fun Counter() { +fun Subject() { var count by remember { mutableIntStateOf(0) } Button(onClick = { count++ }) { Text("Count: $count") diff --git a/skills/compose-expert/references/theming-material3.md b/skills/compose-expert/references/theming-material3.md index 16d2b43..36fed50 100644 --- a/skills/compose-expert/references/theming-material3.md +++ b/skills/compose-expert/references/theming-material3.md @@ -4,8 +4,9 @@ `MaterialTheme` is the root provider for design tokens in Compose Material 3. It establishes `colorScheme`, `typography`, and `shapes` across your app. -```kotlin compile +```kotlin verify // name: material-theme-provides-tokens +// assert: text = "Uses MaterialTheme.typography.bodyLarge" @Composable fun Subject() { MaterialTheme( From 3324a11cbd2853bf2fce9fc9710ff26a15a6a7c9 Mon Sep 17 00:00:00 2001 From: Adit lal Date: Wed, 8 Jul 2026 15:21:39 +0530 Subject: [PATCH 2/3] test(verify-claims): add broader Compose rules coverage --- skills/compose-expert/references/performance.md | 15 +++++++++++++++ skills/compose-expert/references/side-effects.md | 15 +++++++++++++++ .../compose-expert/references/view-composition.md | 13 +++++++++++++ 3 files changed, 43 insertions(+) diff --git a/skills/compose-expert/references/performance.md b/skills/compose-expert/references/performance.md index a697a8c..72fd8d9 100644 --- a/skills/compose-expert/references/performance.md +++ b/skills/compose-expert/references/performance.md @@ -57,6 +57,21 @@ A type is **stable** if: - Overrides to `equals()` and `hashCode()` are based on stable properties - Recomposition is skipped when the same instance is passed +Executable check for a stable model rendered through a composable: + +```kotlin verify +// name: stable-model-renders-text +// assert: text = "Ada" +@Immutable +data class Person(val name: String, val age: Int) + +@Composable +fun Subject() { + val person = Person("Ada", 42) + Text(person.name) +} +``` + Mark stable types explicitly: ```kotlin diff --git a/skills/compose-expert/references/side-effects.md b/skills/compose-expert/references/side-effects.md index b735bba..04617bd 100644 --- a/skills/compose-expert/references/side-effects.md +++ b/skills/compose-expert/references/side-effects.md @@ -86,6 +86,21 @@ Source: `compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ `LaunchedEffect` launches a coroutine in a scope tied to the composable's lifecycle. The coroutine is cancelled if the key changes or the composable leaves composition. +Executable check for the common "load then render" pattern: + +```kotlin verify +// name: launched-effect-updates-ui +// assert: text = "loaded" +@Composable +fun Subject() { + var data by remember { mutableStateOf("loading") } + LaunchedEffect(Unit) { + data = "loaded" + } + Text(data) +} +``` + ```kotlin @Composable fun DataLoader(userId: String) { diff --git a/skills/compose-expert/references/view-composition.md b/skills/compose-expert/references/view-composition.md index 0cb83ce..b94d7aa 100644 --- a/skills/compose-expert/references/view-composition.md +++ b/skills/compose-expert/references/view-composition.md @@ -109,6 +109,19 @@ ListItem( **Key principle:** Slots accept `@Composable` lambdas, not pre-composed values. This ensures composition is deferred and scope-aware. +Executable check for the slot pattern: + +```kotlin verify +// name: slot-pattern-renders-child-content +// assert: text = "Hello" +@Composable +fun Subject() { + Card { + Text("Hello") + } +} +``` + ```kotlin // ❌ Wrong: passes composed value fun CustomLayout(content: String) { ... } From 9e3e0845ad8f8cbdc36b32418252c26feb80dd71 Mon Sep 17 00:00:00 2001 From: Adit lal Date: Wed, 8 Jul 2026 15:31:36 +0530 Subject: [PATCH 3/3] test(verify-claims): add trust-factor repeats and negatives --- CONTRIBUTING.md | 6 ++++++ buildSrc/src/main/kotlin/ClaimParser.kt | 17 +++++++++++----- .../src/main/kotlin/GenerateClaimTests.kt | 20 ++++++++++++------- .../references/accessibility.md | 2 ++ skills/compose-expert/references/animation.md | 2 ++ .../references/composition-locals.md | 2 ++ .../references/lists-scrolling.md | 2 ++ skills/compose-expert/references/modifiers.md | 2 ++ .../compose-expert/references/performance.md | 2 ++ .../compose-expert/references/side-effects.md | 2 ++ .../references/state-management.md | 2 ++ .../references/theming-material3.md | 2 ++ .../references/view-composition.md | 2 ++ 13 files changed, 51 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8843ca8..ea24e7f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -82,4 +82,10 @@ contract: number fails CI; a missing or stale API in an opt-in compile block fails compilation. Plain ` ```kotlin ` blocks are illustrative and are not executed. +The executable claim format supports a few trust-oriented knobs: + +- `// repeat: N` runs the same claim `N` times to catch flaky or unstable rules. +- `// assert-not: text = "..."` checks that a string is absent from the rendered semantics tree. +- `// assert: has-click-action = "..."` checks clickable semantics on a node with matching text. + The `verify-claims` CI job runs this on every PR and master push. diff --git a/buildSrc/src/main/kotlin/ClaimParser.kt b/buildSrc/src/main/kotlin/ClaimParser.kt index e629f0c..43fb1bb 100644 --- a/buildSrc/src/main/kotlin/ClaimParser.kt +++ b/buildSrc/src/main/kotlin/ClaimParser.kt @@ -1,5 +1,5 @@ -data class Assertion(val prop: String, val value: String) -data class Claim(val name: String, val subjectSource: String, val asserts: List) +data class Assertion(val prop: String, val value: String, val negated: Boolean = false) +data class Claim(val name: String, val subjectSource: String, val asserts: List, val repeatCount: Int) data class CompileBlock(val name: String, val subjectSource: String) object ClaimParser { @@ -7,8 +7,10 @@ object ClaimParser { private val compileFenceOpen = Regex("""^```kotlin\s+compile\s*$""") private val fenceClose = Regex("""^```\s*$""") private val nameLine = Regex("""^//\s*name:\s*([a-z0-9-]+)\s*$""") + private val repeatLine = Regex("""^//\s*repeat:\s*(\d+)\s*$""") private val dpAssertLine = Regex("""^//\s*assert:\s*(width|height)\s*=\s*(\d+)\.dp\s*$""") private val stringAssertLine = Regex("""^//\s*assert:\s*(text|has-click-action)\s*=\s*"([^"]+)"\s*$""") + private val stringNegativeAssertLine = Regex("""^//\s*assert-not:\s*(text)\s*=\s*"([^"]+)"\s*$""") private val allowedProps = setOf("width", "height", "text", "has-click-action") fun parse(markdown: String, sourceFile: String): List { @@ -63,27 +65,32 @@ object ClaimParser { ?: err("missing '// name:' line") if (!seenNames.add(name)) err("duplicate name '$name'") + val repeatCount = body.firstNotNullOfOrNull { repeatLine.matchEntire(it.trim())?.groupValues?.get(1)?.toInt() } ?: 1 + if (repeatCount < 1) err("repeat count must be >= 1") + val asserts = body.mapNotNull { line -> val trimmed = line.trim() dpAssertLine.matchEntire(trimmed)?.let { m -> Assertion(m.groupValues[1], m.groupValues[2]) } ?: stringAssertLine.matchEntire(trimmed)?.let { m -> Assertion(m.groupValues[1], m.groupValues[2]) + } ?: stringNegativeAssertLine.matchEntire(trimmed)?.let { m -> + Assertion(m.groupValues[1], m.groupValues[2], true) } } if (asserts.isEmpty()) { - err("no '// assert: = N.dp' or '// assert: = \"...\"' lines") + err("no '// assert:' or '// assert-not:' lines") } asserts.forEach { if (it.prop !in allowedProps) err("unsupported assert prop '${it.prop}'") } val subject = body.filterNot { l -> val t = l.trim() - nameLine.matches(t) || dpAssertLine.matches(t) || stringAssertLine.matches(t) + nameLine.matches(t) || repeatLine.matches(t) || dpAssertLine.matches(t) || stringAssertLine.matches(t) || stringNegativeAssertLine.matches(t) }.joinToString("\n").trim() if (!subject.contains(Regex("""@Composable\s+fun\s+Subject\s*\("""))) { err("no '@Composable fun Subject()' found") } - return Claim(name, subject, asserts) + return Claim(name, subject, asserts, repeatCount) } private fun toCompileBlock( diff --git a/buildSrc/src/main/kotlin/GenerateClaimTests.kt b/buildSrc/src/main/kotlin/GenerateClaimTests.kt index 38ffa46..a3b006f 100644 --- a/buildSrc/src/main/kotlin/GenerateClaimTests.kt +++ b/buildSrc/src/main/kotlin/GenerateClaimTests.kt @@ -21,13 +21,16 @@ abstract class GenerateClaimTests : DefaultTask() { out.deleteRecursively(); out.mkdirs() val tests = claims.joinToString("\n\n") { c -> - val asserts = c.asserts.joinToString("\n") { a -> emitAssertion(a) } - """ - @Test fun `${c.name}`() { - rule.setContent { ClaimSubjects.`${c.name}`() } - $asserts + (1..c.repeatCount).joinToString("\n\n") { trial -> + val asserts = c.asserts.joinToString("\n") { a -> emitAssertion(a) } + val suffix = if (c.repeatCount == 1) "" else "_trial$trial" + """ + @Test fun `${c.name}$suffix`() { + rule.setContent { ClaimSubjects.`${c.name}`() } + $asserts + } + """.trimIndent() } - """.trimIndent() } val subjects = claims.joinToString("\n\n") { c -> @@ -88,7 +91,10 @@ abstract class GenerateClaimTests : DefaultTask() { when (assertion.prop) { "width" -> " rule.onRoot().assertWidthIsEqualTo(${assertion.value}.dp)" "height" -> " rule.onRoot().assertHeightIsEqualTo(${assertion.value}.dp)" - "text" -> " rule.onNodeWithText(\"${assertion.value.escapeKotlinString()}\").assertExists()" + "text" -> { + val call = if (assertion.negated) "assertDoesNotExist()" else "assertExists()" + " rule.onNodeWithText(\"${assertion.value.escapeKotlinString()}\").$call" + } "has-click-action" -> " rule.onNodeWithText(\"${assertion.value.escapeKotlinString()}\").assertHasClickAction()" else -> error("unsupported assertion prop '${assertion.prop}'") diff --git a/skills/compose-expert/references/accessibility.md b/skills/compose-expert/references/accessibility.md index b8137b2..03ce6d5 100644 --- a/skills/compose-expert/references/accessibility.md +++ b/skills/compose-expert/references/accessibility.md @@ -23,8 +23,10 @@ Executable smoke check for built-in button semantics: ```kotlin verify // name: button-text-exposes-click-action +// repeat: 3 // assert: text = "Click me" // assert: has-click-action = "Click me" +// assert-not: text = "Delete" @Composable fun Subject() { Button(onClick = { }) { Text("Click me") } diff --git a/skills/compose-expert/references/animation.md b/skills/compose-expert/references/animation.md index 6ee4634..6ae3383 100644 --- a/skills/compose-expert/references/animation.md +++ b/skills/compose-expert/references/animation.md @@ -35,7 +35,9 @@ Controls appear/disappear animations with enter and exit transitions. ```kotlin verify // name: animated-visibility-renders-visible-content +// repeat: 3 // assert: text = "Hello!" +// assert-not: text = "Goodbye!" @Composable fun Subject() { var visible by remember { mutableStateOf(true) } diff --git a/skills/compose-expert/references/composition-locals.md b/skills/compose-expert/references/composition-locals.md index 8f12242..ddc5a5a 100644 --- a/skills/compose-expert/references/composition-locals.md +++ b/skills/compose-expert/references/composition-locals.md @@ -8,7 +8,9 @@ A CompositionLocal is a slot in the composition that holds a value accessible to ```kotlin verify // name: composition-local-provider-overrides-default +// repeat: 3 // assert: text = "Dark" +// assert-not: text = "Light" val localAppTheme = compositionLocalOf { "Light" } @Composable diff --git a/skills/compose-expert/references/lists-scrolling.md b/skills/compose-expert/references/lists-scrolling.md index 26110ec..3ac11c8 100644 --- a/skills/compose-expert/references/lists-scrolling.md +++ b/skills/compose-expert/references/lists-scrolling.md @@ -71,7 +71,9 @@ Executable smoke check for the count overload: ```kotlin verify // name: lazy-column-count-renders-visible-items +// repeat: 3 // assert: text = "Item 0" +// assert-not: text = "Item 3" @Composable fun Subject() { LazyColumn { diff --git a/skills/compose-expert/references/modifiers.md b/skills/compose-expert/references/modifiers.md index 09b3070..2103c1f 100644 --- a/skills/compose-expert/references/modifiers.md +++ b/skills/compose-expert/references/modifiers.md @@ -10,6 +10,7 @@ Order matters. Modifiers are applied left-to-right in the DSL, but conceptually ```kotlin verify // name: padding-before-size-adds-to-footprint +// repeat: 3 // assert: width = 132.dp // assert: height = 132.dp // Footprint = 132x132. size fixes the 100x100 inner box, padding adds 16dp on @@ -26,6 +27,7 @@ Order matters. Modifiers are applied left-to-right in the DSL, but conceptually ```kotlin verify // name: size-before-padding-keeps-footprint +// repeat: 3 // assert: width = 100.dp // assert: height = 100.dp // Footprint = 100x100, NOT 132x132. size fixes the element at 100x100 first; diff --git a/skills/compose-expert/references/performance.md b/skills/compose-expert/references/performance.md index 72fd8d9..ee8f55d 100644 --- a/skills/compose-expert/references/performance.md +++ b/skills/compose-expert/references/performance.md @@ -61,7 +61,9 @@ Executable check for a stable model rendered through a composable: ```kotlin verify // name: stable-model-renders-text +// repeat: 3 // assert: text = "Ada" +// assert-not: text = "Bob" @Immutable data class Person(val name: String, val age: Int) diff --git a/skills/compose-expert/references/side-effects.md b/skills/compose-expert/references/side-effects.md index 04617bd..635c203 100644 --- a/skills/compose-expert/references/side-effects.md +++ b/skills/compose-expert/references/side-effects.md @@ -90,7 +90,9 @@ Executable check for the common "load then render" pattern: ```kotlin verify // name: launched-effect-updates-ui +// repeat: 3 // assert: text = "loaded" +// assert-not: text = "loading" @Composable fun Subject() { var data by remember { mutableStateOf("loading") } diff --git a/skills/compose-expert/references/state-management.md b/skills/compose-expert/references/state-management.md index f9089d1..e3e82a6 100644 --- a/skills/compose-expert/references/state-management.md +++ b/skills/compose-expert/references/state-management.md @@ -31,8 +31,10 @@ Both associate state with a composition key, but differ in persistence scope. ```kotlin verify // name: remember-counter-renders-clickable-state +// repeat: 3 // assert: text = "Count: 0" // assert: has-click-action = "Count: 0" +// assert-not: text = "Count: 1" @Composable fun Subject() { var count by remember { mutableIntStateOf(0) } diff --git a/skills/compose-expert/references/theming-material3.md b/skills/compose-expert/references/theming-material3.md index 36fed50..03534f6 100644 --- a/skills/compose-expert/references/theming-material3.md +++ b/skills/compose-expert/references/theming-material3.md @@ -6,7 +6,9 @@ ```kotlin verify // name: material-theme-provides-tokens +// repeat: 3 // assert: text = "Uses MaterialTheme.typography.bodyLarge" +// assert-not: text = "MaterialExpressiveTheme" @Composable fun Subject() { MaterialTheme( diff --git a/skills/compose-expert/references/view-composition.md b/skills/compose-expert/references/view-composition.md index b94d7aa..48318e9 100644 --- a/skills/compose-expert/references/view-composition.md +++ b/skills/compose-expert/references/view-composition.md @@ -113,7 +113,9 @@ Executable check for the slot pattern: ```kotlin verify // name: slot-pattern-renders-child-content +// repeat: 3 // assert: text = "Hello" +// assert-not: text = "World" @Composable fun Subject() { Card {