diff --git a/packages/SettingsLib/Spa/screenshot/Android.bp b/packages/SettingsLib/Spa/screenshot/Android.bp new file mode 100644 index 0000000000000..4e6b646da819e --- /dev/null +++ b/packages/SettingsLib/Spa/screenshot/Android.bp @@ -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"], +} diff --git a/packages/SettingsLib/Spa/screenshot/AndroidManifest.xml b/packages/SettingsLib/Spa/screenshot/AndroidManifest.xml new file mode 100644 index 0000000000000..d59a1542498d0 --- /dev/null +++ b/packages/SettingsLib/Spa/screenshot/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + diff --git a/packages/SettingsLib/Spa/screenshot/AndroidTest.xml b/packages/SettingsLib/Spa/screenshot/AndroidTest.xml new file mode 100644 index 0000000000000..e0c08e811c460 --- /dev/null +++ b/packages/SettingsLib/Spa/screenshot/AndroidTest.xml @@ -0,0 +1,36 @@ + + + + diff --git a/packages/SettingsLib/Spa/screenshot/assets/phone/dark_landscape_preference.png b/packages/SettingsLib/Spa/screenshot/assets/phone/dark_landscape_preference.png new file mode 100644 index 0000000000000..6086e2d15dd27 Binary files /dev/null and b/packages/SettingsLib/Spa/screenshot/assets/phone/dark_landscape_preference.png differ diff --git a/packages/SettingsLib/Spa/screenshot/assets/phone/dark_portrait_preference.png b/packages/SettingsLib/Spa/screenshot/assets/phone/dark_portrait_preference.png new file mode 100644 index 0000000000000..aa6c5b7cd131e Binary files /dev/null and b/packages/SettingsLib/Spa/screenshot/assets/phone/dark_portrait_preference.png differ diff --git a/packages/SettingsLib/Spa/screenshot/assets/phone/light_landscape_preference.png b/packages/SettingsLib/Spa/screenshot/assets/phone/light_landscape_preference.png new file mode 100644 index 0000000000000..cac990caad635 Binary files /dev/null and b/packages/SettingsLib/Spa/screenshot/assets/phone/light_landscape_preference.png differ diff --git a/packages/SettingsLib/Spa/screenshot/assets/phone/light_portrait_preference.png b/packages/SettingsLib/Spa/screenshot/assets/phone/light_portrait_preference.png new file mode 100644 index 0000000000000..f6298c0b62c45 Binary files /dev/null and b/packages/SettingsLib/Spa/screenshot/assets/phone/light_portrait_preference.png differ diff --git a/packages/SettingsLib/Spa/screenshot/assets/tablet/dark_landscape_preference.png b/packages/SettingsLib/Spa/screenshot/assets/tablet/dark_landscape_preference.png new file mode 100644 index 0000000000000..9391eeb01e740 Binary files /dev/null and b/packages/SettingsLib/Spa/screenshot/assets/tablet/dark_landscape_preference.png differ diff --git a/packages/SettingsLib/Spa/screenshot/assets/tablet/dark_portrait_preference.png b/packages/SettingsLib/Spa/screenshot/assets/tablet/dark_portrait_preference.png new file mode 100644 index 0000000000000..94e28437c04ea Binary files /dev/null and b/packages/SettingsLib/Spa/screenshot/assets/tablet/dark_portrait_preference.png differ diff --git a/packages/SettingsLib/Spa/screenshot/assets/tablet/light_landscape_preference.png b/packages/SettingsLib/Spa/screenshot/assets/tablet/light_landscape_preference.png new file mode 100644 index 0000000000000..b1d03c31362ee Binary files /dev/null and b/packages/SettingsLib/Spa/screenshot/assets/tablet/light_landscape_preference.png differ diff --git a/packages/SettingsLib/Spa/screenshot/assets/tablet/light_portrait_preference.png b/packages/SettingsLib/Spa/screenshot/assets/tablet/light_portrait_preference.png new file mode 100644 index 0000000000000..95f19da3c52cd Binary files /dev/null and b/packages/SettingsLib/Spa/screenshot/assets/tablet/light_portrait_preference.png differ diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/Bitmap.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/Bitmap.kt new file mode 100644 index 0000000000000..814d4a1d1edee --- /dev/null +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/Bitmap.kt @@ -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() diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/DefaultDeviceEmulationSpec.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/DefaultDeviceEmulationSpec.kt new file mode 100644 index 0000000000000..d7f42b3d1be3a --- /dev/null +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/DefaultDeviceEmulationSpec.kt @@ -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, + ) +} diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsGoldenImagePathManager.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsGoldenImagePathManager.kt new file mode 100644 index 0000000000000..25bc098b29b76 --- /dev/null +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsGoldenImagePathManager.kt @@ -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" + } +} diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsScreenshotTestRule.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsScreenshotTestRule.kt new file mode 100644 index 0000000000000..7a7cf318fb4f4 --- /dev/null +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsScreenshotTestRule.kt @@ -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() + 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) + } +} diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/PreferenceScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/PreferenceScreenshotTest.kt new file mode 100644 index 0000000000000..9631826382937 --- /dev/null +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/PreferenceScreenshotTest.kt @@ -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) + } + }) + } + } + } +} \ No newline at end of file