Skip to content

Commit 19fdb93

Browse files
committed
Add testify-paparazzi utility library (Ext/Paparazzi)
Introduces a new JVM library module providing Paparazzi snapshot testing utilities: device presets, theme helpers, font scale/locale testing, accessibility snapshot support, state matrix testing, and a high-level ComposableSnapshotRule. Includes CI verification in bitrise.yml.
1 parent 396a59a commit 19fdb93

14 files changed

Lines changed: 1053 additions & 0 deletions

Ext/Paparazzi/README.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Testify — Android Screenshot Testing — Paparazzi Extensions
2+
3+
<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>
4+
5+
**Utility library for [Paparazzi](https://github.com/cashapp/paparazzi) snapshot testing, providing factory functions, theme helpers, and multi-variant testing support for Compose UIs.**
6+
7+
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:
8+
9+
- **Device presets** — Curated set of common device configurations (phone, tablet, foldable).
10+
- **Theme helpers** — A `ThemeProvider` interface and extension functions for automatic light/dark snapshot coverage.
11+
- **Factory functions**`TestifyPaparazzi.component()` and `TestifyPaparazzi.screen()` replace repetitive `Paparazzi(...)` constructors.
12+
- **Font scale testing** — Presets and helpers for verifying accessibility font sizes.
13+
- **Locale/RTL testing** — Presets for internationalization and pseudolocalization testing.
14+
- **Accessibility snapshots** — Pre-configured `AccessibilityRenderExtension` factory.
15+
- **State matrix testing** — Snapshot multiple component states from a single test method.
16+
- **ComposableSnapshotRule** — A high-level JUnit rule combining all features into one declaration.
17+
18+
# Set up testify-paparazzi
19+
20+
**settings.gradle**
21+
22+
Ensure that `mavenCentral()` is available in `dependencyResolutionManagement`.
23+
24+
**Application build.gradle**
25+
```groovy
26+
dependencies {
27+
testImplementation "dev.testify:testify-paparazzi:5.0.1"
28+
testImplementation "app.cash.paparazzi:paparazzi:2.0.0-alpha04"
29+
}
30+
```
31+
32+
# Write a test
33+
34+
### Basic snapshot with theme
35+
36+
Define a `ThemeProvider` for your app's theme:
37+
38+
```kotlin
39+
val myThemeProvider = ThemeProvider { darkTheme, content ->
40+
MyAppTheme(darkTheme = darkTheme) { content() }
41+
}
42+
```
43+
44+
### Using ComposableSnapshotRule
45+
46+
The highest-level API. A single rule declaration provides themed snapshots with no boilerplate:
47+
48+
```kotlin
49+
class MyComponentTest {
50+
51+
@get:Rule val snapshot = ComposableSnapshotRule(themeProvider = myThemeProvider)
52+
53+
@Test fun default() = snapshot.snapshot { MyComponent() }
54+
55+
@Test fun darkTheme() = snapshot.snapshot(variant = ThemeVariant.DARK) { MyComponent() }
56+
57+
@Test fun allThemes() = snapshot.snapshotAllThemes { MyComponent() }
58+
}
59+
```
60+
61+
### Using factory functions directly
62+
63+
For more control, use `TestifyPaparazzi` factory functions with the snapshot extension functions:
64+
65+
```kotlin
66+
class MyComponentTest {
67+
68+
@get:Rule val paparazzi = TestifyPaparazzi.component()
69+
70+
@Test fun default() {
71+
paparazzi.themedSnapshot(myThemeProvider) { MyComponent() }
72+
}
73+
74+
@Test fun allThemes() {
75+
paparazzi.snapshotAllThemes(myThemeProvider) { MyComponent() }
76+
}
77+
}
78+
```
79+
80+
### State matrix testing
81+
82+
Snapshot multiple component states from a single test method:
83+
84+
```kotlin
85+
@Test fun ratingStates() {
86+
paparazzi.snapshotStates(
87+
variants = listOf(
88+
StateVariant("zero_stars", 0),
89+
StateVariant("three_stars", 3),
90+
StateVariant("five_stars", 5),
91+
),
92+
themeProvider = myThemeProvider,
93+
) { rating ->
94+
RatingBar(rating = rating)
95+
}
96+
}
97+
```
98+
99+
### Font scale testing
100+
101+
Verify your UI at different accessibility font sizes:
102+
103+
```kotlin
104+
@Test fun largeFonts() {
105+
paparazzi.snapshotAllFontScales { MyComponent() }
106+
}
107+
```
108+
109+
---
110+
111+
# License
112+
113+
MIT License
114+
115+
Copyright (c) 2026 ndtp
116+
117+
Permission is hereby granted, free of charge, to any person obtaining a copy
118+
of this software and associated documentation files (the "Software"), to deal
119+
in the Software without restriction, including without limitation the rights
120+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
121+
copies of the Software, and to permit persons to whom the Software is
122+
furnished to do so, subject to the following conditions:
123+
124+
The above copyright notice and this permission notice shall be included in all
125+
copies or substantial portions of the Software.
126+
127+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
128+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
129+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
130+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
131+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
132+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
133+
SOFTWARE.

Ext/Paparazzi/build.gradle

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import com.android.build.gradle.tasks.BundleAar
2+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
3+
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
4+
5+
plugins {
6+
id 'com.android.library'
7+
id 'org.jetbrains.dokka'
8+
id 'maven-publish'
9+
id 'signing'
10+
}
11+
12+
ext {
13+
pom = [
14+
publishedGroupId : 'dev.testify',
15+
artifact : 'testify-paparazzi',
16+
libraryName : 'testify-paparazzi',
17+
libraryDescription: 'Paparazzi snapshot testing utilities for Android Testify',
18+
siteUrl : 'https://github.com/ndtp/android-testify',
19+
gitUrl : 'https://github.com/ndtp/android-testify.git',
20+
licenseName : 'The MIT License',
21+
licenseUrl : 'https://opensource.org/licenses/MIT',
22+
author : 'ndtp'
23+
]
24+
}
25+
26+
version = project.findProperty("testify_version") ?: "0.0.1-SNAPSHOT"
27+
group = pom.publishedGroupId
28+
29+
base {
30+
archivesName = pom.artifact
31+
}
32+
33+
android {
34+
namespace "dev.testify.paparazzi"
35+
36+
lint {
37+
abortOnError true
38+
warningsAsErrors true
39+
textOutput = file('stdout')
40+
textReport true
41+
xmlReport false
42+
}
43+
44+
defaultConfig {
45+
compileSdkVersion = libs.versions.compileSdk.get().toInteger()
46+
minSdkVersion libs.versions.minSdk.get().toInteger()
47+
targetSdkVersion libs.versions.targetSdk.get().toInteger()
48+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
49+
}
50+
51+
testOptions {
52+
unitTests.returnDefaultValues = true
53+
unitTests.all {
54+
testLogging {
55+
events "passed", "skipped", "failed", "standardOut", "standardError"
56+
outputs.upToDateWhen { false }
57+
showStandardStreams = true
58+
}
59+
}
60+
}
61+
62+
compileOptions {
63+
sourceCompatibility JavaVersion.VERSION_25
64+
targetCompatibility JavaVersion.VERSION_25
65+
}
66+
67+
dependencies {
68+
compileOnly libs.paparazzi
69+
compileOnly libs.junit4
70+
71+
compileOnly(platform(libs.androidx.compose.bom))
72+
compileOnly libs.androidx.compose.runtime
73+
compileOnly libs.androidx.ui
74+
75+
}
76+
packaging {
77+
resources {
78+
excludes += [
79+
'MANIFEST.MF',
80+
'META-INF/LICENSE.md',
81+
'META-INF/LICENSE-notice.md'
82+
]
83+
}
84+
}
85+
publishing {
86+
singleVariant("release") {
87+
// if you don't want sources/javadoc, remove these lines
88+
withSourcesJar()
89+
withJavadocJar()
90+
}
91+
}
92+
}
93+
94+
afterEvaluate {
95+
apply from: "../../publish.build.gradle"
96+
}
97+
98+
tasks.withType(KotlinJvmCompile).configureEach {
99+
compilerOptions {
100+
allWarningsAsErrors.set(true)
101+
jvmTarget.set(JvmTarget.JVM_25)
102+
}
103+
}
104+
105+
tasks.withType(BundleAar).configureEach {
106+
archiveFileName.set("${pom.artifact}-${version}.aar")
107+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2026 ndtp
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
package dev.testify.paparazzi
26+
27+
import app.cash.paparazzi.Paparazzi
28+
import app.cash.paparazzi.accessibility.AccessibilityRenderExtension
29+
30+
/**
31+
* Creates a [Paparazzi] instance with [AccessibilityRenderExtension] pre-configured.
32+
*
33+
* The accessibility render extension overlays accessibility metadata (content descriptions,
34+
* roles, and touch target sizes) on top of the rendered snapshot, making it easy to verify
35+
* that composables are properly annotated for screen readers.
36+
*
37+
* @param device The device configuration to use. Defaults to [TestifyPaparazzi.defaultDevice].
38+
* @param theme The Android theme to apply. Defaults to [TestifyPaparazzi.defaultTheme].
39+
* @return A [Paparazzi] instance configured with the accessibility render extension.
40+
*/
41+
fun TestifyPaparazzi.accessibility(
42+
device: DevicePreset = defaultDevice,
43+
theme: String = defaultTheme,
44+
): Paparazzi = Paparazzi(
45+
deviceConfig = device.config,
46+
theme = theme,
47+
renderExtensions = setOf(AccessibilityRenderExtension()),
48+
)

0 commit comments

Comments
 (0)