Skip to content

Commit

Permalink
ScreenshotHelper.kt
Browse files Browse the repository at this point in the history
  • Loading branch information
juliewww committed Sep 11, 2023
1 parent 16ca88c commit 806df94
Show file tree
Hide file tree
Showing 2 changed files with 210 additions and 0 deletions.
2 changes: 2 additions & 0 deletions core/testing/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ dependencies {
implementation(projects.core.common)
implementation(projects.core.componentController)
implementation(projects.core.data)
implementation(projects.core.designsystem)
implementation(projects.core.domain)
implementation(projects.core.model)
implementation(projects.core.ui)
implementation(libs.kotlinx.datetime)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/*
* Copyright 2023 Blocker
*
* 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.merxury.blocker.core.testing.util

import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.onRoot
import androidx.test.ext.junit.rules.ActivityScenarioRule
import com.github.takahirom.roborazzi.RoborazziOptions
import com.github.takahirom.roborazzi.RoborazziOptions.CompareOptions
import com.github.takahirom.roborazzi.RoborazziOptions.RecordOptions
import com.github.takahirom.roborazzi.captureRoboImage
import com.google.accompanist.testharness.TestHarness
import com.merxury.blocker.core.designsystem.theme.BlockerTheme
import org.robolectric.RuntimeEnvironment

val DefaultRoborazziOptions =
RoborazziOptions(
compareOptions = CompareOptions(changeThreshold = 0f), // Pixel-perfect matching
recordOptions = RecordOptions(resizeScale = 0.5), // Reduce the size of the PNGs
)

enum class DefaultTestDevices(val description: String, val spec: String) {
PHONE("phone", "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480"),
FOLDABLE("foldable", "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480"),
TABLET("tablet", "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480"),
}

fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>.captureMultiDevice(
screenshotName: String,
body: @Composable () -> Unit,
) {
DefaultTestDevices.values().forEach {
this.captureForDevice(it.description, it.spec, screenshotName, body = body)
}
}

fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>.captureForDevice(
deviceName: String,
deviceSpec: String,
screenshotName: String,
roborazziOptions: RoborazziOptions = DefaultRoborazziOptions,
darkMode: Boolean = false,
body: @Composable () -> Unit,
) {
val (width, height, dpi) = extractSpecs(deviceSpec)

// Set qualifiers from specs
RuntimeEnvironment.setQualifiers("w${width}dp-h${height}dp-${dpi}dpi")

this.activity.setContent {
CompositionLocalProvider(
LocalInspectionMode provides true,
) {
TestHarness(darkMode = darkMode) {
body()
}
}
}
this.onRoot()
.captureRoboImage(
"src/test/screenshots/${screenshotName}_$deviceName.png",
roborazziOptions = roborazziOptions,
)
}

/**
* Takes six screenshots combining light/dark and default/Android themes and whether dynamic color
* is enabled.
*/
fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>.captureMultiTheme(
name: String,
overrideFileName: String? = null,
shouldCompareDarkMode: Boolean = true,
shouldCompareDynamicColor: Boolean = true,
shouldCompareAndroidTheme: Boolean = true,
content: @Composable (desc: String) -> Unit,
) {
val darkModeValues = if (shouldCompareDarkMode) listOf(true, false) else listOf(false)
val dynamicThemingValues = if (shouldCompareDynamicColor) listOf(true, false) else listOf(false)
val androidThemeValues = if (shouldCompareAndroidTheme) listOf(true, false) else listOf(false)

var darkMode by mutableStateOf(true)
var dynamicTheming by mutableStateOf(false)
var blockerTheme by mutableStateOf(false)

this.setContent {
CompositionLocalProvider(
LocalInspectionMode provides true,
) {
BlockerTheme(
blockerTheme = blockerTheme,
darkTheme = darkMode,
disableDynamicTheming = !dynamicTheming,
) {
// Keying is necessary in some cases (e.g. animations)
key(blockerTheme, darkMode, dynamicTheming) {
val description = generateDescription(
shouldCompareDarkMode,
darkMode,
shouldCompareAndroidTheme,
blockerTheme,
shouldCompareDynamicColor,
dynamicTheming,
)
content(description)
}
}
}
}

// Create permutations
darkModeValues.forEach { isDarkMode ->
darkMode = isDarkMode
val darkModeDesc = if (isDarkMode) "dark" else "light"

androidThemeValues.forEach { isAndroidTheme ->
blockerTheme = isAndroidTheme
val androidThemeDesc = if (isAndroidTheme) "androidTheme" else "defaultTheme"

dynamicThemingValues.forEach dynamicTheme@{ isDynamicTheming ->
// Skip tests with both Android Theme and Dynamic color as they're incompatible.
if (isAndroidTheme && isDynamicTheming) return@dynamicTheme

dynamicTheming = isDynamicTheming
val dynamicThemingDesc = if (isDynamicTheming) "dynamic" else "notDynamic"

val filename = overrideFileName ?: name

this.onRoot()
.captureRoboImage(
"src/test/screenshots/" +
"$name/$filename" +
"_$darkModeDesc" +
"_$androidThemeDesc" +
"_$dynamicThemingDesc" +
".png",
roborazziOptions = DefaultRoborazziOptions,
)
}
}
}
}

@Composable
private fun generateDescription(
shouldCompareDarkMode: Boolean,
darkMode: Boolean,
shouldCompareAndroidTheme: Boolean,
androidTheme: Boolean,
shouldCompareDynamicColor: Boolean,
dynamicTheming: Boolean,
): String {
val description = "" +
if (shouldCompareDarkMode) {
if (darkMode) "Dark" else "Light"
} else {
""
} +
if (shouldCompareAndroidTheme) {
if (androidTheme) " Android" else " Default"
} else {
""
} +
if (shouldCompareDynamicColor) {
if (dynamicTheming) " Dynamic" else ""
} else {
""
}

return description.trim()
}

/**
* Extracts some properties from the spec string. Note that this function is not exhaustive.
*/
private fun extractSpecs(deviceSpec: String): TestDeviceSpecs {
val specs = deviceSpec.substringAfter("spec:")
.split(",").map { it.split("=") }.associate { it[0] to it[1] }
val width = specs["width"]?.toInt() ?: 640
val height = specs["height"]?.toInt() ?: 480
val dpi = specs["dpi"]?.toInt() ?: 480
return TestDeviceSpecs(width, height, dpi)
}

data class TestDeviceSpecs(val width: Int, val height: Int, val dpi: Int)

0 comments on commit 806df94

Please sign in to comment.