Skip to content

Commit 8eacc38

Browse files
runningcodeclaude
andcommitted
feat(snapshots): Restructure sidecar JSON to match ingestion schema
Bucket appearance inputs (locale, device, font_scale, api_level, width_dp, height_dp, show_system_ui, show_background, preview_name) under `tags`; move preview identity (class_name, method_name, image_file_name) under `context`; replace the `night_mode` boolean with a `color_mode` enum that is emitted only when `uiMode` explicitly sets `UI_MODE_NIGHT_YES` or `UI_MODE_NIGHT_NO`. The serializer is extended with a small recursive `renderJson` helper so the template can emit nested objects without pulling in a JSON library. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 055f0a6 commit 8eacc38

2 files changed

Lines changed: 89 additions & 16 deletions

File tree

plugin-build/src/main/kotlin/io/sentry/android/gradle/snapshot/GenerateSnapshotTestsTask.kt

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ abstract class GenerateSnapshotTestsTask : DefaultTask() {
104104
package $PACKAGE_NAME
105105
106106
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
107+
import android.content.res.Configuration.UI_MODE_NIGHT_NO
107108
import android.content.res.Configuration.UI_MODE_NIGHT_YES
108109
import androidx.compose.foundation.background
109110
import androidx.compose.foundation.layout.Box
@@ -357,23 +358,33 @@ class $CLASS_NAME(
357358
val imagesDir = File(snapshotDir, "images")
358359
imagesDir.mkdirs()
359360
val info = preview.previewInfo
360-
val metadata = linkedMapOf<String, Any>(
361-
"display_name" to screenshotId.removePrefix(preview.declaringClass + "."),
361+
362+
val tags = linkedMapOf<String, Any>()
363+
if (info.name.isNotBlank()) tags["preview_name"] = info.name
364+
if (info.locale.isNotBlank()) tags["locale"] = info.locale
365+
if (info.device.isNotBlank()) tags["device"] = info.device
366+
if (info.fontScale != 1f) tags["font_scale"] = info.fontScale
367+
if (info.apiLevel != -1) tags["api_level"] = info.apiLevel
368+
if (info.widthDp > 0) tags["width_dp"] = info.widthDp
369+
if (info.heightDp > 0) tags["height_dp"] = info.heightDp
370+
if (info.showSystemUi) tags["show_system_ui"] = true
371+
if (info.showBackground) tags["show_background"] = true
372+
373+
val context = linkedMapOf<String, Any>(
362374
"image_file_name" to screenshotId,
363375
"class_name" to preview.declaringClass,
364376
"method_name" to preview.methodName,
365377
)
378+
379+
val metadata = linkedMapOf<String, Any>(
380+
"display_name" to screenshotId.removePrefix(preview.declaringClass + "."),
381+
)
366382
if (info.group.isNotBlank()) metadata["group"] = info.group
367-
if (info.name.isNotBlank()) metadata["preview_name"] = info.name
368-
if (info.locale.isNotBlank()) metadata["locale"] = info.locale
369-
if (info.device.isNotBlank()) metadata["device"] = info.device
370-
metadata["night_mode"] = (info.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES)
371-
if (info.fontScale != 1f) metadata["font_scale"] = info.fontScale
372-
if (info.apiLevel != -1) metadata["api_level"] = info.apiLevel
373-
if (info.widthDp > 0) metadata["width_dp"] = info.widthDp
374-
if (info.heightDp > 0) metadata["height_dp"] = info.heightDp
375-
if (info.showSystemUi) metadata["show_system_ui"] = true
376-
if (info.showBackground) metadata["show_background"] = true
383+
384+
when (info.uiMode and UI_MODE_NIGHT_MASK) {
385+
UI_MODE_NIGHT_YES -> metadata["color_mode"] = "dark"
386+
UI_MODE_NIGHT_NO -> metadata["color_mode"] = "light"
387+
}
377388
378389
val diffThreshold: Float? = runCatching {
379390
val declaring = Class.forName(preview.declaringClass)
@@ -386,15 +397,33 @@ class $CLASS_NAME(
386397
}.getOrNull()
387398
if (diffThreshold != null && diffThreshold != 0f) metadata["diff_threshold"] = diffThreshold
388399
389-
val json = metadata.entries.joinToString(",\n ", prefix = "{\n ", postfix = "\n}") { (k, v) ->
390-
if (v is String) "\"" + k + "\": \"" + escapeJson(v) + "\""
391-
else "\"" + k + "\": " + v
392-
}
400+
if (tags.isNotEmpty()) metadata["tags"] = tags
401+
metadata["context"] = context
402+
403+
val json = renderJson(metadata, 0)
393404
val sidecarName = "Paparazzi_Preview_Test_" +
394405
screenshotId.lowercase(Locale.US).replace("\\s".toRegex(), "_")
395406
File(imagesDir, "${'$'}{sidecarName}.json").writeText(json)
396407
}
397408
409+
private fun renderJson(value: Any, indentLevel: Int): String {
410+
val indent = " ".repeat(indentLevel)
411+
val childIndent = " ".repeat(indentLevel + 1)
412+
return when (value) {
413+
is String -> "\"" + escapeJson(value) + "\""
414+
is Boolean, is Number -> value.toString()
415+
is Map<*, *> -> when (value.isEmpty()) {
416+
true -> "{}"
417+
false -> value.entries.joinToString(
418+
separator = ",\n${'$'}childIndent",
419+
prefix = "{\n${'$'}childIndent",
420+
postfix = "\n${'$'}indent}",
421+
) { (k, v) -> "\"${'$'}k\": " + renderJson(v!!, indentLevel + 1) }
422+
}
423+
else -> "\"" + escapeJson(value.toString()) + "\""
424+
}
425+
}
426+
398427
private fun escapeJson(s: String): String =
399428
s.replace("\\", "\\\\").replace("\"", "\\\"")
400429
}

plugin-build/src/test/kotlin/io/sentry/android/gradle/snapshot/GenerateSnapshotTestsTaskTest.kt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,50 @@ class GenerateSnapshotTestsTaskTest {
146146
)
147147
}
148148

149+
@Test
150+
fun `generated sidecar places preview location fields in context block`() {
151+
val content = generateAndRead(packageTrees = listOf("com.example"))
152+
153+
assertTrue(content.contains("val context = linkedMapOf<String, Any>("))
154+
assertTrue(content.contains("\"image_file_name\" to screenshotId"))
155+
assertTrue(content.contains("\"class_name\" to preview.declaringClass"))
156+
assertTrue(content.contains("\"method_name\" to preview.methodName"))
157+
assertTrue(content.contains("metadata[\"context\"] = context"))
158+
}
159+
160+
@Test
161+
fun `generated sidecar places appearance inputs in tags block`() {
162+
val content = generateAndRead(packageTrees = listOf("com.example"))
163+
164+
assertTrue(content.contains("val tags = linkedMapOf<String, Any>()"))
165+
assertTrue(content.contains("if (info.name.isNotBlank()) tags[\"preview_name\"] = info.name"))
166+
assertTrue(content.contains("if (info.locale.isNotBlank()) tags[\"locale\"] = info.locale"))
167+
assertTrue(content.contains("if (info.device.isNotBlank()) tags[\"device\"] = info.device"))
168+
assertTrue(content.contains("if (info.fontScale != 1f) tags[\"font_scale\"] = info.fontScale"))
169+
assertTrue(content.contains("if (info.apiLevel != -1) tags[\"api_level\"] = info.apiLevel"))
170+
assertTrue(content.contains("if (info.widthDp > 0) tags[\"width_dp\"] = info.widthDp"))
171+
assertTrue(content.contains("if (info.heightDp > 0) tags[\"height_dp\"] = info.heightDp"))
172+
assertTrue(content.contains("if (info.showSystemUi) tags[\"show_system_ui\"] = true"))
173+
assertTrue(content.contains("if (info.showBackground) tags[\"show_background\"] = true"))
174+
assertTrue(content.contains("if (tags.isNotEmpty()) metadata[\"tags\"] = tags"))
175+
}
176+
177+
@Test
178+
fun `generated sidecar maps night mode to color_mode enum`() {
179+
val content = generateAndRead(packageTrees = listOf("com.example"))
180+
181+
assertTrue(content.contains("when (info.uiMode and UI_MODE_NIGHT_MASK) {"))
182+
assertTrue(content.contains("UI_MODE_NIGHT_YES -> metadata[\"color_mode\"] = \"dark\""))
183+
assertTrue(content.contains("UI_MODE_NIGHT_NO -> metadata[\"color_mode\"] = \"light\""))
184+
}
185+
186+
@Test
187+
fun `generated sidecar does not emit legacy night_mode field`() {
188+
val content = generateAndRead(packageTrees = listOf("com.example"))
189+
190+
assertFalse(content.contains("metadata[\"night_mode\"]"))
191+
}
192+
149193
@Test
150194
fun `parseMajorVersion extracts major from standard semver`() {
151195
assertEquals(1, parseMajorVersion("1.3.5"))

0 commit comments

Comments
 (0)