blob: e075abe157d2e7212ab421b8555b3eac1fcf6e3a [file] [log] [blame]
/*
* 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.Activity
import android.app.Dialog
import android.graphics.Bitmap
import android.os.Build
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import androidx.activity.ComponentActivity
import androidx.test.ext.junit.rules.ActivityScenarioRule
import java.util.concurrent.TimeUnit
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
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
import platform.test.screenshot.matchers.BitmapMatcher
/** A rule for View screenshot diff unit tests. */
open class ViewScreenshotTestRule(
emulationSpec: DeviceEmulationSpec,
private val matcher: BitmapMatcher = UnitTestBitmapMatcher,
assetsPathRelativeToBuildRoot: String
) : TestRule {
private val colorsRule = MaterialYouColorsRule()
private val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
protected val screenshotRule =
ScreenshotTestRule(
SystemUIGoldenImagePathManager(
getEmulatedDevicePathConfig(emulationSpec),
assetsPathRelativeToBuildRoot
)
)
private val activityRule = ActivityScenarioRule(ScreenshotActivity::class.java)
private val roboRule =
RuleChain.outerRule(deviceEmulationRule).around(screenshotRule).around(activityRule)
private val delegateRule = RuleChain.outerRule(colorsRule).around(roboRule)
private val isRobolectric = if (Build.FINGERPRINT.contains("robolectric")) true else false
override fun apply(base: Statement, description: Description): Statement {
if (isRobolectric) {
// In robolectric mode, we enable NATIVE graphics and unpack font and icu files.
// We need to use reflection, as this library is only needed and therefore
// only available in deviceless mode.
val nativeLoaderClassName = "org.robolectric.nativeruntime.DefaultNativeRuntimeLoader"
val defaultNativeRuntimeLoader = Class.forName(nativeLoaderClassName)
System.setProperty("robolectric.graphicsMode", "NATIVE")
defaultNativeRuntimeLoader.getMethod("injectAndLoad").invoke(null)
}
val ruleToApply = if (isRobolectric) roboRule else delegateRule
return ruleToApply.apply(base, description)
}
protected fun takeScreenshot(
mode: Mode = Mode.WrapContent,
viewProvider: (ComponentActivity) -> View,
): Bitmap {
activityRule.scenario.onActivity { activity ->
// Make sure that the activity draws full screen and fits the whole display instead of
// the system bars.
val window = activity.window
window.setDecorFitsSystemWindows(false)
// Set the content.
activity.setContentView(viewProvider(activity), mode.layoutParams)
// Elevation/shadows is not deterministic when doing hardware rendering, so we disable
// it for any view in the hierarchy.
window.decorView.removeElevationRecursively()
activity.currentFocus?.clearFocus()
}
// 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).
var contentView: View? = null
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)
contentView = content.getChildAt(0)
}
return if (isRobolectric) {
contentView?.captureToBitmap()?.get(10, TimeUnit.SECONDS)
?: error("timeout while trying to capture view to bitmap")
} else {
contentView?.toBitmap() ?: error("contentView is null")
}
}
/**
* Compare the content of the view provided by [viewProvider] with the golden image identified
* by [goldenIdentifier] in the context of [emulationSpec].
*/
fun screenshotTest(
goldenIdentifier: String,
mode: Mode = Mode.WrapContent,
viewProvider: (ComponentActivity) -> View,
) {
val bitmap = takeScreenshot(mode, viewProvider)
screenshotRule.assertBitmapAgainstGolden(
bitmap,
goldenIdentifier,
matcher,
)
}
/**
* Compare the content of the dialog provided by [dialogProvider] with the golden image
* identified by [goldenIdentifier] in the context of [emulationSpec].
*/
fun dialogScreenshotTest(
goldenIdentifier: String,
dialogProvider: (Activity) -> Dialog,
) {
var dialog: Dialog? = null
activityRule.scenario.onActivity { activity ->
dialog =
dialogProvider(activity).apply {
// Make sure that the dialog draws full screen and fits the whole display
// instead of the system bars.
window.setDecorFitsSystemWindows(false)
// Disable enter/exit animations.
create()
window.setWindowAnimations(0)
// Elevation/shadows is not deterministic when doing hardware rendering, so we
// disable it for any view in the hierarchy.
window.decorView.removeElevationRecursively()
// Show the dialog.
show()
}
}
try {
val bitmap = dialog?.toBitmap() ?: error("dialog is null")
screenshotRule.assertBitmapAgainstGolden(
bitmap,
goldenIdentifier,
matcher,
)
} finally {
dialog?.dismiss()
}
}
private fun Dialog.toBitmap(): Bitmap {
val window = window
return window.decorView.toBitmap(window)
}
enum class Mode(val layoutParams: LayoutParams) {
WrapContent(LayoutParams(WRAP_CONTENT, WRAP_CONTENT)),
MatchSize(LayoutParams(MATCH_PARENT, MATCH_PARENT)),
MatchWidth(LayoutParams(MATCH_PARENT, WRAP_CONTENT)),
MatchHeight(LayoutParams(WRAP_CONTENT, MATCH_PARENT)),
}
}