Skip to content
Open
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
131 changes: 131 additions & 0 deletions PAPARAZZI_SETUP.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
# Paparazzi UI Snapshot Testing Setup

This project uses [Paparazzi](https://github.com/cashapp/paparazzi) for snapshot testing of Compose UI components.

## Overview

Paparazzi enables fast, deterministic screenshot testing without requiring an Android device or emulator. All UI snapshots are generated during unit tests and stored as golden images.

## Configuration

### Versions
- **Android Gradle Plugin**: 8.5.2
- **Kotlin**: 2.0.0
- **Paparazzi**: 1.3.4
- **Compile SDK**: 34

### Build Configuration

Paparazzi is configured in both `app/build.gradle` and `composed-barcodes/build.gradle`:

```gradle
plugins {
id 'app.cash.paparazzi'
}

dependencies {
testImplementation(libs.junit)
}
```

## Snapshot Tests

### Library Module (`composed-barcodes`)

**Location**: `composed-barcodes/src/test/kotlin/com/simonsickle/compose/barcodes/`

#### Test Files:
- `BarcodeSnapshotTest.kt` - Tests all Barcode component preview variations

#### Preview Composables:
Created in `BarcodePreviews.kt`, covering:
- QR Code (default, small, high-resolution, no-progress variations)
- Code 128, Code 39, EAN-13, UPC-A
- Data Matrix, Aztec, PDF417

### App Module (`app`)

**Location**: `app/src/test/java/com/simonsickle/composedbarcodes/examples/`

#### Test Files:
- `BarcodePreviewSnapshotTest.kt` - Tests all barcode example screens

Covers all 13 barcode types:
QR, Aztec, Code39, Code93, Code128, Codabar, DataMatrix, EAN8, EAN13, ITF, PDF417, UPC-A, UPC-E

## Running Tests

### Record Golden Images
```bash
# Record all snapshots
./gradlew recordPaparazziDebug

# Record for specific module
./gradlew composed-barcodes:recordPaparazziDebug
./gradlew app:recordPaparazziDebug
```

### Verify Snapshots
```bash
# Verify all snapshots match golden images
./gradlew verifyPaparazziDebug

# Verify for specific module
./gradlew composed-barcodes:verifyPaparazziDebug
./gradlew app:verifyPaparazziDebug
```

### View Reports
After running tests, view the generated report at:
- Library: `composed-barcodes/build/reports/paparazzi/debug/index.html`
- App: `app/build/reports/paparazzi/debug/index.html`

## CI Integration

The GitHub Actions workflow (`.github/workflows/unit-tests.yml`) runs Paparazzi tests automatically on push and pull requests:

```yaml
- name: Run Paparazzi snapshot tests - Library
run: ./gradlew composed-barcodes:verifyPaparazziDebug --stacktrace

- name: Run Paparazzi snapshot tests - App
run: ./gradlew app:verifyPaparazziDebug --stacktrace
```

## Implementation Details

### Synchronous Barcode for Testing

To ensure snapshots capture actual barcodes instead of loading states, the library provides a `SynchronousBarcode` composable specifically for preview and testing purposes. This version generates barcodes synchronously during composition, making it ideal for Paparazzi snapshot tests.

- **Production use**: Use the async `Barcode` composable with loading indicator support
- **Testing/Previews**: Use `SynchronousBarcode` for reliable snapshot testing

All preview composables in `BarcodePreviews.kt` use `SynchronousBarcode` to ensure accurate snapshot rendering.

**Important**: When using `SynchronousBarcode`, ensure the `width` and `height` parameters match the modifier dimensions to prevent barcode distortion. This maintains proper aspect ratios for each barcode type (e.g., square for QR codes and Data Matrix, wide rectangles for Code 128, EAN-13, and UPC-A).

## Known Limitations

1. **App Module**: There's a known Gradle dependency version mismatch in the app module (requires compileSdk 35 and AGP 8.6.0+, currently using 34 and 8.5.2). This doesn't affect the library module tests.

## Troubleshooting

### Tests Failing After Dependency Updates
Run `./gradlew --stop` to stop all Gradle daemons, then re-run tests.

### Snapshot Mismatches
If legitimate UI changes were made, record new golden images:
```bash
./gradlew recordPaparazziDebug
```

### Build Failures
Ensure you're using the correct SDK version (34) and the specified plugin versions.

## Best Practices

1. **Always record golden images** after intentional UI changes
2. **Review snapshot diffs** carefully before accepting changes
3. **Keep snapshots small** - test individual components rather than entire screens when possible
4. **Use descriptive test names** to make snapshot files easy to identify
13 changes: 11 additions & 2 deletions app/build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.compose'
id 'app.cash.paparazzi'
}

android {
defaultConfig {
applicationId "com.simonsickle.composedbarcodes"
compileSdk = 36
compileSdk = 34
minSdkVersion 26
targetSdkVersion 36
targetSdkVersion 34
versionCode 2
versionName "1.1.0"

Expand All@@ -27,6 +29,10 @@ android {
targetCompatibility JavaVersion.VERSION_19
}

kotlinOptions {
jvmTarget = '19'
}

buildFeatures {
compose true
}
Expand All@@ -50,4 +56,7 @@ dependencies {

// Nav components
implementation(libs.androidx.navigation.compose)

// Testing
testImplementation(libs.junit)
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.simonsickle.compose.barcodes.Barcode
import com.simonsickle.compose.barcodes.SynchronousBarcode
import com.simonsickle.compose.barcodes.BarcodeType

@Composable
Expand All@@ -30,7 +30,7 @@ fun GenericBarcodeExample(
.fillMaxSize()
) {
if (barcodeType.isValueValid(value)) {
Barcode(
SynchronousBarcode(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.width(300.dp)
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
package com.simonsickle.composedbarcodes.examples

import app.cash.paparazzi.DeviceConfig
import app.cash.paparazzi.Paparazzi
import org.junit.Rule
import org.junit.Test

/**
* Paparazzi snapshot tests for all barcode preview composables.
* These tests ensure that the UI renders correctly and catches any visual regressions.
*/
class BarcodePreviewSnapshotTest {

@get:Rule
val paparazzi = Paparazzi(
deviceConfig = DeviceConfig.PIXEL_5,
showSystemUi = false
)

@Test
fun qrCodePreview() {
paparazzi.snapshot {
Qr()
}
}

@Test
fun aztecPreview() {
paparazzi.snapshot {
Aztec()
}
}

@Test
fun code39Preview() {
paparazzi.snapshot {
Code39()
}
}

@Test
fun code93Preview() {
paparazzi.snapshot {
Code93()
}
}

@Test
fun code128Preview() {
paparazzi.snapshot {
Code128()
}
}

@Test
fun codabarPreview() {
paparazzi.snapshot {
Codabar()
}
}

@Test
fun dataMatrixPreview() {
paparazzi.snapshot {
DataMatrix()
}
}

@Test
fun ean8Preview() {
paparazzi.snapshot {
Ean8()
}
}

@Test
fun ean13Preview() {
paparazzi.snapshot {
Ean13()
}
}

@Test
fun itfPreview() {
paparazzi.snapshot {
Itf()
}
}

@Test
fun pdf417Preview() {
paparazzi.snapshot {
Pdf417()
}
}

@Test
fun upcAPreview() {
paparazzi.snapshot {
UpcA()
}
}

@Test
fun upcEPreview() {
paparazzi.snapshot {
UpcE()
}
}
}
8 changes: 5 additions & 3 deletions build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
plugins {
id("com.android.application") version '9.1.0' apply false
id("com.android.library") version '9.1.0' apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.3.10" apply false
id("com.android.application") version '8.5.2' apply false
id("com.android.library") version '8.5.2' apply false
id("org.jetbrains.kotlin.android") version "2.0.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" apply false
id("app.cash.paparazzi") version "1.3.4" apply false
id("io.github.gradle-nexus.publish-plugin") version "2.0.0"
}

Expand Down
15 changes: 12 additions & 3 deletions composed-barcodes/build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
plugins {
id "com.android.library"
id "org.jetbrains.kotlin.android"
id "org.jetbrains.kotlin.plugin.compose"
id "app.cash.paparazzi"
}

ext {
Expand All@@ -13,7 +15,7 @@ android {
namespace "com.simonsickle.compose"

defaultConfig {
compileSdk = 36
compileSdk = 34
minSdkVersion 21
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
Expand All@@ -35,6 +37,10 @@ android {
targetCompatibility JavaVersion.VERSION_19
}

kotlinOptions {
jvmTarget = '19'
}

buildFeatures {
compose true
}
Expand All@@ -50,7 +56,7 @@ android {

testOptions {
animationsDisabled true
targetSdk 36
targetSdk 34
}

publishing {
Expand All@@ -60,7 +66,7 @@ android {
}
}
lint {
targetSdk 36
targetSdk 34
}
}

Expand All@@ -75,6 +81,9 @@ dependencies {
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.ui.tooling.preview)
debugImplementation(libs.androidx.compose.ui.tooling)

// Testing
testImplementation(libs.junit)
}

apply from: "${rootDir}/scripts/publish-module.gradle"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Add Paparazzi snapshot testing for UI tests by firebender-bot[bot] · Pull Request #20 · simonsickle/ComposedBarcodes · GitHub
Skip to content
Open
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
131 changes: 131 additions & 0 deletions PAPARAZZI_SETUP.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
# Paparazzi UI Snapshot Testing Setup

This project uses [Paparazzi](https://github.com/cashapp/paparazzi) for snapshot testing of Compose UI components.

## Overview

Paparazzi enables fast, deterministic screenshot testing without requiring an Android device or emulator. All UI snapshots are generated during unit tests and stored as golden images.

## Configuration

### Versions
- **Android Gradle Plugin**: 8.5.2
- **Kotlin**: 2.0.0
- **Paparazzi**: 1.3.4
- **Compile SDK**: 34

### Build Configuration

Paparazzi is configured in both `app/build.gradle` and `composed-barcodes/build.gradle`:

```gradle
plugins {
id 'app.cash.paparazzi'
}

dependencies {
testImplementation(libs.junit)
}
```

## Snapshot Tests

### Library Module (`composed-barcodes`)

**Location**: `composed-barcodes/src/test/kotlin/com/simonsickle/compose/barcodes/`

#### Test Files:
- `BarcodeSnapshotTest.kt` - Tests all Barcode component preview variations

#### Preview Composables:
Created in `BarcodePreviews.kt`, covering:
- QR Code (default, small, high-resolution, no-progress variations)
- Code 128, Code 39, EAN-13, UPC-A
- Data Matrix, Aztec, PDF417

### App Module (`app`)

**Location**: `app/src/test/java/com/simonsickle/composedbarcodes/examples/`

#### Test Files:
- `BarcodePreviewSnapshotTest.kt` - Tests all barcode example screens

Covers all 13 barcode types:
QR, Aztec, Code39, Code93, Code128, Codabar, DataMatrix, EAN8, EAN13, ITF, PDF417, UPC-A, UPC-E

## Running Tests

### Record Golden Images
```bash
# Record all snapshots
./gradlew recordPaparazziDebug

# Record for specific module
./gradlew composed-barcodes:recordPaparazziDebug
./gradlew app:recordPaparazziDebug
```

### Verify Snapshots
```bash
# Verify all snapshots match golden images
./gradlew verifyPaparazziDebug

# Verify for specific module
./gradlew composed-barcodes:verifyPaparazziDebug
./gradlew app:verifyPaparazziDebug
```

### View Reports
After running tests, view the generated report at:
- Library: `composed-barcodes/build/reports/paparazzi/debug/index.html`
- App: `app/build/reports/paparazzi/debug/index.html`

## CI Integration

The GitHub Actions workflow (`.github/workflows/unit-tests.yml`) runs Paparazzi tests automatically on push and pull requests:

```yaml
- name: Run Paparazzi snapshot tests - Library
run: ./gradlew composed-barcodes:verifyPaparazziDebug --stacktrace

- name: Run Paparazzi snapshot tests - App
run: ./gradlew app:verifyPaparazziDebug --stacktrace
```

## Implementation Details

### Synchronous Barcode for Testing

To ensure snapshots capture actual barcodes instead of loading states, the library provides a `SynchronousBarcode` composable specifically for preview and testing purposes. This version generates barcodes synchronously during composition, making it ideal for Paparazzi snapshot tests.

- **Production use**: Use the async `Barcode` composable with loading indicator support
- **Testing/Previews**: Use `SynchronousBarcode` for reliable snapshot testing

All preview composables in `BarcodePreviews.kt` use `SynchronousBarcode` to ensure accurate snapshot rendering.

**Important**: When using `SynchronousBarcode`, ensure the `width` and `height` parameters match the modifier dimensions to prevent barcode distortion. This maintains proper aspect ratios for each barcode type (e.g., square for QR codes and Data Matrix, wide rectangles for Code 128, EAN-13, and UPC-A).

## Known Limitations

1. **App Module**: There's a known Gradle dependency version mismatch in the app module (requires compileSdk 35 and AGP 8.6.0+, currently using 34 and 8.5.2). This doesn't affect the library module tests.

## Troubleshooting

### Tests Failing After Dependency Updates
Run `./gradlew --stop` to stop all Gradle daemons, then re-run tests.

### Snapshot Mismatches
If legitimate UI changes were made, record new golden images:
```bash
./gradlew recordPaparazziDebug
```

### Build Failures
Ensure you're using the correct SDK version (34) and the specified plugin versions.

## Best Practices

1. **Always record golden images** after intentional UI changes
2. **Review snapshot diffs** carefully before accepting changes
3. **Keep snapshots small** - test individual components rather than entire screens when possible
4. **Use descriptive test names** to make snapshot files easy to identify
13 changes: 11 additions & 2 deletions app/build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.compose'
id 'app.cash.paparazzi'
}

android {
defaultConfig {
applicationId "com.simonsickle.composedbarcodes"
compileSdk = 36
compileSdk = 34
minSdkVersion 26
targetSdkVersion 36
targetSdkVersion 34
versionCode 2
versionName "1.1.0"

Expand All@@ -27,6 +29,10 @@ android {
targetCompatibility JavaVersion.VERSION_19
}

kotlinOptions {
jvmTarget = '19'
}

buildFeatures {
compose true
}
Expand All@@ -50,4 +56,7 @@ dependencies {

// Nav components
implementation(libs.androidx.navigation.compose)

// Testing
testImplementation(libs.junit)
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.simonsickle.compose.barcodes.Barcode
import com.simonsickle.compose.barcodes.SynchronousBarcode
import com.simonsickle.compose.barcodes.BarcodeType

@Composable
Expand All@@ -30,7 +30,7 @@ fun GenericBarcodeExample(
.fillMaxSize()
) {
if (barcodeType.isValueValid(value)) {
Barcode(
SynchronousBarcode(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.width(300.dp)
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
package com.simonsickle.composedbarcodes.examples

import app.cash.paparazzi.DeviceConfig
import app.cash.paparazzi.Paparazzi
import org.junit.Rule
import org.junit.Test

/**
* Paparazzi snapshot tests for all barcode preview composables.
* These tests ensure that the UI renders correctly and catches any visual regressions.
*/
class BarcodePreviewSnapshotTest {

@get:Rule
val paparazzi = Paparazzi(
deviceConfig = DeviceConfig.PIXEL_5,
showSystemUi = false
)

@Test
fun qrCodePreview() {
paparazzi.snapshot {
Qr()
}
}

@Test
fun aztecPreview() {
paparazzi.snapshot {
Aztec()
}
}

@Test
fun code39Preview() {
paparazzi.snapshot {
Code39()
}
}

@Test
fun code93Preview() {
paparazzi.snapshot {
Code93()
}
}

@Test
fun code128Preview() {
paparazzi.snapshot {
Code128()
}
}

@Test
fun codabarPreview() {
paparazzi.snapshot {
Codabar()
}
}

@Test
fun dataMatrixPreview() {
paparazzi.snapshot {
DataMatrix()
}
}

@Test
fun ean8Preview() {
paparazzi.snapshot {
Ean8()
}
}

@Test
fun ean13Preview() {
paparazzi.snapshot {
Ean13()
}
}

@Test
fun itfPreview() {
paparazzi.snapshot {
Itf()
}
}

@Test
fun pdf417Preview() {
paparazzi.snapshot {
Pdf417()
}
}

@Test
fun upcAPreview() {
paparazzi.snapshot {
UpcA()
}
}

@Test
fun upcEPreview() {
paparazzi.snapshot {
UpcE()
}
}
}
8 changes: 5 additions & 3 deletions build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
plugins {
id("com.android.application") version '9.1.0' apply false
id("com.android.library") version '9.1.0' apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.3.10" apply false
id("com.android.application") version '8.5.2' apply false
id("com.android.library") version '8.5.2' apply false
id("org.jetbrains.kotlin.android") version "2.0.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" apply false
id("app.cash.paparazzi") version "1.3.4" apply false
id("io.github.gradle-nexus.publish-plugin") version "2.0.0"
}

Expand Down
15 changes: 12 additions & 3 deletions composed-barcodes/build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
plugins {
id "com.android.library"
id "org.jetbrains.kotlin.android"
id "org.jetbrains.kotlin.plugin.compose"
id "app.cash.paparazzi"
}

ext {
Expand All@@ -13,7 +15,7 @@ android {
namespace "com.simonsickle.compose"

defaultConfig {
compileSdk = 36
compileSdk = 34
minSdkVersion 21
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
Expand All@@ -35,6 +37,10 @@ android {
targetCompatibility JavaVersion.VERSION_19
}

kotlinOptions {
jvmTarget = '19'
}

buildFeatures {
compose true
}
Expand All@@ -50,7 +56,7 @@ android {

testOptions {
animationsDisabled true
targetSdk 36
targetSdk 34
}

publishing {
Expand All@@ -60,7 +66,7 @@ android {
}
}
lint {
targetSdk 36
targetSdk 34
}
}

Expand All@@ -75,6 +81,9 @@ dependencies {
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.ui.tooling.preview)
debugImplementation(libs.androidx.compose.ui.tooling)

// Testing
testImplementation(libs.junit)
}

apply from: "${rootDir}/scripts/publish-module.gradle"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add Paparazzi snapshot testing for UI tests by firebender-bot[bot] · Pull Request #20 · simonsickle/ComposedBarcodes · GitHub
Skip to content
Open
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
131 changes: 131 additions & 0 deletions PAPARAZZI_SETUP.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
# Paparazzi UI Snapshot Testing Setup

This project uses [Paparazzi](https://github.com/cashapp/paparazzi) for snapshot testing of Compose UI components.

## Overview

Paparazzi enables fast, deterministic screenshot testing without requiring an Android device or emulator. All UI snapshots are generated during unit tests and stored as golden images.

## Configuration

### Versions
- **Android Gradle Plugin**: 8.5.2
- **Kotlin**: 2.0.0
- **Paparazzi**: 1.3.4
- **Compile SDK**: 34

### Build Configuration

Paparazzi is configured in both `app/build.gradle` and `composed-barcodes/build.gradle`:

```gradle
plugins {
id 'app.cash.paparazzi'
}

dependencies {
testImplementation(libs.junit)
}
```

## Snapshot Tests

### Library Module (`composed-barcodes`)

**Location**: `composed-barcodes/src/test/kotlin/com/simonsickle/compose/barcodes/`

#### Test Files:
- `BarcodeSnapshotTest.kt` - Tests all Barcode component preview variations

#### Preview Composables:
Created in `BarcodePreviews.kt`, covering:
- QR Code (default, small, high-resolution, no-progress variations)
- Code 128, Code 39, EAN-13, UPC-A
- Data Matrix, Aztec, PDF417

### App Module (`app`)

**Location**: `app/src/test/java/com/simonsickle/composedbarcodes/examples/`

#### Test Files:
- `BarcodePreviewSnapshotTest.kt` - Tests all barcode example screens

Covers all 13 barcode types:
QR, Aztec, Code39, Code93, Code128, Codabar, DataMatrix, EAN8, EAN13, ITF, PDF417, UPC-A, UPC-E

## Running Tests

### Record Golden Images
```bash
# Record all snapshots
./gradlew recordPaparazziDebug

# Record for specific module
./gradlew composed-barcodes:recordPaparazziDebug
./gradlew app:recordPaparazziDebug
```

### Verify Snapshots
```bash
# Verify all snapshots match golden images
./gradlew verifyPaparazziDebug

# Verify for specific module
./gradlew composed-barcodes:verifyPaparazziDebug
./gradlew app:verifyPaparazziDebug
```

### View Reports
After running tests, view the generated report at:
- Library: `composed-barcodes/build/reports/paparazzi/debug/index.html`
- App: `app/build/reports/paparazzi/debug/index.html`

## CI Integration

The GitHub Actions workflow (`.github/workflows/unit-tests.yml`) runs Paparazzi tests automatically on push and pull requests:

```yaml
- name: Run Paparazzi snapshot tests - Library
run: ./gradlew composed-barcodes:verifyPaparazziDebug --stacktrace

- name: Run Paparazzi snapshot tests - App
run: ./gradlew app:verifyPaparazziDebug --stacktrace
```

## Implementation Details

### Synchronous Barcode for Testing

To ensure snapshots capture actual barcodes instead of loading states, the library provides a `SynchronousBarcode` composable specifically for preview and testing purposes. This version generates barcodes synchronously during composition, making it ideal for Paparazzi snapshot tests.

- **Production use**: Use the async `Barcode` composable with loading indicator support
- **Testing/Previews**: Use `SynchronousBarcode` for reliable snapshot testing

All preview composables in `BarcodePreviews.kt` use `SynchronousBarcode` to ensure accurate snapshot rendering.

**Important**: When using `SynchronousBarcode`, ensure the `width` and `height` parameters match the modifier dimensions to prevent barcode distortion. This maintains proper aspect ratios for each barcode type (e.g., square for QR codes and Data Matrix, wide rectangles for Code 128, EAN-13, and UPC-A).

## Known Limitations

1. **App Module**: There's a known Gradle dependency version mismatch in the app module (requires compileSdk 35 and AGP 8.6.0+, currently using 34 and 8.5.2). This doesn't affect the library module tests.

## Troubleshooting

### Tests Failing After Dependency Updates
Run `./gradlew --stop` to stop all Gradle daemons, then re-run tests.

### Snapshot Mismatches
If legitimate UI changes were made, record new golden images:
```bash
./gradlew recordPaparazziDebug
```

### Build Failures
Ensure you're using the correct SDK version (34) and the specified plugin versions.

## Best Practices

1. **Always record golden images** after intentional UI changes
2. **Review snapshot diffs** carefully before accepting changes
3. **Keep snapshots small** - test individual components rather than entire screens when possible
4. **Use descriptive test names** to make snapshot files easy to identify
13 changes: 11 additions & 2 deletions app/build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.compose'
id 'app.cash.paparazzi'
}

android {
defaultConfig {
applicationId "com.simonsickle.composedbarcodes"
compileSdk = 36
compileSdk = 34
minSdkVersion 26
targetSdkVersion 36
targetSdkVersion 34
versionCode 2
versionName "1.1.0"

Expand All@@ -27,6 +29,10 @@ android {
targetCompatibility JavaVersion.VERSION_19
}

kotlinOptions {
jvmTarget = '19'
}

buildFeatures {
compose true
}
Expand All@@ -50,4 +56,7 @@ dependencies {

// Nav components
implementation(libs.androidx.navigation.compose)

// Testing
testImplementation(libs.junit)
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.simonsickle.compose.barcodes.Barcode
import com.simonsickle.compose.barcodes.SynchronousBarcode
import com.simonsickle.compose.barcodes.BarcodeType

@Composable
Expand All@@ -30,7 +30,7 @@ fun GenericBarcodeExample(
.fillMaxSize()
) {
if (barcodeType.isValueValid(value)) {
Barcode(
SynchronousBarcode(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.width(300.dp)
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
package com.simonsickle.composedbarcodes.examples

import app.cash.paparazzi.DeviceConfig
import app.cash.paparazzi.Paparazzi
import org.junit.Rule
import org.junit.Test

/**
* Paparazzi snapshot tests for all barcode preview composables.
* These tests ensure that the UI renders correctly and catches any visual regressions.
*/
class BarcodePreviewSnapshotTest {

@get:Rule
val paparazzi = Paparazzi(
deviceConfig = DeviceConfig.PIXEL_5,
showSystemUi = false
)

@Test
fun qrCodePreview() {
paparazzi.snapshot {
Qr()
}
}

@Test
fun aztecPreview() {
paparazzi.snapshot {
Aztec()
}
}

@Test
fun code39Preview() {
paparazzi.snapshot {
Code39()
}
}

@Test
fun code93Preview() {
paparazzi.snapshot {
Code93()
}
}

@Test
fun code128Preview() {
paparazzi.snapshot {
Code128()
}
}

@Test
fun codabarPreview() {
paparazzi.snapshot {
Codabar()
}
}

@Test
fun dataMatrixPreview() {
paparazzi.snapshot {
DataMatrix()
}
}

@Test
fun ean8Preview() {
paparazzi.snapshot {
Ean8()
}
}

@Test
fun ean13Preview() {
paparazzi.snapshot {
Ean13()
}
}

@Test
fun itfPreview() {
paparazzi.snapshot {
Itf()
}
}

@Test
fun pdf417Preview() {
paparazzi.snapshot {
Pdf417()
}
}

@Test
fun upcAPreview() {
paparazzi.snapshot {
UpcA()
}
}

@Test
fun upcEPreview() {
paparazzi.snapshot {
UpcE()
}
}
}
8 changes: 5 additions & 3 deletions build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
plugins {
id("com.android.application") version '9.1.0' apply false
id("com.android.library") version '9.1.0' apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.3.10" apply false
id("com.android.application") version '8.5.2' apply false
id("com.android.library") version '8.5.2' apply false
id("org.jetbrains.kotlin.android") version "2.0.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" apply false
id("app.cash.paparazzi") version "1.3.4" apply false
id("io.github.gradle-nexus.publish-plugin") version "2.0.0"
}

Expand Down
15 changes: 12 additions & 3 deletions composed-barcodes/build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
plugins {
id "com.android.library"
id "org.jetbrains.kotlin.android"
id "org.jetbrains.kotlin.plugin.compose"
id "app.cash.paparazzi"
}

ext {
Expand All@@ -13,7 +15,7 @@ android {
namespace "com.simonsickle.compose"

defaultConfig {
compileSdk = 36
compileSdk = 34
minSdkVersion 21
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
Expand All@@ -35,6 +37,10 @@ android {
targetCompatibility JavaVersion.VERSION_19
}

kotlinOptions {
jvmTarget = '19'
}

buildFeatures {
compose true
}
Expand All@@ -50,7 +56,7 @@ android {

testOptions {
animationsDisabled true
targetSdk 36
targetSdk 34
}

publishing {
Expand All@@ -60,7 +66,7 @@ android {
}
}
lint {
targetSdk 36
targetSdk 34
}
}

Expand All@@ -75,6 +81,9 @@ dependencies {
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.ui.tooling.preview)
debugImplementation(libs.androidx.compose.ui.tooling)

// Testing
testImplementation(libs.junit)
}

apply from: "${rootDir}/scripts/publish-module.gradle"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add Paparazzi snapshot testing for UI tests by firebender-bot[bot] · Pull Request #20 · simonsickle/ComposedBarcodes · GitHub
Skip to content
Open
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
131 changes: 131 additions & 0 deletions PAPARAZZI_SETUP.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
# Paparazzi UI Snapshot Testing Setup

This project uses [Paparazzi](https://github.com/cashapp/paparazzi) for snapshot testing of Compose UI components.

## Overview

Paparazzi enables fast, deterministic screenshot testing without requiring an Android device or emulator. All UI snapshots are generated during unit tests and stored as golden images.

## Configuration

### Versions
- **Android Gradle Plugin**: 8.5.2
- **Kotlin**: 2.0.0
- **Paparazzi**: 1.3.4
- **Compile SDK**: 34

### Build Configuration

Paparazzi is configured in both `app/build.gradle` and `composed-barcodes/build.gradle`:

```gradle
plugins {
id 'app.cash.paparazzi'
}

dependencies {
testImplementation(libs.junit)
}
```

## Snapshot Tests

### Library Module (`composed-barcodes`)

**Location**: `composed-barcodes/src/test/kotlin/com/simonsickle/compose/barcodes/`

#### Test Files:
- `BarcodeSnapshotTest.kt` - Tests all Barcode component preview variations

#### Preview Composables:
Created in `BarcodePreviews.kt`, covering:
- QR Code (default, small, high-resolution, no-progress variations)
- Code 128, Code 39, EAN-13, UPC-A
- Data Matrix, Aztec, PDF417

### App Module (`app`)

**Location**: `app/src/test/java/com/simonsickle/composedbarcodes/examples/`

#### Test Files:
- `BarcodePreviewSnapshotTest.kt` - Tests all barcode example screens

Covers all 13 barcode types:
QR, Aztec, Code39, Code93, Code128, Codabar, DataMatrix, EAN8, EAN13, ITF, PDF417, UPC-A, UPC-E

## Running Tests

### Record Golden Images
```bash
# Record all snapshots
./gradlew recordPaparazziDebug

# Record for specific module
./gradlew composed-barcodes:recordPaparazziDebug
./gradlew app:recordPaparazziDebug
```

### Verify Snapshots
```bash
# Verify all snapshots match golden images
./gradlew verifyPaparazziDebug

# Verify for specific module
./gradlew composed-barcodes:verifyPaparazziDebug
./gradlew app:verifyPaparazziDebug
```

### View Reports
After running tests, view the generated report at:
- Library: `composed-barcodes/build/reports/paparazzi/debug/index.html`
- App: `app/build/reports/paparazzi/debug/index.html`

## CI Integration

The GitHub Actions workflow (`.github/workflows/unit-tests.yml`) runs Paparazzi tests automatically on push and pull requests:

```yaml
- name: Run Paparazzi snapshot tests - Library
run: ./gradlew composed-barcodes:verifyPaparazziDebug --stacktrace

- name: Run Paparazzi snapshot tests - App
run: ./gradlew app:verifyPaparazziDebug --stacktrace
```

## Implementation Details

### Synchronous Barcode for Testing

To ensure snapshots capture actual barcodes instead of loading states, the library provides a `SynchronousBarcode` composable specifically for preview and testing purposes. This version generates barcodes synchronously during composition, making it ideal for Paparazzi snapshot tests.

- **Production use**: Use the async `Barcode` composable with loading indicator support
- **Testing/Previews**: Use `SynchronousBarcode` for reliable snapshot testing

All preview composables in `BarcodePreviews.kt` use `SynchronousBarcode` to ensure accurate snapshot rendering.

**Important**: When using `SynchronousBarcode`, ensure the `width` and `height` parameters match the modifier dimensions to prevent barcode distortion. This maintains proper aspect ratios for each barcode type (e.g., square for QR codes and Data Matrix, wide rectangles for Code 128, EAN-13, and UPC-A).

## Known Limitations

1. **App Module**: There's a known Gradle dependency version mismatch in the app module (requires compileSdk 35 and AGP 8.6.0+, currently using 34 and 8.5.2). This doesn't affect the library module tests.

## Troubleshooting

### Tests Failing After Dependency Updates
Run `./gradlew --stop` to stop all Gradle daemons, then re-run tests.

### Snapshot Mismatches
If legitimate UI changes were made, record new golden images:
```bash
./gradlew recordPaparazziDebug
```

### Build Failures
Ensure you're using the correct SDK version (34) and the specified plugin versions.

## Best Practices

1. **Always record golden images** after intentional UI changes
2. **Review snapshot diffs** carefully before accepting changes
3. **Keep snapshots small** - test individual components rather than entire screens when possible
4. **Use descriptive test names** to make snapshot files easy to identify
13 changes: 11 additions & 2 deletions app/build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.compose'
id 'app.cash.paparazzi'
}

android {
defaultConfig {
applicationId "com.simonsickle.composedbarcodes"
compileSdk = 36
compileSdk = 34
minSdkVersion 26
targetSdkVersion 36
targetSdkVersion 34
versionCode 2
versionName "1.1.0"

Expand All@@ -27,6 +29,10 @@ android {
targetCompatibility JavaVersion.VERSION_19
}

kotlinOptions {
jvmTarget = '19'
}

buildFeatures {
compose true
}
Expand All@@ -50,4 +56,7 @@ dependencies {

// Nav components
implementation(libs.androidx.navigation.compose)

// Testing
testImplementation(libs.junit)
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.simonsickle.compose.barcodes.Barcode
import com.simonsickle.compose.barcodes.SynchronousBarcode
import com.simonsickle.compose.barcodes.BarcodeType

@Composable
Expand All@@ -30,7 +30,7 @@ fun GenericBarcodeExample(
.fillMaxSize()
) {
if (barcodeType.isValueValid(value)) {
Barcode(
SynchronousBarcode(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.width(300.dp)
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
package com.simonsickle.composedbarcodes.examples

import app.cash.paparazzi.DeviceConfig
import app.cash.paparazzi.Paparazzi
import org.junit.Rule
import org.junit.Test

/**
* Paparazzi snapshot tests for all barcode preview composables.
* These tests ensure that the UI renders correctly and catches any visual regressions.
*/
class BarcodePreviewSnapshotTest {

@get:Rule
val paparazzi = Paparazzi(
deviceConfig = DeviceConfig.PIXEL_5,
showSystemUi = false
)

@Test
fun qrCodePreview() {
paparazzi.snapshot {
Qr()
}
}

@Test
fun aztecPreview() {
paparazzi.snapshot {
Aztec()
}
}

@Test
fun code39Preview() {
paparazzi.snapshot {
Code39()
}
}

@Test
fun code93Preview() {
paparazzi.snapshot {
Code93()
}
}

@Test
fun code128Preview() {
paparazzi.snapshot {
Code128()
}
}

@Test
fun codabarPreview() {
paparazzi.snapshot {
Codabar()
}
}

@Test
fun dataMatrixPreview() {
paparazzi.snapshot {
DataMatrix()
}
}

@Test
fun ean8Preview() {
paparazzi.snapshot {
Ean8()
}
}

@Test
fun ean13Preview() {
paparazzi.snapshot {
Ean13()
}
}

@Test
fun itfPreview() {
paparazzi.snapshot {
Itf()
}
}

@Test
fun pdf417Preview() {
paparazzi.snapshot {
Pdf417()
}
}

@Test
fun upcAPreview() {
paparazzi.snapshot {
UpcA()
}
}

@Test
fun upcEPreview() {
paparazzi.snapshot {
UpcE()
}
}
}
8 changes: 5 additions & 3 deletions build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
plugins {
id("com.android.application") version '9.1.0' apply false
id("com.android.library") version '9.1.0' apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.3.10" apply false
id("com.android.application") version '8.5.2' apply false
id("com.android.library") version '8.5.2' apply false
id("org.jetbrains.kotlin.android") version "2.0.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" apply false
id("app.cash.paparazzi") version "1.3.4" apply false
id("io.github.gradle-nexus.publish-plugin") version "2.0.0"
}

Expand Down
15 changes: 12 additions & 3 deletions composed-barcodes/build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
plugins {
id "com.android.library"
id "org.jetbrains.kotlin.android"
id "org.jetbrains.kotlin.plugin.compose"
id "app.cash.paparazzi"
}

ext {
Expand All@@ -13,7 +15,7 @@ android {
namespace "com.simonsickle.compose"

defaultConfig {
compileSdk = 36
compileSdk = 34
minSdkVersion 21
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
Expand All@@ -35,6 +37,10 @@ android {
targetCompatibility JavaVersion.VERSION_19
}

kotlinOptions {
jvmTarget = '19'
}

buildFeatures {
compose true
}
Expand All@@ -50,7 +56,7 @@ android {

testOptions {
animationsDisabled true
targetSdk 36
targetSdk 34
}

publishing {
Expand All@@ -60,7 +66,7 @@ android {
}
}
lint {
targetSdk 36
targetSdk 34
}
}

Expand All@@ -75,6 +81,9 @@ dependencies {
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.ui.tooling.preview)
debugImplementation(libs.androidx.compose.ui.tooling)

// Testing
testImplementation(libs.junit)
}

apply from: "${rootDir}/scripts/publish-module.gradle"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Add Paparazzi snapshot testing for UI tests by firebender-bot[bot] · Pull Request #20 · simonsickle/ComposedBarcodes · GitHub
Skip to content
Open
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
131 changes: 131 additions & 0 deletions PAPARAZZI_SETUP.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
# Paparazzi UI Snapshot Testing Setup

This project uses [Paparazzi](https://github.com/cashapp/paparazzi) for snapshot testing of Compose UI components.

## Overview

Paparazzi enables fast, deterministic screenshot testing without requiring an Android device or emulator. All UI snapshots are generated during unit tests and stored as golden images.

## Configuration

### Versions
- **Android Gradle Plugin**: 8.5.2
- **Kotlin**: 2.0.0
- **Paparazzi**: 1.3.4
- **Compile SDK**: 34

### Build Configuration

Paparazzi is configured in both `app/build.gradle` and `composed-barcodes/build.gradle`:

```gradle
plugins {
id 'app.cash.paparazzi'
}

dependencies {
testImplementation(libs.junit)
}
```

## Snapshot Tests

### Library Module (`composed-barcodes`)

**Location**: `composed-barcodes/src/test/kotlin/com/simonsickle/compose/barcodes/`

#### Test Files:
- `BarcodeSnapshotTest.kt` - Tests all Barcode component preview variations

#### Preview Composables:
Created in `BarcodePreviews.kt`, covering:
- QR Code (default, small, high-resolution, no-progress variations)
- Code 128, Code 39, EAN-13, UPC-A
- Data Matrix, Aztec, PDF417

### App Module (`app`)

**Location**: `app/src/test/java/com/simonsickle/composedbarcodes/examples/`

#### Test Files:
- `BarcodePreviewSnapshotTest.kt` - Tests all barcode example screens

Covers all 13 barcode types:
QR, Aztec, Code39, Code93, Code128, Codabar, DataMatrix, EAN8, EAN13, ITF, PDF417, UPC-A, UPC-E

## Running Tests

### Record Golden Images
```bash
# Record all snapshots
./gradlew recordPaparazziDebug

# Record for specific module
./gradlew composed-barcodes:recordPaparazziDebug
./gradlew app:recordPaparazziDebug
```

### Verify Snapshots
```bash
# Verify all snapshots match golden images
./gradlew verifyPaparazziDebug

# Verify for specific module
./gradlew composed-barcodes:verifyPaparazziDebug
./gradlew app:verifyPaparazziDebug
```

### View Reports
After running tests, view the generated report at:
- Library: `composed-barcodes/build/reports/paparazzi/debug/index.html`
- App: `app/build/reports/paparazzi/debug/index.html`

## CI Integration

The GitHub Actions workflow (`.github/workflows/unit-tests.yml`) runs Paparazzi tests automatically on push and pull requests:

```yaml
- name: Run Paparazzi snapshot tests - Library
run: ./gradlew composed-barcodes:verifyPaparazziDebug --stacktrace

- name: Run Paparazzi snapshot tests - App
run: ./gradlew app:verifyPaparazziDebug --stacktrace
```

## Implementation Details

### Synchronous Barcode for Testing

To ensure snapshots capture actual barcodes instead of loading states, the library provides a `SynchronousBarcode` composable specifically for preview and testing purposes. This version generates barcodes synchronously during composition, making it ideal for Paparazzi snapshot tests.

- **Production use**: Use the async `Barcode` composable with loading indicator support
- **Testing/Previews**: Use `SynchronousBarcode` for reliable snapshot testing

All preview composables in `BarcodePreviews.kt` use `SynchronousBarcode` to ensure accurate snapshot rendering.

**Important**: When using `SynchronousBarcode`, ensure the `width` and `height` parameters match the modifier dimensions to prevent barcode distortion. This maintains proper aspect ratios for each barcode type (e.g., square for QR codes and Data Matrix, wide rectangles for Code 128, EAN-13, and UPC-A).

## Known Limitations

1. **App Module**: There's a known Gradle dependency version mismatch in the app module (requires compileSdk 35 and AGP 8.6.0+, currently using 34 and 8.5.2). This doesn't affect the library module tests.

## Troubleshooting

### Tests Failing After Dependency Updates
Run `./gradlew --stop` to stop all Gradle daemons, then re-run tests.

### Snapshot Mismatches
If legitimate UI changes were made, record new golden images:
```bash
./gradlew recordPaparazziDebug
```

### Build Failures
Ensure you're using the correct SDK version (34) and the specified plugin versions.

## Best Practices

1. **Always record golden images** after intentional UI changes
2. **Review snapshot diffs** carefully before accepting changes
3. **Keep snapshots small** - test individual components rather than entire screens when possible
4. **Use descriptive test names** to make snapshot files easy to identify
13 changes: 11 additions & 2 deletions app/build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.compose'
id 'app.cash.paparazzi'
}

android {
defaultConfig {
applicationId "com.simonsickle.composedbarcodes"
compileSdk = 36
compileSdk = 34
minSdkVersion 26
targetSdkVersion 36
targetSdkVersion 34
versionCode 2
versionName "1.1.0"

Expand All@@ -27,6 +29,10 @@ android {
targetCompatibility JavaVersion.VERSION_19
}

kotlinOptions {
jvmTarget = '19'
}

buildFeatures {
compose true
}
Expand All@@ -50,4 +56,7 @@ dependencies {

// Nav components
implementation(libs.androidx.navigation.compose)

// Testing
testImplementation(libs.junit)
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.simonsickle.compose.barcodes.Barcode
import com.simonsickle.compose.barcodes.SynchronousBarcode
import com.simonsickle.compose.barcodes.BarcodeType

@Composable
Expand All@@ -30,7 +30,7 @@ fun GenericBarcodeExample(
.fillMaxSize()
) {
if (barcodeType.isValueValid(value)) {
Barcode(
SynchronousBarcode(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.width(300.dp)
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
package com.simonsickle.composedbarcodes.examples

import app.cash.paparazzi.DeviceConfig
import app.cash.paparazzi.Paparazzi
import org.junit.Rule
import org.junit.Test

/**
* Paparazzi snapshot tests for all barcode preview composables.
* These tests ensure that the UI renders correctly and catches any visual regressions.
*/
class BarcodePreviewSnapshotTest {

@get:Rule
val paparazzi = Paparazzi(
deviceConfig = DeviceConfig.PIXEL_5,
showSystemUi = false
)

@Test
fun qrCodePreview() {
paparazzi.snapshot {
Qr()
}
}

@Test
fun aztecPreview() {
paparazzi.snapshot {
Aztec()
}
}

@Test
fun code39Preview() {
paparazzi.snapshot {
Code39()
}
}

@Test
fun code93Preview() {
paparazzi.snapshot {
Code93()
}
}

@Test
fun code128Preview() {
paparazzi.snapshot {
Code128()
}
}

@Test
fun codabarPreview() {
paparazzi.snapshot {
Codabar()
}
}

@Test
fun dataMatrixPreview() {
paparazzi.snapshot {
DataMatrix()
}
}

@Test
fun ean8Preview() {
paparazzi.snapshot {
Ean8()
}
}

@Test
fun ean13Preview() {
paparazzi.snapshot {
Ean13()
}
}

@Test
fun itfPreview() {
paparazzi.snapshot {
Itf()
}
}

@Test
fun pdf417Preview() {
paparazzi.snapshot {
Pdf417()
}
}

@Test
fun upcAPreview() {
paparazzi.snapshot {
UpcA()
}
}

@Test
fun upcEPreview() {
paparazzi.snapshot {
UpcE()
}
}
}
8 changes: 5 additions & 3 deletions build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
plugins {
id("com.android.application") version '9.1.0' apply false
id("com.android.library") version '9.1.0' apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.3.10" apply false
id("com.android.application") version '8.5.2' apply false
id("com.android.library") version '8.5.2' apply false
id("org.jetbrains.kotlin.android") version "2.0.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" apply false
id("app.cash.paparazzi") version "1.3.4" apply false
id("io.github.gradle-nexus.publish-plugin") version "2.0.0"
}

Expand Down
15 changes: 12 additions & 3 deletions composed-barcodes/build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
plugins {
id "com.android.library"
id "org.jetbrains.kotlin.android"
id "org.jetbrains.kotlin.plugin.compose"
id "app.cash.paparazzi"
}

ext {
Expand All@@ -13,7 +15,7 @@ android {
namespace "com.simonsickle.compose"

defaultConfig {
compileSdk = 36
compileSdk = 34
minSdkVersion 21
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
Expand All@@ -35,6 +37,10 @@ android {
targetCompatibility JavaVersion.VERSION_19
}

kotlinOptions {
jvmTarget = '19'
}

buildFeatures {
compose true
}
Expand All@@ -50,7 +56,7 @@ android {

testOptions {
animationsDisabled true
targetSdk 36
targetSdk 34
}

publishing {
Expand All@@ -60,7 +66,7 @@ android {
}
}
lint {
targetSdk 36
targetSdk 34
}
}

Expand All@@ -75,6 +81,9 @@ dependencies {
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.ui.tooling.preview)
debugImplementation(libs.androidx.compose.ui.tooling)

// Testing
testImplementation(libs.junit)
}

apply from: "${rootDir}/scripts/publish-module.gradle"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add Paparazzi snapshot testing for UI tests by firebender-bot[bot] · Pull Request #20 · simonsickle/ComposedBarcodes · GitHub
Skip to content
Open
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
131 changes: 131 additions & 0 deletions PAPARAZZI_SETUP.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
# Paparazzi UI Snapshot Testing Setup

This project uses [Paparazzi](https://github.com/cashapp/paparazzi) for snapshot testing of Compose UI components.

## Overview

Paparazzi enables fast, deterministic screenshot testing without requiring an Android device or emulator. All UI snapshots are generated during unit tests and stored as golden images.

## Configuration

### Versions
- **Android Gradle Plugin**: 8.5.2
- **Kotlin**: 2.0.0
- **Paparazzi**: 1.3.4
- **Compile SDK**: 34

### Build Configuration

Paparazzi is configured in both `app/build.gradle` and `composed-barcodes/build.gradle`:

```gradle
plugins {
id 'app.cash.paparazzi'
}

dependencies {
testImplementation(libs.junit)
}
```

## Snapshot Tests

### Library Module (`composed-barcodes`)

**Location**: `composed-barcodes/src/test/kotlin/com/simonsickle/compose/barcodes/`

#### Test Files:
- `BarcodeSnapshotTest.kt` - Tests all Barcode component preview variations

#### Preview Composables:
Created in `BarcodePreviews.kt`, covering:
- QR Code (default, small, high-resolution, no-progress variations)
- Code 128, Code 39, EAN-13, UPC-A
- Data Matrix, Aztec, PDF417

### App Module (`app`)

**Location**: `app/src/test/java/com/simonsickle/composedbarcodes/examples/`

#### Test Files:
- `BarcodePreviewSnapshotTest.kt` - Tests all barcode example screens

Covers all 13 barcode types:
QR, Aztec, Code39, Code93, Code128, Codabar, DataMatrix, EAN8, EAN13, ITF, PDF417, UPC-A, UPC-E

## Running Tests

### Record Golden Images
```bash
# Record all snapshots
./gradlew recordPaparazziDebug

# Record for specific module
./gradlew composed-barcodes:recordPaparazziDebug
./gradlew app:recordPaparazziDebug
```

### Verify Snapshots
```bash
# Verify all snapshots match golden images
./gradlew verifyPaparazziDebug

# Verify for specific module
./gradlew composed-barcodes:verifyPaparazziDebug
./gradlew app:verifyPaparazziDebug
```

### View Reports
After running tests, view the generated report at:
- Library: `composed-barcodes/build/reports/paparazzi/debug/index.html`
- App: `app/build/reports/paparazzi/debug/index.html`

## CI Integration

The GitHub Actions workflow (`.github/workflows/unit-tests.yml`) runs Paparazzi tests automatically on push and pull requests:

```yaml
- name: Run Paparazzi snapshot tests - Library
run: ./gradlew composed-barcodes:verifyPaparazziDebug --stacktrace

- name: Run Paparazzi snapshot tests - App
run: ./gradlew app:verifyPaparazziDebug --stacktrace
```

## Implementation Details

### Synchronous Barcode for Testing

To ensure snapshots capture actual barcodes instead of loading states, the library provides a `SynchronousBarcode` composable specifically for preview and testing purposes. This version generates barcodes synchronously during composition, making it ideal for Paparazzi snapshot tests.

- **Production use**: Use the async `Barcode` composable with loading indicator support
- **Testing/Previews**: Use `SynchronousBarcode` for reliable snapshot testing

All preview composables in `BarcodePreviews.kt` use `SynchronousBarcode` to ensure accurate snapshot rendering.

**Important**: When using `SynchronousBarcode`, ensure the `width` and `height` parameters match the modifier dimensions to prevent barcode distortion. This maintains proper aspect ratios for each barcode type (e.g., square for QR codes and Data Matrix, wide rectangles for Code 128, EAN-13, and UPC-A).

## Known Limitations

1. **App Module**: There's a known Gradle dependency version mismatch in the app module (requires compileSdk 35 and AGP 8.6.0+, currently using 34 and 8.5.2). This doesn't affect the library module tests.

## Troubleshooting

### Tests Failing After Dependency Updates
Run `./gradlew --stop` to stop all Gradle daemons, then re-run tests.

### Snapshot Mismatches
If legitimate UI changes were made, record new golden images:
```bash
./gradlew recordPaparazziDebug
```

### Build Failures
Ensure you're using the correct SDK version (34) and the specified plugin versions.

## Best Practices

1. **Always record golden images** after intentional UI changes
2. **Review snapshot diffs** carefully before accepting changes
3. **Keep snapshots small** - test individual components rather than entire screens when possible
4. **Use descriptive test names** to make snapshot files easy to identify
13 changes: 11 additions & 2 deletions app/build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.compose'
id 'app.cash.paparazzi'
}

android {
defaultConfig {
applicationId "com.simonsickle.composedbarcodes"
compileSdk = 36
compileSdk = 34
minSdkVersion 26
targetSdkVersion 36
targetSdkVersion 34
versionCode 2
versionName "1.1.0"

Expand All@@ -27,6 +29,10 @@ android {
targetCompatibility JavaVersion.VERSION_19
}

kotlinOptions {
jvmTarget = '19'
}

buildFeatures {
compose true
}
Expand All@@ -50,4 +56,7 @@ dependencies {

// Nav components
implementation(libs.androidx.navigation.compose)

// Testing
testImplementation(libs.junit)
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.simonsickle.compose.barcodes.Barcode
import com.simonsickle.compose.barcodes.SynchronousBarcode
import com.simonsickle.compose.barcodes.BarcodeType

@Composable
Expand All@@ -30,7 +30,7 @@ fun GenericBarcodeExample(
.fillMaxSize()
) {
if (barcodeType.isValueValid(value)) {
Barcode(
SynchronousBarcode(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.width(300.dp)
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
package com.simonsickle.composedbarcodes.examples

import app.cash.paparazzi.DeviceConfig
import app.cash.paparazzi.Paparazzi
import org.junit.Rule
import org.junit.Test

/**
* Paparazzi snapshot tests for all barcode preview composables.
* These tests ensure that the UI renders correctly and catches any visual regressions.
*/
class BarcodePreviewSnapshotTest {

@get:Rule
val paparazzi = Paparazzi(
deviceConfig = DeviceConfig.PIXEL_5,
showSystemUi = false
)

@Test
fun qrCodePreview() {
paparazzi.snapshot {
Qr()
}
}

@Test
fun aztecPreview() {
paparazzi.snapshot {
Aztec()
}
}

@Test
fun code39Preview() {
paparazzi.snapshot {
Code39()
}
}

@Test
fun code93Preview() {
paparazzi.snapshot {
Code93()
}
}

@Test
fun code128Preview() {
paparazzi.snapshot {
Code128()
}
}

@Test
fun codabarPreview() {
paparazzi.snapshot {
Codabar()
}
}

@Test
fun dataMatrixPreview() {
paparazzi.snapshot {
DataMatrix()
}
}

@Test
fun ean8Preview() {
paparazzi.snapshot {
Ean8()
}
}

@Test
fun ean13Preview() {
paparazzi.snapshot {
Ean13()
}
}

@Test
fun itfPreview() {
paparazzi.snapshot {
Itf()
}
}

@Test
fun pdf417Preview() {
paparazzi.snapshot {
Pdf417()
}
}

@Test
fun upcAPreview() {
paparazzi.snapshot {
UpcA()
}
}

@Test
fun upcEPreview() {
paparazzi.snapshot {
UpcE()
}
}
}
8 changes: 5 additions & 3 deletions build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
plugins {
id("com.android.application") version '9.1.0' apply false
id("com.android.library") version '9.1.0' apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.3.10" apply false
id("com.android.application") version '8.5.2' apply false
id("com.android.library") version '8.5.2' apply false
id("org.jetbrains.kotlin.android") version "2.0.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" apply false
id("app.cash.paparazzi") version "1.3.4" apply false
id("io.github.gradle-nexus.publish-plugin") version "2.0.0"
}

Expand Down
15 changes: 12 additions & 3 deletions composed-barcodes/build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
plugins {
id "com.android.library"
id "org.jetbrains.kotlin.android"
id "org.jetbrains.kotlin.plugin.compose"
id "app.cash.paparazzi"
}

ext {
Expand All@@ -13,7 +15,7 @@ android {
namespace "com.simonsickle.compose"

defaultConfig {
compileSdk = 36
compileSdk = 34
minSdkVersion 21
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
Expand All@@ -35,6 +37,10 @@ android {
targetCompatibility JavaVersion.VERSION_19
}

kotlinOptions {
jvmTarget = '19'
}

buildFeatures {
compose true
}
Expand All@@ -50,7 +56,7 @@ android {

testOptions {
animationsDisabled true
targetSdk 36
targetSdk 34
}

publishing {
Expand All@@ -60,7 +66,7 @@ android {
}
}
lint {
targetSdk 36
targetSdk 34
}
}

Expand All@@ -75,6 +81,9 @@ dependencies {
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.ui.tooling.preview)
debugImplementation(libs.androidx.compose.ui.tooling)

// Testing
testImplementation(libs.junit)
}

apply from: "${rootDir}/scripts/publish-module.gradle"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add Paparazzi snapshot testing for UI tests by firebender-bot[bot] · Pull Request #20 · simonsickle/ComposedBarcodes · GitHub
Skip to content
Open
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
131 changes: 131 additions & 0 deletions PAPARAZZI_SETUP.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
# Paparazzi UI Snapshot Testing Setup

This project uses [Paparazzi](https://github.com/cashapp/paparazzi) for snapshot testing of Compose UI components.

## Overview

Paparazzi enables fast, deterministic screenshot testing without requiring an Android device or emulator. All UI snapshots are generated during unit tests and stored as golden images.

## Configuration

### Versions
- **Android Gradle Plugin**: 8.5.2
- **Kotlin**: 2.0.0
- **Paparazzi**: 1.3.4
- **Compile SDK**: 34

### Build Configuration

Paparazzi is configured in both `app/build.gradle` and `composed-barcodes/build.gradle`:

```gradle
plugins {
id 'app.cash.paparazzi'
}

dependencies {
testImplementation(libs.junit)
}
```

## Snapshot Tests

### Library Module (`composed-barcodes`)

**Location**: `composed-barcodes/src/test/kotlin/com/simonsickle/compose/barcodes/`

#### Test Files:
- `BarcodeSnapshotTest.kt` - Tests all Barcode component preview variations

#### Preview Composables:
Created in `BarcodePreviews.kt`, covering:
- QR Code (default, small, high-resolution, no-progress variations)
- Code 128, Code 39, EAN-13, UPC-A
- Data Matrix, Aztec, PDF417

### App Module (`app`)

**Location**: `app/src/test/java/com/simonsickle/composedbarcodes/examples/`

#### Test Files:
- `BarcodePreviewSnapshotTest.kt` - Tests all barcode example screens

Covers all 13 barcode types:
QR, Aztec, Code39, Code93, Code128, Codabar, DataMatrix, EAN8, EAN13, ITF, PDF417, UPC-A, UPC-E

## Running Tests

### Record Golden Images
```bash
# Record all snapshots
./gradlew recordPaparazziDebug

# Record for specific module
./gradlew composed-barcodes:recordPaparazziDebug
./gradlew app:recordPaparazziDebug
```

### Verify Snapshots
```bash
# Verify all snapshots match golden images
./gradlew verifyPaparazziDebug

# Verify for specific module
./gradlew composed-barcodes:verifyPaparazziDebug
./gradlew app:verifyPaparazziDebug
```

### View Reports
After running tests, view the generated report at:
- Library: `composed-barcodes/build/reports/paparazzi/debug/index.html`
- App: `app/build/reports/paparazzi/debug/index.html`

## CI Integration

The GitHub Actions workflow (`.github/workflows/unit-tests.yml`) runs Paparazzi tests automatically on push and pull requests:

```yaml
- name: Run Paparazzi snapshot tests - Library
run: ./gradlew composed-barcodes:verifyPaparazziDebug --stacktrace

- name: Run Paparazzi snapshot tests - App
run: ./gradlew app:verifyPaparazziDebug --stacktrace
```

## Implementation Details

### Synchronous Barcode for Testing

To ensure snapshots capture actual barcodes instead of loading states, the library provides a `SynchronousBarcode` composable specifically for preview and testing purposes. This version generates barcodes synchronously during composition, making it ideal for Paparazzi snapshot tests.

- **Production use**: Use the async `Barcode` composable with loading indicator support
- **Testing/Previews**: Use `SynchronousBarcode` for reliable snapshot testing

All preview composables in `BarcodePreviews.kt` use `SynchronousBarcode` to ensure accurate snapshot rendering.

**Important**: When using `SynchronousBarcode`, ensure the `width` and `height` parameters match the modifier dimensions to prevent barcode distortion. This maintains proper aspect ratios for each barcode type (e.g., square for QR codes and Data Matrix, wide rectangles for Code 128, EAN-13, and UPC-A).

## Known Limitations

1. **App Module**: There's a known Gradle dependency version mismatch in the app module (requires compileSdk 35 and AGP 8.6.0+, currently using 34 and 8.5.2). This doesn't affect the library module tests.

## Troubleshooting

### Tests Failing After Dependency Updates
Run `./gradlew --stop` to stop all Gradle daemons, then re-run tests.

### Snapshot Mismatches
If legitimate UI changes were made, record new golden images:
```bash
./gradlew recordPaparazziDebug
```

### Build Failures
Ensure you're using the correct SDK version (34) and the specified plugin versions.

## Best Practices

1. **Always record golden images** after intentional UI changes
2. **Review snapshot diffs** carefully before accepting changes
3. **Keep snapshots small** - test individual components rather than entire screens when possible
4. **Use descriptive test names** to make snapshot files easy to identify
13 changes: 11 additions & 2 deletions app/build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.compose'
id 'app.cash.paparazzi'
}

android {
defaultConfig {
applicationId "com.simonsickle.composedbarcodes"
compileSdk = 36
compileSdk = 34
minSdkVersion 26
targetSdkVersion 36
targetSdkVersion 34
versionCode 2
versionName "1.1.0"

Expand All@@ -27,6 +29,10 @@ android {
targetCompatibility JavaVersion.VERSION_19
}

kotlinOptions {
jvmTarget = '19'
}

buildFeatures {
compose true
}
Expand All@@ -50,4 +56,7 @@ dependencies {

// Nav components
implementation(libs.androidx.navigation.compose)

// Testing
testImplementation(libs.junit)
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.simonsickle.compose.barcodes.Barcode
import com.simonsickle.compose.barcodes.SynchronousBarcode
import com.simonsickle.compose.barcodes.BarcodeType

@Composable
Expand All@@ -30,7 +30,7 @@ fun GenericBarcodeExample(
.fillMaxSize()
) {
if (barcodeType.isValueValid(value)) {
Barcode(
SynchronousBarcode(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.width(300.dp)
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
package com.simonsickle.composedbarcodes.examples

import app.cash.paparazzi.DeviceConfig
import app.cash.paparazzi.Paparazzi
import org.junit.Rule
import org.junit.Test

/**
* Paparazzi snapshot tests for all barcode preview composables.
* These tests ensure that the UI renders correctly and catches any visual regressions.
*/
class BarcodePreviewSnapshotTest {

@get:Rule
val paparazzi = Paparazzi(
deviceConfig = DeviceConfig.PIXEL_5,
showSystemUi = false
)

@Test
fun qrCodePreview() {
paparazzi.snapshot {
Qr()
}
}

@Test
fun aztecPreview() {
paparazzi.snapshot {
Aztec()
}
}

@Test
fun code39Preview() {
paparazzi.snapshot {
Code39()
}
}

@Test
fun code93Preview() {
paparazzi.snapshot {
Code93()
}
}

@Test
fun code128Preview() {
paparazzi.snapshot {
Code128()
}
}

@Test
fun codabarPreview() {
paparazzi.snapshot {
Codabar()
}
}

@Test
fun dataMatrixPreview() {
paparazzi.snapshot {
DataMatrix()
}
}

@Test
fun ean8Preview() {
paparazzi.snapshot {
Ean8()
}
}

@Test
fun ean13Preview() {
paparazzi.snapshot {
Ean13()
}
}

@Test
fun itfPreview() {
paparazzi.snapshot {
Itf()
}
}

@Test
fun pdf417Preview() {
paparazzi.snapshot {
Pdf417()
}
}

@Test
fun upcAPreview() {
paparazzi.snapshot {
UpcA()
}
}

@Test
fun upcEPreview() {
paparazzi.snapshot {
UpcE()
}
}
}
8 changes: 5 additions & 3 deletions build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
plugins {
id("com.android.application") version '9.1.0' apply false
id("com.android.library") version '9.1.0' apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.3.10" apply false
id("com.android.application") version '8.5.2' apply false
id("com.android.library") version '8.5.2' apply false
id("org.jetbrains.kotlin.android") version "2.0.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" apply false
id("app.cash.paparazzi") version "1.3.4" apply false
id("io.github.gradle-nexus.publish-plugin") version "2.0.0"
}

Expand Down
15 changes: 12 additions & 3 deletions composed-barcodes/build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
plugins {
id "com.android.library"
id "org.jetbrains.kotlin.android"
id "org.jetbrains.kotlin.plugin.compose"
id "app.cash.paparazzi"
}

ext {
Expand All@@ -13,7 +15,7 @@ android {
namespace "com.simonsickle.compose"

defaultConfig {
compileSdk = 36
compileSdk = 34
minSdkVersion 21
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
Expand All@@ -35,6 +37,10 @@ android {
targetCompatibility JavaVersion.VERSION_19
}

kotlinOptions {
jvmTarget = '19'
}

buildFeatures {
compose true
}
Expand All@@ -50,7 +56,7 @@ android {

testOptions {
animationsDisabled true
targetSdk 36
targetSdk 34
}

publishing {
Expand All@@ -60,7 +66,7 @@ android {
}
}
lint {
targetSdk 36
targetSdk 34
}
}

Expand All@@ -75,6 +81,9 @@ dependencies {
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.ui.tooling.preview)
debugImplementation(libs.androidx.compose.ui.tooling)

// Testing
testImplementation(libs.junit)
}

apply from: "${rootDir}/scripts/publish-module.gradle"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Add Paparazzi snapshot testing for UI tests by firebender-bot[bot] · Pull Request #20 · simonsickle/ComposedBarcodes · GitHub
Skip to content
Open
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
131 changes: 131 additions & 0 deletions PAPARAZZI_SETUP.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
# Paparazzi UI Snapshot Testing Setup

This project uses [Paparazzi](https://github.com/cashapp/paparazzi) for snapshot testing of Compose UI components.

## Overview

Paparazzi enables fast, deterministic screenshot testing without requiring an Android device or emulator. All UI snapshots are generated during unit tests and stored as golden images.

## Configuration

### Versions
- **Android Gradle Plugin**: 8.5.2
- **Kotlin**: 2.0.0
- **Paparazzi**: 1.3.4
- **Compile SDK**: 34

### Build Configuration

Paparazzi is configured in both `app/build.gradle` and `composed-barcodes/build.gradle`:

```gradle
plugins {
id 'app.cash.paparazzi'
}

dependencies {
testImplementation(libs.junit)
}
```

## Snapshot Tests

### Library Module (`composed-barcodes`)

**Location**: `composed-barcodes/src/test/kotlin/com/simonsickle/compose/barcodes/`

#### Test Files:
- `BarcodeSnapshotTest.kt` - Tests all Barcode component preview variations

#### Preview Composables:
Created in `BarcodePreviews.kt`, covering:
- QR Code (default, small, high-resolution, no-progress variations)
- Code 128, Code 39, EAN-13, UPC-A
- Data Matrix, Aztec, PDF417

### App Module (`app`)

**Location**: `app/src/test/java/com/simonsickle/composedbarcodes/examples/`

#### Test Files:
- `BarcodePreviewSnapshotTest.kt` - Tests all barcode example screens

Covers all 13 barcode types:
QR, Aztec, Code39, Code93, Code128, Codabar, DataMatrix, EAN8, EAN13, ITF, PDF417, UPC-A, UPC-E

## Running Tests

### Record Golden Images
```bash
# Record all snapshots
./gradlew recordPaparazziDebug

# Record for specific module
./gradlew composed-barcodes:recordPaparazziDebug
./gradlew app:recordPaparazziDebug
```

### Verify Snapshots
```bash
# Verify all snapshots match golden images
./gradlew verifyPaparazziDebug

# Verify for specific module
./gradlew composed-barcodes:verifyPaparazziDebug
./gradlew app:verifyPaparazziDebug
```

### View Reports
After running tests, view the generated report at:
- Library: `composed-barcodes/build/reports/paparazzi/debug/index.html`
- App: `app/build/reports/paparazzi/debug/index.html`

## CI Integration

The GitHub Actions workflow (`.github/workflows/unit-tests.yml`) runs Paparazzi tests automatically on push and pull requests:

```yaml
- name: Run Paparazzi snapshot tests - Library
run: ./gradlew composed-barcodes:verifyPaparazziDebug --stacktrace

- name: Run Paparazzi snapshot tests - App
run: ./gradlew app:verifyPaparazziDebug --stacktrace
```

## Implementation Details

### Synchronous Barcode for Testing

To ensure snapshots capture actual barcodes instead of loading states, the library provides a `SynchronousBarcode` composable specifically for preview and testing purposes. This version generates barcodes synchronously during composition, making it ideal for Paparazzi snapshot tests.

- **Production use**: Use the async `Barcode` composable with loading indicator support
- **Testing/Previews**: Use `SynchronousBarcode` for reliable snapshot testing

All preview composables in `BarcodePreviews.kt` use `SynchronousBarcode` to ensure accurate snapshot rendering.

**Important**: When using `SynchronousBarcode`, ensure the `width` and `height` parameters match the modifier dimensions to prevent barcode distortion. This maintains proper aspect ratios for each barcode type (e.g., square for QR codes and Data Matrix, wide rectangles for Code 128, EAN-13, and UPC-A).

## Known Limitations

1. **App Module**: There's a known Gradle dependency version mismatch in the app module (requires compileSdk 35 and AGP 8.6.0+, currently using 34 and 8.5.2). This doesn't affect the library module tests.

## Troubleshooting

### Tests Failing After Dependency Updates
Run `./gradlew --stop` to stop all Gradle daemons, then re-run tests.

### Snapshot Mismatches
If legitimate UI changes were made, record new golden images:
```bash
./gradlew recordPaparazziDebug
```

### Build Failures
Ensure you're using the correct SDK version (34) and the specified plugin versions.

## Best Practices

1. **Always record golden images** after intentional UI changes
2. **Review snapshot diffs** carefully before accepting changes
3. **Keep snapshots small** - test individual components rather than entire screens when possible
4. **Use descriptive test names** to make snapshot files easy to identify
13 changes: 11 additions & 2 deletions app/build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.compose'
id 'app.cash.paparazzi'
}

android {
defaultConfig {
applicationId "com.simonsickle.composedbarcodes"
compileSdk = 36
compileSdk = 34
minSdkVersion 26
targetSdkVersion 36
targetSdkVersion 34
versionCode 2
versionName "1.1.0"

Expand All@@ -27,6 +29,10 @@ android {
targetCompatibility JavaVersion.VERSION_19
}

kotlinOptions {
jvmTarget = '19'
}

buildFeatures {
compose true
}
Expand All@@ -50,4 +56,7 @@ dependencies {

// Nav components
implementation(libs.androidx.navigation.compose)

// Testing
testImplementation(libs.junit)
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.simonsickle.compose.barcodes.Barcode
import com.simonsickle.compose.barcodes.SynchronousBarcode
import com.simonsickle.compose.barcodes.BarcodeType

@Composable
Expand All@@ -30,7 +30,7 @@ fun GenericBarcodeExample(
.fillMaxSize()
) {
if (barcodeType.isValueValid(value)) {
Barcode(
SynchronousBarcode(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.width(300.dp)
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
package com.simonsickle.composedbarcodes.examples

import app.cash.paparazzi.DeviceConfig
import app.cash.paparazzi.Paparazzi
import org.junit.Rule
import org.junit.Test

/**
* Paparazzi snapshot tests for all barcode preview composables.
* These tests ensure that the UI renders correctly and catches any visual regressions.
*/
class BarcodePreviewSnapshotTest {

@get:Rule
val paparazzi = Paparazzi(
deviceConfig = DeviceConfig.PIXEL_5,
showSystemUi = false
)

@Test
fun qrCodePreview() {
paparazzi.snapshot {
Qr()
}
}

@Test
fun aztecPreview() {
paparazzi.snapshot {
Aztec()
}
}

@Test
fun code39Preview() {
paparazzi.snapshot {
Code39()
}
}

@Test
fun code93Preview() {
paparazzi.snapshot {
Code93()
}
}

@Test
fun code128Preview() {
paparazzi.snapshot {
Code128()
}
}

@Test
fun codabarPreview() {
paparazzi.snapshot {
Codabar()
}
}

@Test
fun dataMatrixPreview() {
paparazzi.snapshot {
DataMatrix()
}
}

@Test
fun ean8Preview() {
paparazzi.snapshot {
Ean8()
}
}

@Test
fun ean13Preview() {
paparazzi.snapshot {
Ean13()
}
}

@Test
fun itfPreview() {
paparazzi.snapshot {
Itf()
}
}

@Test
fun pdf417Preview() {
paparazzi.snapshot {
Pdf417()
}
}

@Test
fun upcAPreview() {
paparazzi.snapshot {
UpcA()
}
}

@Test
fun upcEPreview() {
paparazzi.snapshot {
UpcE()
}
}
}
8 changes: 5 additions & 3 deletions build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
plugins {
id("com.android.application") version '9.1.0' apply false
id("com.android.library") version '9.1.0' apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.3.10" apply false
id("com.android.application") version '8.5.2' apply false
id("com.android.library") version '8.5.2' apply false
id("org.jetbrains.kotlin.android") version "2.0.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" apply false
id("app.cash.paparazzi") version "1.3.4" apply false
id("io.github.gradle-nexus.publish-plugin") version "2.0.0"
}

Expand Down
15 changes: 12 additions & 3 deletions composed-barcodes/build.gradle
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
plugins {
id "com.android.library"
id "org.jetbrains.kotlin.android"
id "org.jetbrains.kotlin.plugin.compose"
id "app.cash.paparazzi"
}

ext {
Expand All@@ -13,7 +15,7 @@ android {
namespace "com.simonsickle.compose"

defaultConfig {
compileSdk = 36
compileSdk = 34
minSdkVersion 21
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
Expand All@@ -35,6 +37,10 @@ android {
targetCompatibility JavaVersion.VERSION_19
}

kotlinOptions {
jvmTarget = '19'
}

buildFeatures {
compose true
}
Expand All@@ -50,7 +56,7 @@ android {

testOptions {
animationsDisabled true
targetSdk 36
targetSdk 34
}

publishing {
Expand All@@ -60,7 +66,7 @@ android {
}
}
lint {
targetSdk 36
targetSdk 34
}
}

Expand All@@ -75,6 +81,9 @@ dependencies {
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.ui.tooling.preview)
debugImplementation(libs.androidx.compose.ui.tooling)

// Testing
testImplementation(libs.junit)
}

apply from: "${rootDir}/scripts/publish-module.gradle"
Loading