Automatic @Preview screenshot testing utilizing Showkase, Paparazzi and TestParameterInjector. Showkase aggregates all @Preview definitions, and collects them into a list of components via a KSP processor. We can then use these components as parameterized inputs to tests configured with TestParameterInjector, and then we use Paparazzi to render each preview and diff the output. This CL configures Showkase to run on all UI modules, and then aggregates them all in a test-only preview-screenshots module. These tests run all previews in a matrix of configurations: - Each @Preview - Nexus 5, Pixel 5, Pixel C - Font scale of 1.0 and 1.5 This could be expanded even further if desired (locale, theming, etc.) Recording tests can be done with `./gradlew recordPaparazziDemoDebug`, and then checked with `./gradlew verifyPaparazziDemoDebug`. `check` is configured to depend on `verifyPaparazziDemoDebug`. Change-Id: I13a64e187517c08a69487caca09c1db52c6e4c98av/paparazzi-showkase-preview-screenshot-testing
@ -0,0 +1,32 @@
|
||||
# Preview Screenshot Tests
|
||||
|
||||
`preview-screenshots` performs automatic `@Preview` screenshot testing utilizing
|
||||
[Showkase](https://github.com/airbnb/Showkase),
|
||||
[Paparazzi](https://github.com/cashapp/paparazzi) and
|
||||
[TestParameterInjector](https://github.com/google/TestParameterInjector).
|
||||
|
||||
Showkase aggregates all `@Preview` definitions, and collects them into a list of components
|
||||
via a KSP processor. We can then use these components as parameterized inputs to tests
|
||||
configured with TestParameterInjector, and then we use Paparazzi to render each preview
|
||||
and diff the output.
|
||||
|
||||
This CL configures Showkase to run on all UI modules, and then aggregates them all in a test-only
|
||||
preview-screenshots module. These tests run all previews in a matrix of configurations:
|
||||
- Each `@Preview`
|
||||
- Nexus 5, Pixel 5, Pixel C
|
||||
- Font scale of `1.0` and `1.5`
|
||||
|
||||
This could be expanded even further if desired (locale, theming, etc.)
|
||||
|
||||
Screenshots are rendered with `layoutlib`, which is the same tool that drives rendering previews
|
||||
in Android Studio. As a result, the screenshots being taken do not have the same fidelity as what
|
||||
will actually displayed on a real device. In particular, certain devices might behave slightly
|
||||
differently, and it isn't possible to verify display interactions with the system (dialogs,
|
||||
soft keyboard, etc.)
|
||||
|
||||
The advantage, however, is that Paparazzi runs in JVM tests, which means they are faster, and don't
|
||||
require managing a virtual or physical device, making it easier to parameterize across different
|
||||
device sizes and configurations.
|
||||
|
||||
Recording tests can be done with `./gradlew recordPaparazziDemoDebug`, and then checked with
|
||||
`./gradlew verifyPaparazziDemoDebug`. `check` is configured to depend on `verifyPaparazziDemoDebug`.
|
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import com.google.samples.apps.nowinandroid.Flavor
|
||||
import com.google.samples.apps.nowinandroid.FlavorDimension
|
||||
|
||||
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
|
||||
@Suppress("DSL_SCOPE_VIOLATION")
|
||||
plugins {
|
||||
id("nowinandroid.android.library")
|
||||
id("nowinandroid.android.library.compose")
|
||||
id("nowinandroid.android.library.jacoco")
|
||||
id("nowinandroid.spotless")
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.paparazzi)
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
// Disable release builds for this test-only library, no need to run screenshot tests more than
|
||||
// once
|
||||
beforeVariants(selector().withBuildType("release")) { builder ->
|
||||
builder.enable = false
|
||||
}
|
||||
// Disable prod builds for this test-only library, no need to run screenshot tests more than
|
||||
// once
|
||||
beforeVariants(
|
||||
selector().withFlavor(FlavorDimension.contentType.name to Flavor.prod.name)
|
||||
) { builder ->
|
||||
builder.enable = false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core-ui"))
|
||||
implementation(project(":feature-author"))
|
||||
implementation(project(":feature-foryou"))
|
||||
implementation(project(":feature-interests"))
|
||||
implementation(project(":feature-topic"))
|
||||
|
||||
implementation(libs.showkase.runtime)
|
||||
ksp(libs.showkase.processor)
|
||||
|
||||
testImplementation(libs.junit4)
|
||||
testImplementation(libs.testParameterInjector)
|
||||
}
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn("verifyPaparazziDemoDebug")
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
// Increase memory for Paparazzi tests
|
||||
maxHeapSize = "2g"
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.samples.apps.nowinandroid.preview.screenshots">
|
||||
|
||||
</manifest>
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.unit.Density
|
||||
import app.cash.paparazzi.DeviceConfig
|
||||
import app.cash.paparazzi.Paparazzi
|
||||
import com.airbnb.android.showkase.models.Showkase
|
||||
import com.airbnb.android.showkase.models.ShowkaseBrowserComponent
|
||||
import com.google.testing.junit.testparameterinjector.TestParameter
|
||||
import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider
|
||||
import com.google.testing.junit.testparameterinjector.TestParameterInjector
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
class ComponentPreview(
|
||||
private val showkaseBrowserComponent: ShowkaseBrowserComponent
|
||||
) {
|
||||
val content: @Composable () -> Unit = showkaseBrowserComponent.component
|
||||
override fun toString(): String = showkaseBrowserComponent.componentKey
|
||||
}
|
||||
|
||||
@RunWith(TestParameterInjector::class)
|
||||
class PreviewScreenshotTests {
|
||||
|
||||
object PreviewProvider : TestParameterValuesProvider {
|
||||
override fun provideValues(): List<ComponentPreview> =
|
||||
Showkase.getMetadata().componentList.map(::ComponentPreview)
|
||||
}
|
||||
|
||||
enum class BaseDeviceConfig(
|
||||
val deviceConfig: DeviceConfig,
|
||||
) {
|
||||
NEXUS_5(DeviceConfig.NEXUS_5),
|
||||
PIXEL_5(DeviceConfig.PIXEL_5),
|
||||
PIXEL_C(DeviceConfig.PIXEL_C),
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val paparazzi = Paparazzi(
|
||||
maxPercentDifference = 0.0,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun preview_tests(
|
||||
@TestParameter(valuesProvider = PreviewProvider::class) componentPreview: ComponentPreview,
|
||||
@TestParameter baseDeviceConfig: BaseDeviceConfig,
|
||||
@TestParameter(value = ["1.0", "1.5"]) fontScale: Float
|
||||
) {
|
||||
paparazzi.unsafeUpdateConfig(
|
||||
baseDeviceConfig.deviceConfig.copy(
|
||||
softButtons = false,
|
||||
)
|
||||
)
|
||||
paparazzi.snapshot {
|
||||
CompositionLocalProvider(
|
||||
LocalInspectionMode provides true,
|
||||
LocalDensity provides Density(
|
||||
density = LocalDensity.current.density,
|
||||
fontScale = fontScale
|
||||
)
|
||||
) {
|
||||
Box {
|
||||
componentPreview.content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid
|
||||
|
||||
import com.airbnb.android.showkase.annotation.ShowkaseRoot
|
||||
import com.airbnb.android.showkase.annotation.ShowkaseRootModule
|
||||
|
||||
@ShowkaseRoot
|
||||
class ScreenshotShowkaseModule : ShowkaseRootModule
|
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 6.1 KiB |
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 6.0 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 9.9 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 8.3 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 9.9 KiB |
After Width: | Height: | Size: 9.7 KiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 8.5 KiB |
After Width: | Height: | Size: 7.3 KiB |
After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 156 KiB |
After Width: | Height: | Size: 154 KiB |
After Width: | Height: | Size: 80 KiB |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 130 KiB |
After Width: | Height: | Size: 129 KiB |
After Width: | Height: | Size: 158 KiB |
After Width: | Height: | Size: 158 KiB |
After Width: | Height: | Size: 112 KiB |
After Width: | Height: | Size: 97 KiB |
After Width: | Height: | Size: 152 KiB |
After Width: | Height: | Size: 146 KiB |
After Width: | Height: | Size: 91 KiB |
After Width: | Height: | Size: 114 KiB |
After Width: | Height: | Size: 98 KiB |
After Width: | Height: | Size: 96 KiB |
After Width: | Height: | Size: 169 KiB |
After Width: | Height: | Size: 158 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 9.7 KiB |