Merge "Introduce SpaScreenshotTests for SPA screenshot testing."

This commit is contained in:
Kelly Zhang
2022-12-08 10:58:29 +00:00
committed by Android (Google) Code Review
16 changed files with 467 additions and 0 deletions

View 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"],
}

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -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()

View File

@@ -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,
)
}

View File

@@ -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"
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
})
}
}
}
}