Skip to content

Commit 7938856

Browse files
runningcodeclaude
andauthored
feat(snapshots): Restructure sidecar JSON to match ingestion schema (#1162)
* 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> * feat(snapshots): Move ui_mode into sidecar tags block Relocate the uiMode → light/dark mapping from the top-level metadata field `color_mode` into the `tags` map, keyed as `ui_mode`. This aligns the field with the other appearance inputs (locale, device, font_scale, etc.) and matches the ingestion schema's expectation that preview configuration travels as tags rather than first-class metadata. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 055f0a6 commit 7938856

2 files changed

Lines changed: 88 additions & 16 deletions

File tree

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

Lines changed: 44 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,32 @@ 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+
when (info.uiMode and UI_MODE_NIGHT_MASK) {
373+
UI_MODE_NIGHT_YES -> tags["ui_mode"] = "dark"
374+
UI_MODE_NIGHT_NO -> tags["ui_mode"] = "light"
375+
}
376+
377+
val context = linkedMapOf<String, Any>(
362378
"image_file_name" to screenshotId,
363379
"class_name" to preview.declaringClass,
364380
"method_name" to preview.methodName,
365381
)
382+
383+
val metadata = linkedMapOf<String, Any>(
384+
"display_name" to screenshotId.removePrefix(preview.declaringClass + "."),
385+
)
366386
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
377387
378388
val diffThreshold: Float? = runCatching {
379389
val declaring = Class.forName(preview.declaringClass)
@@ -386,15 +396,33 @@ class $CLASS_NAME(
386396
}.getOrNull()
387397
if (diffThreshold != null && diffThreshold != 0f) metadata["diff_threshold"] = diffThreshold
388398
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-
}
399+
if (tags.isNotEmpty()) metadata["tags"] = tags
400+
metadata["context"] = context
401+
402+
val json = renderJson(metadata, 0)
393403
val sidecarName = "Paparazzi_Preview_Test_" +
394404
screenshotId.lowercase(Locale.US).replace("\\s".toRegex(), "_")
395405
File(imagesDir, "${'$'}{sidecarName}.json").writeText(json)
396406
}
397407
408+
private fun renderJson(value: Any, indentLevel: Int): String {
409+
val indent = " ".repeat(indentLevel)
410+
val childIndent = " ".repeat(indentLevel + 1)
411+
return when (value) {
412+
is String -> "\"" + escapeJson(value) + "\""
413+
is Boolean, is Number -> value.toString()
414+
is Map<*, *> -> when (value.isEmpty()) {
415+
true -> "{}"
416+
false -> value.entries.joinToString(
417+
separator = ",\n${'$'}childIndent",
418+
prefix = "{\n${'$'}childIndent",
419+
postfix = "\n${'$'}indent}",
420+
) { (k, v) -> "\"${'$'}k\": " + renderJson(v!!, indentLevel + 1) }
421+
}
422+
else -> "\"" + escapeJson(value.toString()) + "\""
423+
}
424+
}
425+
398426
private fun escapeJson(s: String): String =
399427
s.replace("\\", "\\\\").replace("\"", "\\\"")
400428
}

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 places ui_mode in tags block`() {
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 -> tags[\"ui_mode\"] = \"dark\""))
183+
assertTrue(content.contains("UI_MODE_NIGHT_NO -> tags[\"ui_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)