Introduce SpaScreenshotTests for SPA screenshot testing.
This CL introduces the SpaScreenshotTests to test a given View for multiple configurations (devices, theme and orientation). PreferenceScreenshotTest is added as an example, more screendiff tests will be added in follow-up CLs. Test: presubmit Bug: 256770697 Change-Id: I6b62d3cc248cab5bfe9f447e595ba393e9e3c848
41
packages/SettingsLib/Spa/screenshot/Android.bp
Normal file
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// 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 {
|
||||
default_applicable_licenses: ["frameworks_base_license"],
|
||||
}
|
||||
|
||||
android_test {
|
||||
name: "SpaScreenshotTests",
|
||||
test_suites: ["device-tests"],
|
||||
|
||||
asset_dirs: ["assets"],
|
||||
|
||||
srcs: ["src/**/*.kt"],
|
||||
|
||||
certificate: "platform",
|
||||
|
||||
static_libs: [
|
||||
"SpaLib",
|
||||
"SpaLibTestUtils",
|
||||
"androidx.compose.runtime_runtime",
|
||||
"androidx.test.ext.junit",
|
||||
"androidx.test.runner",
|
||||
"mockito-target-minus-junit4",
|
||||
"platform-screenshot-diff-core",
|
||||
],
|
||||
kotlincflags: ["-Xjvm-default=all"],
|
||||
}
|
||||
32
packages/SettingsLib/Spa/screenshot/AndroidManifest.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?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.settingslib.spa.screenshot">
|
||||
|
||||
<uses-sdk android:minSdkVersion="21"/>
|
||||
|
||||
<application>
|
||||
<uses-library android:name="android.test.runner" />
|
||||
<activity android:name=".DebugActivity" android:exported="true" />
|
||||
</application>
|
||||
|
||||
<instrumentation
|
||||
android:name="androidx.test.runner.AndroidJUnitRunner"
|
||||
android:label="Screenshot tests for SpaLib"
|
||||
android:targetPackage="com.android.settingslib.spa.screenshot">
|
||||
</instrumentation>
|
||||
</manifest>
|
||||
36
packages/SettingsLib/Spa/screenshot/AndroidTest.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<configuration description="Runs screendiff tests.">
|
||||
<option name="test-suite-tag" value="apct-instrumentation" />
|
||||
<option name="test-suite-tag" value="apct" />
|
||||
<target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
|
||||
<option name="optimized-property-setting" value="true" />
|
||||
</target_preparer>
|
||||
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
|
||||
<option name="cleanup-apks" value="true" />
|
||||
<option name="test-file-name" value="SpaScreenshotTests.apk" />
|
||||
</target_preparer>
|
||||
<metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
|
||||
<option name="directory-keys"
|
||||
value="/data/user/0/com.android.settingslib.spa.screenshot/files/settings_screenshots" />
|
||||
<option name="collect-on-run-ended-only" value="true" />
|
||||
</metrics_collector>
|
||||
<test class="com.android.tradefed.testtype.AndroidJUnitTest">
|
||||
<option name="package" value="com.android.settingslib.spa.screenshot" />
|
||||
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
|
||||
</test>
|
||||
</configuration>
|
||||
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 49 KiB |
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.settingslib.spa.screenshot
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import platform.test.screenshot.matchers.MSSIMMatcher
|
||||
import platform.test.screenshot.matchers.PixelPerfectMatcher
|
||||
|
||||
/** Draw this [View] into a [Bitmap]. */
|
||||
// TODO(b/195673633): Remove this once Compose screenshot tests use hardware rendering for their
|
||||
// tests.
|
||||
fun View.drawIntoBitmap(): Bitmap {
|
||||
val bitmap =
|
||||
Bitmap.createBitmap(
|
||||
measuredWidth,
|
||||
measuredHeight,
|
||||
Bitmap.Config.ARGB_8888,
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
/**
|
||||
* The [BitmapMatcher][platform.test.screenshot.matchers.BitmapMatcher] that should be used for
|
||||
* screenshot *unit* tests.
|
||||
*/
|
||||
val UnitTestBitmapMatcher =
|
||||
if (Build.CPU_ABI == "x86_64") {
|
||||
// Different CPU architectures can sometimes end up rendering differently, so we can't do
|
||||
// pixel-perfect matching on different architectures using the same golden. Given that our
|
||||
// presubmits are run on cf_x86_64_phone, our goldens should be perfectly matched on the
|
||||
// x86_64 architecture and use the Structural Similarity Index on others.
|
||||
// TODO(b/237511747): Run our screenshot presubmit tests on arm64 instead so that we can
|
||||
// do pixel perfect matching both at presubmit time and at development time with actual
|
||||
// devices.
|
||||
PixelPerfectMatcher()
|
||||
} else {
|
||||
MSSIMMatcher()
|
||||
}
|
||||
|
||||
/**
|
||||
* The [BitmapMatcher][platform.test.screenshot.matchers.BitmapMatcher] that should be used for
|
||||
* screenshot *unit* tests.
|
||||
*
|
||||
* We use the Structural Similarity Index for integration tests because they usually contain
|
||||
* additional information and noise that shouldn't break the test.
|
||||
*/
|
||||
val IntegrationTestBitmapMatcher = MSSIMMatcher()
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.settingslib.spa.screenshot
|
||||
|
||||
import platform.test.screenshot.DeviceEmulationSpec
|
||||
import platform.test.screenshot.DisplaySpec
|
||||
|
||||
/**
|
||||
* The emulations specs for all 8 permutations of:
|
||||
* - phone or tablet.
|
||||
* - dark of light mode.
|
||||
* - portrait or landscape.
|
||||
*/
|
||||
val DeviceEmulationSpec.Companion.PhoneAndTabletFull
|
||||
get() = PhoneAndTabletFullSpec
|
||||
|
||||
private val PhoneAndTabletFullSpec =
|
||||
DeviceEmulationSpec.forDisplays(Displays.Phone, Displays.Tablet)
|
||||
|
||||
/**
|
||||
* The emulations specs of:
|
||||
* - phone + light mode + portrait.
|
||||
* - phone + light mode + landscape.
|
||||
* - tablet + dark mode + portrait.
|
||||
*
|
||||
* This allows to test the most important permutations of a screen/layout with only 3
|
||||
* configurations.
|
||||
*/
|
||||
val DeviceEmulationSpec.Companion.PhoneAndTabletMinimal
|
||||
get() = PhoneAndTabletMinimalSpec
|
||||
|
||||
private val PhoneAndTabletMinimalSpec =
|
||||
DeviceEmulationSpec.forDisplays(Displays.Phone, isDarkTheme = false) +
|
||||
DeviceEmulationSpec.forDisplays(Displays.Tablet, isDarkTheme = true, isLandscape = false)
|
||||
|
||||
object Displays {
|
||||
val Phone =
|
||||
DisplaySpec(
|
||||
"phone",
|
||||
width = 1440,
|
||||
height = 3120,
|
||||
densityDpi = 560,
|
||||
)
|
||||
|
||||
val Tablet =
|
||||
DisplaySpec(
|
||||
"tablet",
|
||||
width = 2560,
|
||||
height = 1600,
|
||||
densityDpi = 320,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.settingslib.spa.screenshot
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import platform.test.screenshot.GoldenImagePathManager
|
||||
import platform.test.screenshot.PathConfig
|
||||
|
||||
/** A [GoldenImagePathManager] that should be used for all Settings screenshot tests. */
|
||||
class SettingsGoldenImagePathManager(
|
||||
pathConfig: PathConfig,
|
||||
assetsPathRelativeToBuildRoot: String
|
||||
) :
|
||||
GoldenImagePathManager(
|
||||
appContext = InstrumentationRegistry.getInstrumentation().context,
|
||||
assetsPathRelativeToBuildRoot = assetsPathRelativeToBuildRoot,
|
||||
deviceLocalPath =
|
||||
InstrumentationRegistry.getInstrumentation()
|
||||
.targetContext
|
||||
.filesDir
|
||||
.absolutePath
|
||||
.toString() + "/settings_screenshots",
|
||||
pathConfig = pathConfig,
|
||||
) {
|
||||
override fun toString(): String {
|
||||
// This string is appended to all actual/expected screenshots on the device, so make sure
|
||||
// it is a static value.
|
||||
return "SettingsGoldenImagePathManager"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.settingslib.spa.screenshot
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.ViewRootForTest
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onRoot
|
||||
import com.android.settingslib.spa.framework.theme.SettingsTheme
|
||||
import org.junit.rules.RuleChain
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
import platform.test.screenshot.DeviceEmulationRule
|
||||
import platform.test.screenshot.DeviceEmulationSpec
|
||||
import platform.test.screenshot.MaterialYouColorsRule
|
||||
import platform.test.screenshot.ScreenshotTestRule
|
||||
import platform.test.screenshot.getEmulatedDevicePathConfig
|
||||
|
||||
/** A rule for Settings screenshot diff tests. */
|
||||
class SettingsScreenshotTestRule(
|
||||
emulationSpec: DeviceEmulationSpec,
|
||||
assetsPathRelativeToBuildRoot: String
|
||||
) : TestRule {
|
||||
private val colorsRule = MaterialYouColorsRule()
|
||||
private val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
|
||||
private val screenshotRule =
|
||||
ScreenshotTestRule(
|
||||
SettingsGoldenImagePathManager(
|
||||
getEmulatedDevicePathConfig(emulationSpec),
|
||||
assetsPathRelativeToBuildRoot
|
||||
)
|
||||
)
|
||||
private val composeRule = createAndroidComposeRule<ComponentActivity>()
|
||||
private val delegateRule =
|
||||
RuleChain.outerRule(colorsRule)
|
||||
.around(deviceEmulationRule)
|
||||
.around(screenshotRule)
|
||||
.around(composeRule)
|
||||
private val matcher = UnitTestBitmapMatcher
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement {
|
||||
return delegateRule.apply(base, description)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare [content] with the golden image identified by [goldenIdentifier] in the context of
|
||||
* [testSpec].
|
||||
*/
|
||||
fun screenshotTest(
|
||||
goldenIdentifier: String,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
// Make sure that the activity draws full screen and fits the whole display.
|
||||
val activity = composeRule.activity
|
||||
activity.mainExecutor.execute { activity.window.setDecorFitsSystemWindows(false) }
|
||||
|
||||
// Set the content using the AndroidComposeRule to make sure that the Activity is set up
|
||||
// correctly.
|
||||
composeRule.setContent {
|
||||
SettingsTheme {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
composeRule.waitForIdle()
|
||||
|
||||
val view = (composeRule.onRoot().fetchSemanticsNode().root as ViewRootForTest).view
|
||||
screenshotRule.assertBitmapAgainstGolden(view.drawIntoBitmap(), goldenIdentifier, matcher)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.settingslib.spa.screenshot
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Autorenew
|
||||
import androidx.compose.material.icons.outlined.DisabledByDefault
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.android.settingslib.spa.framework.compose.toState
|
||||
import com.android.settingslib.spa.widget.preference.Preference
|
||||
import com.android.settingslib.spa.widget.preference.PreferenceModel
|
||||
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
|
||||
import com.android.settingslib.spa.widget.ui.SettingsIcon
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import platform.test.screenshot.DeviceEmulationSpec
|
||||
|
||||
/** A screenshot test for ExampleFeature. */
|
||||
@RunWith(Parameterized::class)
|
||||
class PreferenceScreenshotTest(emulationSpec: DeviceEmulationSpec) {
|
||||
companion object {
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
@JvmStatic
|
||||
fun getTestSpecs() = DeviceEmulationSpec.PhoneAndTabletFull
|
||||
private const val TITLE = "Title"
|
||||
private const val SUMMARY = "Summary"
|
||||
private const val LONG_SUMMARY =
|
||||
"Long long long long long long long long long long long long long long long summary"
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val screenshotRule =
|
||||
SettingsScreenshotTestRule(
|
||||
emulationSpec,
|
||||
"frameworks/base/packages/SettingsLib/Spa/screenshot/assets"
|
||||
)
|
||||
|
||||
@Test
|
||||
fun testPreference() {
|
||||
screenshotRule.screenshotTest("preference") {
|
||||
RegularScaffold(title = "Preference") {
|
||||
Preference(object : PreferenceModel {
|
||||
override val title = TITLE
|
||||
})
|
||||
|
||||
Preference(object : PreferenceModel {
|
||||
override val title = TITLE
|
||||
override val summary = SUMMARY.toState()
|
||||
})
|
||||
|
||||
Preference(object : PreferenceModel {
|
||||
override val title = TITLE
|
||||
override val summary = LONG_SUMMARY.toState()
|
||||
})
|
||||
|
||||
Preference(object : PreferenceModel {
|
||||
override val title = TITLE
|
||||
override val summary = SUMMARY.toState()
|
||||
override val enabled = false.toState()
|
||||
override val icon = @Composable {
|
||||
SettingsIcon(imageVector = Icons.Outlined.DisabledByDefault)
|
||||
}
|
||||
})
|
||||
|
||||
Preference(object : PreferenceModel {
|
||||
override val title = TITLE
|
||||
override val summary = SUMMARY.toState()
|
||||
override val icon = @Composable {
|
||||
SettingsIcon(imageVector = Icons.Outlined.Autorenew)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||