Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions Ext/Paparazzi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Testify — Android Screenshot Testing — Paparazzi Extensions

<a href="https://search.maven.org/artifact/dev.testify/testify-paparazzi"><img alt="Maven Central" src="https://img.shields.io/maven-central/v/dev.testify/testify-paparazzi?color=%236e40ed&label=dev.testify%3Atestify-paparazzi"/></a>

**Utility library for [Paparazzi](https://github.com/cashapp/paparazzi) snapshot testing, providing factory functions, theme helpers, and multi-variant testing support for Compose UIs.**

Paparazzi snapshot tests often repeat identical boilerplate: rule construction, theme wrapping, and manual light/dark duplication. The Testify Paparazzi extension eliminates this repetition by providing:

- **Device presets** — Curated set of common device configurations (phone, tablet, foldable).
- **Theme helpers** — A `ThemeProvider` interface and extension functions for automatic light/dark snapshot coverage.
- **Factory functions** — `TestifyPaparazzi.component()` and `TestifyPaparazzi.screen()` replace repetitive `Paparazzi(...)` constructors.
- **Font scale testing** — Presets and helpers for verifying accessibility font sizes.
- **Locale/RTL testing** — Presets for internationalization and pseudolocalization testing.
- **Accessibility snapshots** — Pre-configured `AccessibilityRenderExtension` factory.
- **State matrix testing** — Snapshot multiple component states from a single test method.
- **ComposableSnapshotRule** — A high-level JUnit rule combining all features into one declaration.

# Set up testify-paparazzi

**settings.gradle**

Ensure that `mavenCentral()` is available in `dependencyResolutionManagement`.

**Application build.gradle**
```groovy
dependencies {
testImplementation "dev.testify:testify-paparazzi:5.0.1"
testImplementation "app.cash.paparazzi:paparazzi:2.0.0-alpha04"
}
```

# Write a test

### Basic snapshot with theme

Define a `ThemeProvider` for your app's theme:

```kotlin
val myThemeProvider = ThemeProvider { darkTheme, content ->
MyAppTheme(darkTheme = darkTheme) { content() }
}
```

### Using ComposableSnapshotRule

The highest-level API. A single rule declaration provides themed snapshots with no boilerplate:

```kotlin
class MyComponentTest {

@get:Rule val snapshot = ComposableSnapshotRule(themeProvider = myThemeProvider)

@Test fun default() = snapshot.snapshot { MyComponent() }

@Test fun darkTheme() = snapshot.snapshot(variant = ThemeVariant.DARK) { MyComponent() }

@Test fun allThemes() = snapshot.snapshotAllThemes { MyComponent() }
}
```

### Using factory functions directly

For more control, use `TestifyPaparazzi` factory functions with the snapshot extension functions:

```kotlin
class MyComponentTest {

@get:Rule val paparazzi = TestifyPaparazzi.component()

@Test fun default() {
paparazzi.themedSnapshot(myThemeProvider) { MyComponent() }
}

@Test fun allThemes() {
paparazzi.snapshotAllThemes(myThemeProvider) { MyComponent() }
}
}
```

### State matrix testing

Snapshot multiple component states from a single test method:

```kotlin
@Test fun ratingStates() {
paparazzi.snapshotStates(
variants = listOf(
StateVariant("zero_stars", 0),
StateVariant("three_stars", 3),
StateVariant("five_stars", 5),
),
themeProvider = myThemeProvider,
) { rating ->
RatingBar(rating = rating)
}
}
```

### Font scale testing

Verify your UI at different accessibility font sizes:

```kotlin
@Test fun largeFonts() {
paparazzi.snapshotAllFontScales { MyComponent() }
}
```

---

# License

MIT License

Copyright (c) 2026 ndtp

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
107 changes: 107 additions & 0 deletions Ext/Paparazzi/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import com.android.build.gradle.tasks.BundleAar
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile

plugins {
id 'com.android.library'
id 'org.jetbrains.dokka'
id 'maven-publish'
id 'signing'
}

ext {
pom = [
publishedGroupId : 'dev.testify',
artifact : 'testify-paparazzi',
libraryName : 'testify-paparazzi',
libraryDescription: 'Paparazzi snapshot testing utilities for Android Testify',
siteUrl : 'https://github.com/ndtp/android-testify',
gitUrl : 'https://github.com/ndtp/android-testify.git',
licenseName : 'The MIT License',
licenseUrl : 'https://opensource.org/licenses/MIT',
author : 'ndtp'
]
}

version = project.findProperty("testify_version") ?: "0.0.1-SNAPSHOT"
group = pom.publishedGroupId

base {
archivesName = pom.artifact
}

android {
namespace "dev.testify.paparazzi"

lint {
abortOnError true
warningsAsErrors true
textOutput = file('stdout')
textReport true
xmlReport false
}

defaultConfig {
compileSdkVersion = libs.versions.compileSdk.get().toInteger()
minSdkVersion libs.versions.minSdk.get().toInteger()
targetSdkVersion libs.versions.targetSdk.get().toInteger()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

testOptions {
unitTests.returnDefaultValues = true
unitTests.all {
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen { false }
showStandardStreams = true
}
}
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_25
targetCompatibility JavaVersion.VERSION_25
}

dependencies {
compileOnly libs.paparazzi
compileOnly libs.junit4

compileOnly(platform(libs.androidx.compose.bom))
compileOnly libs.androidx.compose.runtime
compileOnly libs.androidx.ui

}
packaging {
resources {
excludes += [
'MANIFEST.MF',
'META-INF/LICENSE.md',
'META-INF/LICENSE-notice.md'
]
}
}
publishing {
singleVariant("release") {
// if you don't want sources/javadoc, remove these lines
withSourcesJar()
withJavadocJar()
}
}
}

afterEvaluate {
apply from: "../../publish.build.gradle"
}

tasks.withType(KotlinJvmCompile).configureEach {
compilerOptions {
allWarningsAsErrors.set(true)
jvmTarget.set(JvmTarget.JVM_25)
}
}

tasks.withType(BundleAar).configureEach {
archiveFileName.set("${pom.artifact}-${version}.aar")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2026 ndtp
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package dev.testify.paparazzi

import app.cash.paparazzi.Paparazzi
import app.cash.paparazzi.accessibility.AccessibilityRenderExtension

/**
* Creates a [Paparazzi] instance with [AccessibilityRenderExtension] pre-configured.
*
* The accessibility render extension overlays accessibility metadata (content descriptions,
* roles, and touch target sizes) on top of the rendered snapshot, making it easy to verify
* that composables are properly annotated for screen readers.
*
* @param device The device configuration to use. Defaults to [TestifyPaparazzi.defaultDevice].
* @param theme The Android theme to apply. Defaults to [TestifyPaparazzi.defaultTheme].
* @return A [Paparazzi] instance configured with the accessibility render extension.
*/
fun TestifyPaparazzi.accessibility(
device: DevicePreset = defaultDevice,
theme: String = defaultTheme,
): Paparazzi = Paparazzi(
deviceConfig = device.config,
theme = theme,
renderExtensions = setOf(AccessibilityRenderExtension()),
)
Loading