Introduce SystemUIScreenshotLib for screenshot testing
This CL introduces the SystemUIScreenshotLib, and in particular the (View)ScreenshotTestRule, to easily create a screenshot diff test of a given View for multiple configurations (devices, theme and orientation). Some diff tests will be added in follow-up CLs to keep the size of this one reasonable. Test: atest SystemUIGoogleScreenshotTests Bug: 230832101 Change-Id: I4762ed3d575d7a9f0d8ac6d06026efe1ed2a52be
This commit is contained in:
48
packages/SystemUI/screenshot/Android.bp
Normal file
48
packages/SystemUI/screenshot/Android.bp
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright (C) 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.
|
||||
|
||||
package {
|
||||
// See: http://go/android-license-faq
|
||||
// A large-scale-change added 'default_applicable_licenses' to import
|
||||
// all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
|
||||
// to get the below license kinds:
|
||||
// SPDX-license-identifier-Apache-2.0
|
||||
default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
|
||||
}
|
||||
|
||||
android_library {
|
||||
name: "SystemUIScreenshotLib",
|
||||
manifest: "AndroidManifest.xml",
|
||||
|
||||
srcs: [
|
||||
// All files in this library should be in Kotlin besides some exceptions.
|
||||
"src/**/*.kt",
|
||||
|
||||
// This file was forked from google3, so exceptionally it can be in Java.
|
||||
"src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java",
|
||||
],
|
||||
|
||||
resource_dirs: [
|
||||
"res",
|
||||
],
|
||||
|
||||
static_libs: [
|
||||
"SystemUI-core",
|
||||
"androidx.test.espresso.core",
|
||||
"androidx.appcompat_appcompat",
|
||||
"platform-screenshot-diff-core",
|
||||
],
|
||||
|
||||
kotlincflags: ["-Xjvm-default=all"],
|
||||
}
|
||||
28
packages/SystemUI/screenshot/AndroidManifest.xml
Normal file
28
packages/SystemUI/screenshot/AndroidManifest.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 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.android.systemui.testing.screenshot">
|
||||
<application>
|
||||
<activity
|
||||
android:name="com.android.systemui.testing.screenshot.ScreenshotActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.SystemUI.Screenshot" />
|
||||
</application>
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
|
||||
</manifest>
|
||||
25
packages/SystemUI/screenshot/res/values/themes.xml
Normal file
25
packages/SystemUI/screenshot/res/values/themes.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 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.
|
||||
-->
|
||||
<resources>
|
||||
<style name="Theme.SystemUI.Screenshot" parent="Theme.SystemUI">
|
||||
<item name="android:windowActionBar">false</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
|
||||
<!-- Make sure that device specific cutouts don't impact the outcome of screenshot tests -->
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*/
|
||||
|
||||
package com.android.systemui.testing.screenshot;
|
||||
|
||||
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
|
||||
|
||||
import android.app.UiAutomation;
|
||||
import android.content.Context;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.ColorRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.test.espresso.Espresso;
|
||||
import androidx.test.espresso.IdlingRegistry;
|
||||
import androidx.test.espresso.IdlingResource;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.junit.function.ThrowingRunnable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/*
|
||||
* Note: This file was forked from
|
||||
* google3/third_party/java_src/android_libs/material_components/screenshot_tests/java/android/
|
||||
* support/design/scuba/color/DynamicColorsTestUtils.java.
|
||||
*/
|
||||
|
||||
/** Utility that helps change the dynamic system colors for testing. */
|
||||
@RequiresApi(32)
|
||||
public class DynamicColorsTestUtils {
|
||||
|
||||
private static final String TAG = DynamicColorsTestUtils.class.getSimpleName();
|
||||
|
||||
private static final String THEME_CUSTOMIZATION_KEY = "theme_customization_overlay_packages";
|
||||
private static final String THEME_CUSTOMIZATION_SYSTEM_PALETTE_KEY =
|
||||
"android.theme.customization.system_palette";
|
||||
|
||||
private static final int ORANGE_SYSTEM_SEED_COLOR = 0xA66800;
|
||||
private static final int ORANGE_EXPECTED_SYSTEM_ACCENT1_600_COLOR = -8235756;
|
||||
|
||||
private DynamicColorsTestUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update system dynamic colors (e.g., android.R.color.system_accent1_600) based on an orange
|
||||
* seed color, and then wait for the change to propagate to the app by comparing
|
||||
* android.R.color.system_accent1_600 to the expected orange value.
|
||||
*/
|
||||
public static void updateSystemColorsToOrange() {
|
||||
updateSystemColors(ORANGE_SYSTEM_SEED_COLOR, ORANGE_EXPECTED_SYSTEM_ACCENT1_600_COLOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update system dynamic colors (e.g., android.R.color.system_accent1_600) based on the provided
|
||||
* {@code seedColor}, and then wait for the change to propagate to the app by comparing
|
||||
* android.R.color.system_accent1_600 to {@code expectedSystemAccent1600}.
|
||||
*/
|
||||
public static void updateSystemColors(
|
||||
@ColorInt int seedColor, @ColorInt int expectedSystemAccent1600) {
|
||||
Context context = getInstrumentation().getTargetContext();
|
||||
|
||||
int actualSystemAccent1600 =
|
||||
ContextCompat.getColor(context, android.R.color.system_accent1_600);
|
||||
|
||||
if (expectedSystemAccent1600 == actualSystemAccent1600) {
|
||||
String expectedColorString = Integer.toHexString(expectedSystemAccent1600);
|
||||
Log.d(
|
||||
TAG,
|
||||
"Skipped updating system colors since system_accent1_600 is already equal to "
|
||||
+ "expected: "
|
||||
+ expectedColorString);
|
||||
return;
|
||||
}
|
||||
|
||||
updateSystemColors(seedColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update system dynamic colors (e.g., android.R.color.system_accent1_600) based on the provided
|
||||
* {@code seedColor}, and then wait for the change to propagate to the app by checking
|
||||
* android.R.color.system_accent1_600 for any change.
|
||||
*/
|
||||
public static void updateSystemColors(@ColorInt int seedColor) {
|
||||
Context context = getInstrumentation().getTargetContext();
|
||||
|
||||
// Initialize system color idling resource with original system_accent1_600 value.
|
||||
ColorChangeIdlingResource systemColorIdlingResource =
|
||||
new ColorChangeIdlingResource(context, android.R.color.system_accent1_600);
|
||||
|
||||
// Update system theme color setting to trigger fabricated resource overlay.
|
||||
runWithShellPermissionIdentity(
|
||||
() ->
|
||||
Settings.Secure.putString(
|
||||
context.getContentResolver(),
|
||||
THEME_CUSTOMIZATION_KEY,
|
||||
buildThemeCustomizationString(seedColor)));
|
||||
|
||||
// Wait for system color update to propagate to app.
|
||||
IdlingRegistry idlingRegistry = IdlingRegistry.getInstance();
|
||||
idlingRegistry.register(systemColorIdlingResource);
|
||||
Espresso.onIdle();
|
||||
idlingRegistry.unregister(systemColorIdlingResource);
|
||||
|
||||
Log.d(TAG,
|
||||
Settings.Secure.getString(context.getContentResolver(), THEME_CUSTOMIZATION_KEY));
|
||||
}
|
||||
|
||||
private static String buildThemeCustomizationString(@ColorInt int seedColor) {
|
||||
String seedColorHex = Integer.toHexString(seedColor);
|
||||
Map<String, String> themeCustomizationMap = new HashMap<>();
|
||||
themeCustomizationMap.put(THEME_CUSTOMIZATION_SYSTEM_PALETTE_KEY, seedColorHex);
|
||||
return new JSONObject(themeCustomizationMap).toString();
|
||||
}
|
||||
|
||||
private static void runWithShellPermissionIdentity(@NonNull ThrowingRunnable runnable) {
|
||||
UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
|
||||
uiAutomation.adoptShellPermissionIdentity();
|
||||
try {
|
||||
runnable.run();
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
uiAutomation.dropShellPermissionIdentity();
|
||||
}
|
||||
}
|
||||
|
||||
private static class ColorChangeIdlingResource implements IdlingResource {
|
||||
|
||||
private final Context mContext;
|
||||
private final int mColorResId;
|
||||
private final int mInitialColorInt;
|
||||
|
||||
private ResourceCallback mResourceCallback;
|
||||
private boolean mIdleNow;
|
||||
|
||||
ColorChangeIdlingResource(Context context, @ColorRes int colorResId) {
|
||||
this.mContext = context;
|
||||
this.mColorResId = colorResId;
|
||||
this.mInitialColorInt = ContextCompat.getColor(context, colorResId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return ColorChangeIdlingResource.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIdleNow() {
|
||||
if (mIdleNow) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int currentColorInt = ContextCompat.getColor(mContext, mColorResId);
|
||||
|
||||
String initialColorString = Integer.toHexString(mInitialColorInt);
|
||||
String currentColorString = Integer.toHexString(currentColorInt);
|
||||
Log.d(TAG, String.format("Initial=%s, Current=%s", initialColorString,
|
||||
currentColorString));
|
||||
|
||||
mIdleNow = currentColorInt != mInitialColorInt;
|
||||
Log.d(TAG, String.format("idleNow=%b", mIdleNow));
|
||||
|
||||
if (mIdleNow) {
|
||||
mResourceCallback.onTransitionToIdle();
|
||||
}
|
||||
return mIdleNow;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
|
||||
this.mResourceCallback = resourceCallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*/
|
||||
|
||||
package com.android.systemui.testing.screenshot
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
|
||||
/** The Activity that is launched and whose content is set for screenshot tests. */
|
||||
class ScreenshotActivity : ComponentActivity()
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*/
|
||||
|
||||
package com.android.systemui.testing.screenshot
|
||||
|
||||
import android.app.UiModeManager
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.os.UserHandle
|
||||
import android.view.Display
|
||||
import android.view.View
|
||||
import android.view.WindowManagerGlobal
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
import platform.test.screenshot.GoldenImagePathManager
|
||||
import platform.test.screenshot.PathConfig
|
||||
import platform.test.screenshot.PathElementNoContext
|
||||
import platform.test.screenshot.ScreenshotTestRule
|
||||
import platform.test.screenshot.matchers.PixelPerfectMatcher
|
||||
|
||||
/**
|
||||
* A base rule for screenshot diff tests.
|
||||
*
|
||||
* This rules takes care of setting up the activity according to [testSpec] by:
|
||||
* - emulating the display size and density.
|
||||
* - setting the dark/light mode.
|
||||
* - setting the system (Material You) colors to a fixed value.
|
||||
*
|
||||
* @see ComposeScreenshotTestRule
|
||||
* @see ViewScreenshotTestRule
|
||||
*/
|
||||
class ScreenshotTestRule(private val testSpec: ScreenshotTestSpec) : TestRule {
|
||||
private var currentDisplay: DisplaySpec? = null
|
||||
private var currentGoldenIdentifier: String? = null
|
||||
|
||||
private val pathConfig =
|
||||
PathConfig(
|
||||
PathElementNoContext("model", isDir = true) {
|
||||
currentDisplay?.name ?: error("currentDisplay is null")
|
||||
},
|
||||
)
|
||||
private val defaultMatcher = PixelPerfectMatcher()
|
||||
|
||||
private val screenshotRule =
|
||||
ScreenshotTestRule(
|
||||
SystemUIGoldenImagePathManager(
|
||||
pathConfig,
|
||||
currentGoldenIdentifier = {
|
||||
currentGoldenIdentifier ?: error("currentGoldenIdentifier is null")
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement {
|
||||
// The statement which call beforeTest() before running the test and afterTest() afterwards.
|
||||
val statement =
|
||||
object : Statement() {
|
||||
override fun evaluate() {
|
||||
try {
|
||||
beforeTest()
|
||||
base.evaluate()
|
||||
} finally {
|
||||
afterTest()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return screenshotRule.apply(statement, description)
|
||||
}
|
||||
|
||||
private fun beforeTest() {
|
||||
// Update the system colors to a fixed color, so that tests don't depend on the host device
|
||||
// extracted colors. Note that we don't restore the default device colors at the end of the
|
||||
// test because changing the colors (and waiting for them to be applied) is costly and makes
|
||||
// the screenshot tests noticeably slower.
|
||||
DynamicColorsTestUtils.updateSystemColorsToOrange()
|
||||
|
||||
// Emulate the display size and density.
|
||||
val display = testSpec.display
|
||||
val density = display.densityDpi
|
||||
val wm = WindowManagerGlobal.getWindowManagerService()
|
||||
val (width, height) = getEmulatedDisplaySize()
|
||||
wm.setForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, density, UserHandle.myUserId())
|
||||
wm.setForcedDisplaySize(Display.DEFAULT_DISPLAY, width, height)
|
||||
|
||||
// Force the dark/light theme.
|
||||
val uiModeManager =
|
||||
InstrumentationRegistry.getInstrumentation()
|
||||
.targetContext
|
||||
.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
|
||||
uiModeManager.setApplicationNightMode(
|
||||
if (testSpec.isDarkTheme) {
|
||||
UiModeManager.MODE_NIGHT_YES
|
||||
} else {
|
||||
UiModeManager.MODE_NIGHT_NO
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun afterTest() {
|
||||
// Reset the density and display size.
|
||||
val wm = WindowManagerGlobal.getWindowManagerService()
|
||||
wm.clearForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, UserHandle.myUserId())
|
||||
wm.clearForcedDisplaySize(Display.DEFAULT_DISPLAY)
|
||||
|
||||
// Reset the dark/light theme.
|
||||
val uiModeManager =
|
||||
InstrumentationRegistry.getInstrumentation()
|
||||
.targetContext
|
||||
.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
|
||||
uiModeManager.setApplicationNightMode(UiModeManager.MODE_NIGHT_AUTO)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare the content of [view] with the golden image identified by [goldenIdentifier] in the
|
||||
* context of [testSpec].
|
||||
*/
|
||||
fun screenshotTest(goldenIdentifier: String, view: View) {
|
||||
val bitmap = drawIntoBitmap(view)
|
||||
|
||||
// Compare bitmap against golden asset.
|
||||
val isDarkTheme = testSpec.isDarkTheme
|
||||
val isLandscape = testSpec.isLandscape
|
||||
val identifierWithSpec = buildString {
|
||||
append(goldenIdentifier)
|
||||
if (isDarkTheme) append("_dark")
|
||||
if (isLandscape) append("_landscape")
|
||||
}
|
||||
|
||||
// TODO(b/230832101): Provide a way to pass a PathConfig and override the file name on
|
||||
// device to assertBitmapAgainstGolden instead?
|
||||
currentDisplay = testSpec.display
|
||||
currentGoldenIdentifier = goldenIdentifier
|
||||
screenshotRule.assertBitmapAgainstGolden(bitmap, identifierWithSpec, defaultMatcher)
|
||||
currentDisplay = null
|
||||
currentGoldenIdentifier = goldenIdentifier
|
||||
}
|
||||
|
||||
/** Draw [view] into a [Bitmap]. */
|
||||
private fun drawIntoBitmap(view: View): Bitmap {
|
||||
val bitmap =
|
||||
Bitmap.createBitmap(
|
||||
view.measuredWidth,
|
||||
view.measuredHeight,
|
||||
Bitmap.Config.ARGB_8888,
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
view.draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
/** Get the emulated display size for [testSpec]. */
|
||||
private fun getEmulatedDisplaySize(): Pair<Int, Int> {
|
||||
val display = testSpec.display
|
||||
val isPortraitNaturalPosition = display.width < display.height
|
||||
return if (testSpec.isLandscape) {
|
||||
if (isPortraitNaturalPosition) {
|
||||
display.height to display.width
|
||||
} else {
|
||||
display.width to display.height
|
||||
}
|
||||
} else {
|
||||
if (isPortraitNaturalPosition) {
|
||||
display.width to display.height
|
||||
} else {
|
||||
display.height to display.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SystemUIGoldenImagePathManager(
|
||||
pathConfig: PathConfig,
|
||||
private val currentGoldenIdentifier: () -> String,
|
||||
) :
|
||||
GoldenImagePathManager(
|
||||
appContext = InstrumentationRegistry.getInstrumentation().context,
|
||||
deviceLocalPath =
|
||||
InstrumentationRegistry.getInstrumentation()
|
||||
.targetContext
|
||||
.filesDir
|
||||
.absolutePath
|
||||
.toString() + "/sysui_screenshots",
|
||||
pathConfig = pathConfig,
|
||||
) {
|
||||
// This string is appended to all actual/expected screenshots on the device. We append the
|
||||
// golden identifier so that our pull_golden.py scripts can map a screenshot on device to its
|
||||
// asset (and automatically update it, if necessary).
|
||||
override fun toString() = currentGoldenIdentifier()
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*/
|
||||
|
||||
package com.android.systemui.testing.screenshot
|
||||
|
||||
/** The specification of a device display to be used in a screenshot test. */
|
||||
data class DisplaySpec(
|
||||
val name: String,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val densityDpi: Int,
|
||||
)
|
||||
|
||||
/** The specification of a screenshot diff test. */
|
||||
class ScreenshotTestSpec(
|
||||
val display: DisplaySpec,
|
||||
val isDarkTheme: Boolean = false,
|
||||
val isLandscape: Boolean = false,
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Return a list of [ScreenshotTestSpec] for each of the [displays].
|
||||
*
|
||||
* If [isDarkTheme] is null, this will create a spec for both light and dark themes, for
|
||||
* each of the orientation.
|
||||
*
|
||||
* If [isLandscape] is null, this will create a spec for both portrait and landscape, for
|
||||
* each of the light/dark themes.
|
||||
*/
|
||||
fun forDisplays(
|
||||
vararg displays: DisplaySpec,
|
||||
isDarkTheme: Boolean? = null,
|
||||
isLandscape: Boolean? = null,
|
||||
): List<ScreenshotTestSpec> {
|
||||
return displays.flatMap { display ->
|
||||
buildList {
|
||||
fun addDisplay(isLandscape: Boolean) {
|
||||
if (isDarkTheme != true) {
|
||||
add(ScreenshotTestSpec(display, isDarkTheme = false, isLandscape))
|
||||
}
|
||||
|
||||
if (isDarkTheme != false) {
|
||||
add(ScreenshotTestSpec(display, isDarkTheme = true, isLandscape))
|
||||
}
|
||||
}
|
||||
|
||||
if (isLandscape != true) {
|
||||
addDisplay(isLandscape = false)
|
||||
}
|
||||
|
||||
if (isLandscape != false) {
|
||||
addDisplay(isLandscape = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String = buildString {
|
||||
// This string is appended to PNGs stored in the device, so let's keep it simple.
|
||||
append(display.name)
|
||||
if (isDarkTheme) append("_dark")
|
||||
if (isLandscape) append("_landscape")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.android.systemui.testing.screenshot
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.rules.RuleChain
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
|
||||
/** A rule for View screenshot diff tests. */
|
||||
class ViewScreenshotTestRule(testSpec: ScreenshotTestSpec) : TestRule {
|
||||
private val activityRule = ActivityScenarioRule(ScreenshotActivity::class.java)
|
||||
private val screenshotRule = ScreenshotTestRule(testSpec)
|
||||
|
||||
private val delegate = RuleChain.outerRule(screenshotRule).around(activityRule)
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement {
|
||||
return delegate.apply(base, description)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare the content of [view] with the golden image identified by [goldenIdentifier] in the
|
||||
* context of [testSpec].
|
||||
*/
|
||||
fun screenshotTest(
|
||||
goldenIdentifier: String,
|
||||
layoutParams: LayoutParams =
|
||||
LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT),
|
||||
view: (Activity) -> View,
|
||||
) {
|
||||
activityRule.scenario.onActivity { activity ->
|
||||
// Make sure that the activity draws full screen and fits the whole display instead of
|
||||
// the system bars.
|
||||
activity.window.setDecorFitsSystemWindows(false)
|
||||
activity.setContentView(view(activity), layoutParams)
|
||||
}
|
||||
|
||||
// We call onActivity again because it will make sure that our Activity is done measuring,
|
||||
// laying out and drawing its content (that we set in the previous onActivity lambda).
|
||||
activityRule.scenario.onActivity { activity ->
|
||||
// Check that the content is what we expected.
|
||||
val content = activity.requireViewById<ViewGroup>(android.R.id.content)
|
||||
assertEquals(1, content.childCount)
|
||||
screenshotRule.screenshotTest(goldenIdentifier, content.getChildAt(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user