-
-
Notifications
You must be signed in to change notification settings - Fork 37
feat(snapshot): Write per-image sidecar metadata during test execution #1127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
eeae814
167a58b
cfd6516
3f11ec6
ba4c155
8db3b5b
91bc8f9
31bb4b3
815d501
2caa985
d49ba86
4e80fd8
cc01abb
fb5d697
20af204
11d8b61
67c9dc9
e2c8a67
1ecb044
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -108,6 +108,8 @@ import app.cash.paparazzi.TestName | |
| import app.cash.paparazzi.detectEnvironment | ||
| import com.android.ide.common.rendering.api.SessionParams | ||
| import com.android.resources.* | ||
| import java.io.File | ||
| import java.util.Locale | ||
| import kotlin.math.ceil | ||
| import org.junit.Rule | ||
| import org.junit.Test | ||
|
|
@@ -219,6 +221,7 @@ private object PaparazziPreviewRule { | |
| return Paparazzi( | ||
| environment = detectEnvironment().copy(compileSdkVersion = previewApiLevel), | ||
| deviceConfig = DeviceConfigBuilder.build(preview.previewInfo), | ||
| theme = "android:Theme.Translucent.NoTitleBar", | ||
| supportsRtl = true, | ||
| showSystemUi = previewInfo.showSystemUi, | ||
| renderingMode = when { | ||
|
|
@@ -328,7 +331,47 @@ class $CLASS_NAME( | |
| } | ||
| } | ||
| } | ||
|
|
||
| writeSidecarMetadata(screenshotId, preview) | ||
| } | ||
|
|
||
| private fun writeSidecarMetadata( | ||
| screenshotId: String, | ||
| preview: ComposablePreview<AndroidPreviewInfo>, | ||
| ) { | ||
| val snapshotDir = File(System.getProperty("paparazzi.snapshot.dir")) | ||
| val imagesDir = File(snapshotDir, "images") | ||
| imagesDir.mkdirs() | ||
|
Comment on lines
+342
to
+344
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The task to generate snapshot sidecar files writes to a different directory ( Suggested FixUpdate the Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews. |
||
| val info = preview.previewInfo | ||
| val metadata = linkedMapOf<String, Any>( | ||
| "display_name" to screenshotId.removePrefix(preview.declaringClass + "."), | ||
| "image_file_name" to screenshotId, | ||
| "className" to preview.declaringClass, | ||
|
sentry[bot] marked this conversation as resolved.
|
||
| "methodName" to preview.methodName, | ||
| ) | ||
| if (info.group.isNotBlank()) metadata["group"] = info.group | ||
| if (info.name.isNotBlank()) metadata["previewName"] = info.name | ||
| if (info.locale.isNotBlank()) metadata["locale"] = info.locale | ||
| if (info.device.isNotBlank()) metadata["device"] = info.device | ||
| metadata["nightMode"] = (info.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES) | ||
| if (info.fontScale != 1f) metadata["fontScale"] = info.fontScale | ||
| if (info.apiLevel != -1) metadata["apiLevel"] = info.apiLevel | ||
| if (info.widthDp > 0) metadata["widthDp"] = info.widthDp | ||
| if (info.heightDp > 0) metadata["heightDp"] = info.heightDp | ||
| if (info.showSystemUi) metadata["showSystemUi"] = true | ||
| if (info.showBackground) metadata["showBackground"] = true | ||
|
|
||
| val json = metadata.entries.joinToString(",\n ", prefix = "{\n ", postfix = "\n}") { (k, v) -> | ||
| if (v is String) "\"" + k + "\": \"" + escapeJson(v) + "\"" | ||
| else "\"" + k + "\": " + v | ||
| } | ||
| val sidecarName = "Paparazzi_Preview_Test_" + | ||
| screenshotId.lowercase(Locale.US).replace("\\s".toRegex(), "_") | ||
| File(imagesDir, "${'$'}{sidecarName}.json").writeText(json) | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| private fun escapeJson(s: String): String = | ||
| s.replace("\\", "\\\\").replace("\"", "\\\"") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incomplete JSON escape misses control charactersLow Severity The |
||
| } | ||
| """ | ||
| .trimStart() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,7 +47,8 @@ private const val SQUARE = "id:wearos_square" | |
| // region UI mode constants | ||
| // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/content/res/Configuration.java | ||
|
|
||
| private const val UI_MODE_NIGHT_YES: Int = 32 | ||
| internal const val UI_MODE_NIGHT_MASK: Int = 0x30 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Newly added
|
||
| internal const val UI_MODE_NIGHT_YES: Int = 0x20 | ||
| private const val UI_MODE_TYPE_NORMAL: Int = 1 | ||
|
|
||
| // endregion | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| package io.sentry.android.gradle.snapshot | ||
|
|
||
| import java.io.File | ||
| import kotlin.test.assertFalse | ||
| import kotlin.test.assertTrue | ||
| import org.gradle.testfixtures.ProjectBuilder | ||
| import org.junit.Rule | ||
| import org.junit.Test | ||
| import org.junit.rules.TemporaryFolder | ||
|
|
||
| class GenerateSnapshotTestsTaskTest { | ||
|
|
||
| @get:Rule val tmpDir = TemporaryFolder() | ||
|
|
||
| @Test | ||
| fun `generates test file in correct package directory`() { | ||
| val task = createTask(packageTrees = listOf("com.example")) | ||
|
|
||
| task.generate() | ||
|
|
||
| val outputDir = task.outputDir.get().asFile | ||
| val expectedFile = File(outputDir, "io/sentry/snapshot/ComposablePreviewSnapshotTest.kt") | ||
| assertTrue(expectedFile.exists()) | ||
| } | ||
|
|
||
| @Test | ||
| fun `cleans output directory on re-run`() { | ||
| val task = createTask(packageTrees = listOf("com.example")) | ||
| val outputDir = task.outputDir.get().asFile | ||
|
|
||
| // Create a stale file in the output directory | ||
| val staleDir = File(outputDir, "io/sentry/snapshot") | ||
| staleDir.mkdirs() | ||
| val staleFile = File(staleDir, "OldFile.kt") | ||
| staleFile.writeText("old content") | ||
| assertTrue(staleFile.exists()) | ||
|
|
||
| task.generate() | ||
|
|
||
| assertFalse(staleFile.exists()) | ||
| val expectedFile = File(outputDir, "io/sentry/snapshot/ComposablePreviewSnapshotTest.kt") | ||
| assertTrue(expectedFile.exists()) | ||
| } | ||
|
|
||
| @Test | ||
| fun `generated file contains correct package declaration`() { | ||
| val content = generateAndRead(packageTrees = listOf("com.example")) | ||
|
|
||
| assertTrue(content.contains("package io.sentry.snapshot")) | ||
| } | ||
|
|
||
| @Test | ||
| fun `generated file scans configured package tree`() { | ||
| val content = generateAndRead(packageTrees = listOf("com.example.app")) | ||
|
|
||
| assertTrue(content.contains(".scanPackageTrees(\"com.example.app\")")) | ||
| } | ||
|
|
||
| @Test | ||
| fun `generated file scans multiple package trees`() { | ||
| val content = | ||
| generateAndRead(packageTrees = listOf("com.example.feature1", "com.example.feature2")) | ||
|
|
||
| assertTrue( | ||
| content.contains(".scanPackageTrees(\"com.example.feature1\", \"com.example.feature2\")") | ||
| ) | ||
| } | ||
|
|
||
| @Test | ||
| fun `generated file includes private previews when enabled`() { | ||
| val content = | ||
| generateAndRead(packageTrees = listOf("com.example"), includePrivatePreviews = true) | ||
|
|
||
| assertTrue(content.contains(".includePrivatePreviews()")) | ||
| } | ||
|
|
||
| @Test | ||
| fun `generated file excludes private previews by default`() { | ||
| val content = generateAndRead(packageTrees = listOf("com.example")) | ||
|
|
||
| assertFalse(content.contains(".includePrivatePreviews()")) | ||
| } | ||
|
|
||
| @Test | ||
| fun `generated file contains parameterized test runner`() { | ||
| val content = generateAndRead(packageTrees = listOf("com.example")) | ||
|
|
||
| assertTrue(content.contains("@RunWith(Parameterized::class)")) | ||
| assertTrue(content.contains("class ComposablePreviewSnapshotTest")) | ||
| } | ||
|
|
||
| @Test | ||
| fun `generated sidecar metadata uses short display name`() { | ||
| val content = generateAndRead(packageTrees = listOf("com.example")) | ||
|
|
||
| assertTrue( | ||
| content.contains( | ||
| "\"display_name\" to screenshotId.removePrefix(preview.declaringClass + \".\")" | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
| @Test | ||
| fun `generated sidecar metadata uses full screenshotId for image file name`() { | ||
| val content = generateAndRead(packageTrees = listOf("com.example")) | ||
|
|
||
| assertTrue(content.contains("\"image_file_name\" to screenshotId")) | ||
| } | ||
|
|
||
| @Test | ||
| fun `generated file writes sidecar json to images directory`() { | ||
| val content = generateAndRead(packageTrees = listOf("com.example")) | ||
|
|
||
| assertTrue(content.contains("val imagesDir = File(snapshotDir, \"images\")")) | ||
| assertTrue(content.contains("File(imagesDir, \"\${sidecarName}.json\").writeText(json)")) | ||
| } | ||
|
|
||
| @Test | ||
| fun `generated sidecar filename is lowercased to match Paparazzi image filenames`() { | ||
| val content = generateAndRead(packageTrees = listOf("com.example")) | ||
|
|
||
| assertTrue( | ||
| content.contains("screenshotId.lowercase(Locale.US).replace(\"\\\\s\".toRegex(), \"_\")") | ||
| ) | ||
| } | ||
|
|
||
| private fun generateAndRead( | ||
| packageTrees: List<String>, | ||
| includePrivatePreviews: Boolean = false, | ||
| ): String { | ||
| val task = createTask(packageTrees, includePrivatePreviews) | ||
| task.generate() | ||
| val file = | ||
| File(task.outputDir.get().asFile, "io/sentry/snapshot/ComposablePreviewSnapshotTest.kt") | ||
| return file.readText() | ||
| } | ||
|
|
||
| private fun createTask( | ||
| packageTrees: List<String>, | ||
| includePrivatePreviews: Boolean = false, | ||
| ): GenerateSnapshotTestsTask { | ||
| val project = ProjectBuilder.builder().build() | ||
| return project.tasks | ||
| .register("testGenerateSnapshotTests", GenerateSnapshotTestsTask::class.java) { task -> | ||
| task.includePrivatePreviews.set(includePrivatePreviews) | ||
| task.packageTrees.set(packageTrees) | ||
| task.outputDir.set(tmpDir.newFolder("output")) | ||
| } | ||
| .get() | ||
| } | ||
| } |


Uh oh!
There was an error while loading. Please reload this page.