Skip to content
Merged
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
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
distribution: 'zulu'
java-version: |
17
21
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
Expand Down Expand Up @@ -102,6 +103,7 @@ jobs:
distribution: 'zulu'
java-version: |
17
21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
Expand Down Expand Up @@ -139,6 +141,26 @@ jobs:
if: github.event_name == 'pull_request'
run: ./gradlew apiCheck

- name: Run Tests
run: |
./gradlew :sample:android:verifyPaparazzi
- name: Publish Test Report
if: false && (failure() || success()) && github.event_name == 'pull_request'
uses: mikepenz/action-junit-report@v6
with:
report_paths: '**/sample/android/build/test-results/testDebugUnitTest/TEST-*.xml'
github_token: ${{ secrets.GITHUB_TOKEN }}
fail_on_failure: true
annotate_only: true
detailed_summary: true
Comment on lines +148 to +156
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test report publishing step is disabled with if: false. This means test results won't be published to the PR even if tests fail or succeed. Consider enabling this by changing to if: (failure() || success()) && github.event_name == 'pull_request' to provide better visibility into test results in pull requests.

Copilot uses AI. Check for mistakes.

- name: Archive Test Report
uses: actions/upload-artifact@v6
with:
name: "Test-Artifacts"
path: "sample/android/build/reports/paparazzi/debug/"

- name: Run Lint
if: github.event_name == 'pull_request'
run: ./gradlew lintDebug
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/gradle-dependency-submission.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
distribution: 'zulu'
java-version: |
17
21

- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/static.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ jobs:
distribution: 'zulu'
java-version: |
17
21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
- name: Build Page
Expand Down
1 change: 1 addition & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Collections are marked as stable via the stability config file instead: https://github.com/mikepenz/AboutLibraries/pull/1267
- **Breaking Change**: The already deprecated `generateLibraryDefinitions*` tasks are now removed
- **Breaking Change**: The plugin will now only work for projects that use AGP 7 or newer, with the new variants API via `AndroidComponentsExtension` available
- **Breaking Change**: Due to Paparazzi requiring Java 21 - This project is now also compiled with Java 21

#### v13.2.0

Expand Down
3 changes: 2 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
com.mikepenz.binary-compatibility-validator.enabled=true
com.mikepenz.version-catalog-update.enabled=true
com.mikepenz.compatPatrouille.enabled=false
com.mikepenz.kotlin.version=2.2
com.mikepenz.kotlin.version=2.2
com.mikepenz.java.version=21
2 changes: 2 additions & 0 deletions sample/android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ compose.resources {
packageOfResClass = "com.mikepenz.aboutlibraries.sample.resources"
}

/*
composablePreviewPaparazzi {
enable = true
packages = listOf("com.mikepenz.aboutlibraries.screenshot")
includePrivatePreviews = false
testClassName = "PaparazziTests"
testPackageName = "com.mikepenz.aboutlibraries.screenshot.generated.tests"
}
*/
Comment on lines +39 to +47
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The composablePreviewPaparazzi configuration block is commented out, but the plugin com.mikepenz.convention.composable-preview-scanner.paparazzi-plugin is still applied at line 10. If the plugin is not being used to auto-generate tests (since the test file appears to be manually written), consider whether the plugin should also be removed or if this is intentional for future use.

Copilot uses AI. Check for mistakes.

aboutLibraries {
collect {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
package generated.paparazzi.tests

import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import app.cash.paparazzi.DeviceConfig
import app.cash.paparazzi.HtmlReportWriter
import app.cash.paparazzi.Paparazzi
import app.cash.paparazzi.Snapshot
import app.cash.paparazzi.SnapshotHandler
import app.cash.paparazzi.SnapshotVerifier
import app.cash.paparazzi.TestName
import app.cash.paparazzi.detectEnvironment
import com.android.ide.common.rendering.api.SessionParams
import com.android.resources.Density
import com.android.resources.NightMode
import com.android.resources.ScreenOrientation
import com.android.resources.ScreenRatio
import com.android.resources.ScreenRound
import com.android.resources.ScreenSize
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import sergio.sastre.composable.preview.scanner.android.AndroidComposablePreviewScanner
import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo
import sergio.sastre.composable.preview.scanner.android.device.DevicePreviewInfoParser
import sergio.sastre.composable.preview.scanner.android.device.domain.Device
import sergio.sastre.composable.preview.scanner.android.device.types.DEFAULT
import sergio.sastre.composable.preview.scanner.android.screenshotid.AndroidPreviewScreenshotIdBuilder
import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview
import kotlin.math.ceil

class Dimensions(
val screenWidthInPx: Int,
val screenHeightInPx: Int,
)

object ScreenDimensions {
fun dimensions(
parsedDevice: Device,
widthDp: Int,
heightDp: Int,
): Dimensions {
val conversionFactor = parsedDevice.densityDpi / 160f
val previewWidthInPx = ceil(widthDp * conversionFactor).toInt()
val previewHeightInPx = ceil(heightDp * conversionFactor).toInt()
return Dimensions(
screenHeightInPx = when (heightDp > 0) {
true -> previewHeightInPx
false -> parsedDevice.dimensions.height.toInt()
},
screenWidthInPx = when (widthDp > 0) {
true -> previewWidthInPx
false -> parsedDevice.dimensions.width.toInt()
}
)
}
}

object DeviceConfigBuilder {
fun build(preview: AndroidPreviewInfo): DeviceConfig {
val parsedDevice =
DevicePreviewInfoParser.parse(preview.device)?.inPx() ?: return DeviceConfig()

val dimensions = ScreenDimensions.dimensions(
parsedDevice = parsedDevice,
widthDp = preview.widthDp,
heightDp = preview.heightDp
)

return DeviceConfig(
screenHeight = dimensions.screenHeightInPx,
screenWidth = dimensions.screenWidthInPx,
density = Density(parsedDevice.densityDpi),
xdpi = parsedDevice.densityDpi, // not 100% precise
ydpi = parsedDevice.densityDpi, // not 100% precise
size = ScreenSize.valueOf(parsedDevice.screenSize.name),
ratio = ScreenRatio.valueOf(parsedDevice.screenRatio.name),
screenRound = ScreenRound.valueOf(parsedDevice.shape.name),
orientation = ScreenOrientation.valueOf(parsedDevice.orientation.name),
locale = preview.locale.ifBlank { "en" },
fontScale = preview.fontScale,
nightMode = when (preview.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES) {
true -> NightMode.NIGHT
false -> NightMode.NOTNIGHT
}
)
}
}

// In order to have full control over the screenshot file names
// we need to pass our own SnapshotHandler to the Paparazzi TestRule
private val paparazziTestName =
TestName(packageName = "Paparazzi", className = "Preview", methodName = "Test")

private class PreviewSnapshotVerifier(
maxPercentDifference: Double,
) : SnapshotHandler {
private val snapshotHandler = SnapshotVerifier(
maxPercentDifference = maxPercentDifference
)

override fun newFrameHandler(
snapshot: Snapshot,
frameCount: Int,
fps: Int,
): SnapshotHandler.FrameHandler {
val newSnapshot = Snapshot(
name = snapshot.name,
testName = paparazziTestName,
timestamp = snapshot.timestamp,
tags = snapshot.tags,
file = snapshot.file,
)
return snapshotHandler.newFrameHandler(
snapshot = newSnapshot,
frameCount = frameCount,
fps = fps
)
}

override fun close() {
snapshotHandler.close()
}
}

private class PreviewHtmlReportWriter : SnapshotHandler {
private val snapshotHandler = HtmlReportWriter(maxPercentDifference = 0.1)
override fun newFrameHandler(
snapshot: Snapshot,
frameCount: Int,
fps: Int,
): SnapshotHandler.FrameHandler {
val newSnapshot = Snapshot(
name = snapshot.name,
testName = paparazziTestName,
timestamp = snapshot.timestamp,
tags = snapshot.tags,
file = snapshot.file,
)
return snapshotHandler.newFrameHandler(
snapshot = newSnapshot,
frameCount = frameCount,
fps = fps
)
}

override fun close() {
snapshotHandler.close()
}
}

object PaparazziPreviewRule {
const val UNDEFINED_API_LEVEL = -1
const val MAX_API_LEVEL = 36

fun createFor(preview: ComposablePreview<AndroidPreviewInfo>): Paparazzi {
val previewInfo = preview.previewInfo
val previewApiLevel = when (previewInfo.apiLevel == UNDEFINED_API_LEVEL) {
true -> MAX_API_LEVEL
false -> previewInfo.apiLevel
}
val tolerance = 0.1
return Paparazzi(
environment = detectEnvironment().copy(compileSdkVersion = previewApiLevel),
deviceConfig = DeviceConfigBuilder.build(preview.previewInfo),
supportsRtl = true,
showSystemUi = previewInfo.showSystemUi,
renderingMode = when {
previewInfo.showSystemUi -> SessionParams.RenderingMode.NORMAL
previewInfo.widthDp > 0 && previewInfo.heightDp > 0 -> SessionParams.RenderingMode.FULL_EXPAND
else -> SessionParams.RenderingMode.SHRINK
},
snapshotHandler = when (System.getProperty("paparazzi.test.verify")?.toBoolean() == true) {
true -> PreviewSnapshotVerifier(tolerance)
false -> PreviewHtmlReportWriter()
},
// maxPercentDifference can be configured here if needed
maxPercentDifference = tolerance
)
}
}

/**
* A composable function that wraps content inside a Box with a specified size
* This is used to simulate what previews render when showSystemUi is true:
* - The Preview takes up the entire screen
* - The Composable still keeps its original size,
* - Background color of the Device is white,
* but the @Composable background color is the one defined in the Preview
*/
@Composable
fun SystemUiSize(
widthInDp: Int,
heightInDp: Int,
content: @Composable () -> Unit,
) {
Box(
Modifier
.size(
width = widthInDp.dp,
height = heightInDp.dp
)
.background(Color.White)
) {
content()
}
}

@Composable
fun PreviewBackground(
showBackground: Boolean,
backgroundColor: Long,
content: @Composable () -> Unit,
) {
when (showBackground) {
false -> content()
true -> {
val color = when (backgroundColor != 0L) {
true -> Color(backgroundColor)
false -> Color.White
}
Box(Modifier.background(color)) {
content()
}
}
}
}

@RunWith(Parameterized::class)
class GeneratedComposablePreviewPaparazziTests(
val preview: ComposablePreview<AndroidPreviewInfo>,
) {

companion object {
private val cachedPreviews: List<ComposablePreview<AndroidPreviewInfo>> by lazy {
AndroidComposablePreviewScanner()
.scanPackageTrees("com.mikepenz.aboutlibraries.screenshot")
.getPreviews()
}

@JvmStatic
@Parameterized.Parameters
fun values(): List<ComposablePreview<AndroidPreviewInfo>> = cachedPreviews
}

@get:Rule
val paparazzi: Paparazzi = PaparazziPreviewRule.createFor(preview)

@Test
fun snapshot() {
val screenshotId = AndroidPreviewScreenshotIdBuilder(preview)
.ignoreIdFor("heightDp")
.ignoreIdFor("widthDp")
.ignoreIdFor("showBackground")
.ignoreIdFor("backgroundColor")
.encodeUnsafeCharacters()
.build()

paparazzi.snapshot(name = screenshotId) {
val previewInfo = preview.previewInfo
when (previewInfo.showSystemUi) {
false -> PreviewBackground(
showBackground = previewInfo.showBackground,
backgroundColor = previewInfo.backgroundColor,
) {
preview()
}

true -> {
val parsedDevice = (DevicePreviewInfoParser.parse(previewInfo.device) ?: DEFAULT).inDp()
SystemUiSize(
widthInDp = parsedDevice.dimensions.width.toInt(),
heightInDp = parsedDevice.dimensions.height.toInt()
) {
PreviewBackground(
showBackground = true,
backgroundColor = previewInfo.backgroundColor,
) {
preview()
}
}
}
}
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ dependencyResolutionManagement {

versionCatalogs {
create("baseLibs") {
from("com.mikepenz:version-catalog:0.12.0")
from("com.mikepenz:version-catalog:0.12.3")
}
}
}
Expand Down
Loading