| name | paparazzi-editor |
| description | Writes screenshot and snapshot tests for Jetpack Compose UI using Paparazzi. Use when user asks to "add snapshot tests", "visual regression test", "screenshot test with Paparazzi", or "record UI baselines". |
Paparazzi Editor
Overview
Paparazzi renders Compose UI on the JVM — no emulator or device needed. It generates PNG snapshots that act as visual regression baselines committed to the repository.
Setup
Dependencies
[versions]
paparazzi = "1.3.5"
[plugins]
paparazzi = { id = "app.cash.paparazzi", version.ref = "paparazzi" }
plugins {
alias(libs.plugins.paparazzi)
}
Paparazzi applies its own JVM renderer — no Android emulator required.
Basic Test Structure
class HomeScreenPaparazziTest {
@get:Rule
val paparazzi = Paparazzi(
deviceConfig = DeviceConfig.PIXEL_5,
theme = "android:Theme.Material.Light.NoActionBar",
)
@Test
fun `home screen default state`() {
paparazzi.snapshot {
MyAppTheme {
HomeScreen(
state = HomeViewState(
isLoading = false,
items = sampleItems(),
),
onEvent = {},
sideEffects = emptyFlow(),
onNavigation = {},
)
}
}
}
}
Multi-State Snapshots
Test all relevant states explicitly:
class OrderListScreenTest {
@get:Rule
val paparazzi = Paparazzi(deviceConfig = DeviceConfig.PIXEL_5)
@Test
fun `loading state`() {
paparazzi.snapshot("loading") {
MyAppTheme {
OrderListScreen(
state = OrderListViewState(isLoading = true),
onEvent = {},
sideEffects = emptyFlow(),
onNavigation = {},
)
}
}
}
@Test
fun `empty state`() {
paparazzi.snapshot("empty") {
MyAppTheme {
OrderListScreen(
state = OrderListViewState(orders = emptyList()),
onEvent = {},
sideEffects = emptyFlow(),
onNavigation = {},
)
}
}
}
@Test
fun `populated state`() {
paparazzi.snapshot("populated") {
MyAppTheme {
OrderListScreen(
state = OrderListViewState(orders = sampleOrders()),
onEvent = {},
sideEffects = emptyFlow(),
onNavigation = {},
)
}
}
}
@Test
fun `error state`() {
paparazzi.snapshot("error") {
MyAppTheme {
OrderListScreen(
state = OrderListViewState(errorMessage = "Connection failed"),
onEvent = {},
sideEffects = emptyFlow(),
onNavigation = {},
)
}
}
}
}
Dark Mode with TestParameterInjector
Reduce boilerplate for light/dark variants:
@RunWith(TestParameterInjector::class)
class OrderCardSnapshotTest {
@get:Rule
val paparazzi = Paparazzi(deviceConfig = DeviceConfig.PIXEL_5)
enum class Theme(val uiMode: Int) {
Light(Configuration.UI_MODE_NIGHT_NO),
Dark(Configuration.UI_MODE_NIGHT_YES),
}
@Test
fun `order card`(@TestParameter theme: Theme) {
paparazzi.snapshot(theme.name.lowercase()) {
CompositionLocalProvider(
LocalConfiguration provides Configuration().apply { uiMode = theme.uiMode }
) {
MyAppTheme {
Surface {
OrderCard(
order = Order("1", "Pizza Margherita", 12.99),
onClick = {},
)
}
}
}
}
}
}
Device Configurations
DeviceConfig.PIXEL_5
DeviceConfig.NEXUS_5
DeviceConfig.PIXEL_C
DeviceConfig(
screenWidth = 360,
screenHeight = 800,
xdpi = 420,
ydpi = 420,
orientation = ScreenOrientation.PORTRAIT,
nightMode = NightMode.NIGHT,
fontScale = 1.5f,
)
Font Scale Tests
@Test
fun `order card - large text`() {
paparazzi.snapshot("large_font") {
MyAppTheme {
Surface {
OrderCard(
order = sampleOrder(),
onClick = {},
)
}
}
}
}
val paparazzi = Paparazzi(
deviceConfig = DeviceConfig.PIXEL_5.copy(fontScale = 1.5f)
)
Gradle Commands
./gradlew :feature:orders:recordPaparazziDebug
./gradlew :feature:orders:verifyPaparazziDebug
./gradlew verifyPaparazziDebug
Commit snapshots to the repository. They are the baseline for visual regression detection.
Snapshot File Structure
After recording, snapshots are saved at:
feature/orders/src/test/snapshots/images/
com.example.feature.orders_OrderListScreenTest_loading.png
com.example.feature.orders_OrderListScreenTest_populated.png
com.example.feature.orders_OrderCardSnapshotTest_light.png
com.example.feature.orders_OrderCardSnapshotTest_dark.png
Deterministic Rendering
Paparazzi tests must be fully deterministic:
paparazzi.snapshot {
Text(text = "Updated: ${System.currentTimeMillis()}")
}
paparazzi.snapshot {
Text(text = "Updated: Jan 1, 2024")
}
paparazzi.snapshot {
MyComponent(progress = 0.75f)
}
Test Data Helpers
Create a shared file for snapshot test data:
object SnapshotTestData {
fun sampleOrders() = listOf(
Order(id = "1", name = "Pizza Margherita", price = 12.99),
Order(id = "2", name = "Caesar Salad", price = 8.50),
Order(id = "3", name = "Tiramisu", price = 5.00),
)
fun sampleUser() = User(
id = "u1",
name = "Jane Doe",
email = "jane@example.com",
avatarUrl = null,
)
}
CI Configuration
- name: Verify Snapshots
run: ./gradlew verifyPaparazziDebug
- name: Upload snapshot diffs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: snapshot-diffs
path: '**/build/paparazzi/failures/'
Updating Snapshots
When a UI change is intentional:
./gradlew :feature:orders:recordPaparazziDebug
git diff -- '*.png'
git add feature/orders/src/test/snapshots/
git commit -m "chore: update order snapshots after layout change"
Checklist: New Snapshot Test
References