1 /* <lambda>null2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package platform.test.screenshot 18 19 import android.app.Activity 20 import android.app.Dialog 21 import android.graphics.Bitmap 22 import android.os.Build 23 import android.view.View 24 import android.view.ViewGroup 25 import android.view.ViewGroup.LayoutParams 26 import android.view.ViewGroup.LayoutParams.MATCH_PARENT 27 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT 28 import androidx.activity.ComponentActivity 29 import androidx.test.ext.junit.rules.ActivityScenarioRule 30 import java.util.concurrent.TimeUnit 31 import org.junit.Assert.assertEquals 32 import org.junit.rules.RuleChain 33 import org.junit.rules.TestRule 34 import org.junit.runner.Description 35 import org.junit.runners.model.Statement 36 import platform.test.screenshot.matchers.BitmapMatcher 37 38 /** A rule for View screenshot diff unit tests. */ 39 open class ViewScreenshotTestRule( 40 private val emulationSpec: DeviceEmulationSpec, 41 pathManager: GoldenPathManager, 42 private val matcher: BitmapMatcher = UnitTestBitmapMatcher, 43 private val decorFitsSystemWindows: Boolean = false, 44 protected val screenshotRule: ScreenshotTestRule = ScreenshotTestRule(pathManager), 45 ) : TestRule, BitmapDiffer by screenshotRule, ScreenshotAsserterFactory by screenshotRule { 46 private val colorsRule = MaterialYouColorsRule() 47 private val fontsRule = FontsRule() 48 private val timeZoneRule = TimeZoneRule() 49 private val hardwareRenderingRule = HardwareRenderingRule() 50 private val deviceEmulationRule = DeviceEmulationRule(emulationSpec) 51 private val activityRule = ActivityScenarioRule(ScreenshotActivity::class.java) 52 private val commonRule = 53 RuleChain.outerRule(deviceEmulationRule).around(screenshotRule).around(activityRule) 54 55 // As denoted in `MaterialYouColorsRule` and `FontsRule`, these two rules need to come first, 56 // though their relative orders are not critical. 57 private val deviceRule = RuleChain.outerRule(colorsRule).around(commonRule) 58 private val roboRule = 59 RuleChain.outerRule(colorsRule) 60 .around(fontsRule) 61 .around(timeZoneRule) 62 .around(hardwareRenderingRule) 63 .around(commonRule) 64 private val isRobolectric = if (Build.FINGERPRINT.contains("robolectric")) true else false 65 66 var frameLimit = 10 67 68 override fun apply(base: Statement, description: Description): Statement { 69 val ruleToApply = if (isRobolectric) roboRule else deviceRule 70 return ruleToApply.apply(base, description) 71 } 72 73 protected fun takeScreenshot( 74 mode: Mode = Mode.WrapContent, 75 viewProvider: (ComponentActivity) -> View, 76 checkView: (ComponentActivity, View) -> Boolean = { _, _ -> false }, 77 subviewId: Int? = null, 78 ): Bitmap { 79 activityRule.scenario.onActivity { activity -> 80 // Make sure that the activity draws full screen and fits the whole display instead of 81 // the system bars. 82 val window = activity.window 83 window.setDecorFitsSystemWindows(decorFitsSystemWindows) 84 85 // Set the content. 86 val inflatedView = viewProvider(activity) 87 activity.setContentView(inflatedView, mode.layoutParams) 88 89 // Elevation/shadows is not deterministic when doing hardware rendering, so we disable 90 // it for any view in the hierarchy. 91 window.decorView.removeElevationRecursively() 92 93 activity.currentFocus?.clearFocus() 94 } 95 96 // We call onActivity again because it will make sure that our Activity is done measuring, 97 // laying out and drawing its content (that we set in the previous onActivity lambda). 98 var targetView: View? = null 99 var waitForActivity = true 100 var iterCount = 0 101 while (waitForActivity && iterCount < frameLimit) { 102 activityRule.scenario.onActivity { activity -> 103 // Check that the content is what we expected. 104 val content = activity.requireViewById<ViewGroup>(android.R.id.content) 105 assertEquals(1, content.childCount) 106 targetView = 107 fetchTargetView(content, subviewId).also { 108 waitForActivity = checkView(activity, it) 109 } 110 } 111 iterCount++ 112 } 113 114 if (waitForActivity) { 115 throw IllegalStateException( 116 "checkView returned true but frameLimit was reached. Increase the frame limit if " + 117 "more frames are required before the screenshot is taken." 118 ) 119 } 120 121 return targetView?.captureToBitmapAsync()?.get(10, TimeUnit.SECONDS) 122 ?: error("timeout while trying to capture view to bitmap") 123 } 124 125 private fun fetchTargetView(parent: ViewGroup, subviewId: Int?): View = 126 if (subviewId != null) parent.requireViewById(subviewId) else parent.getChildAt(0) 127 128 /** 129 * Compare the content of the view provided by [viewProvider] with the golden image identified 130 * by [goldenIdentifier] in the context of [emulationSpec]. 131 */ 132 fun screenshotTest( 133 goldenIdentifier: String, 134 mode: Mode = Mode.WrapContent, 135 beforeScreenshot: (ComponentActivity) -> Unit = {}, 136 subviewId: Int? = null, 137 viewProvider: (ComponentActivity) -> View, 138 ) = 139 screenshotTest( 140 goldenIdentifier, 141 mode, 142 checkView = { activity, _ -> 143 beforeScreenshot(activity) 144 false 145 }, 146 subviewId, 147 viewProvider, 148 ) 149 150 /** 151 * Compare the content of the view provided by [viewProvider] with the golden image identified 152 * by [goldenIdentifier] in the context of [emulationSpec]. 153 */ 154 fun screenshotTest( 155 goldenIdentifier: String, 156 mode: Mode = Mode.WrapContent, 157 checkView: (ComponentActivity, View) -> Boolean, 158 subviewId: Int? = null, 159 viewProvider: (ComponentActivity) -> View, 160 ) { 161 val bitmap = takeScreenshot(mode, viewProvider, checkView, subviewId) 162 screenshotRule.assertBitmapAgainstGolden(bitmap, goldenIdentifier, matcher) 163 } 164 165 /** 166 * Compare the content of the dialog provided by [dialogProvider] with the golden image 167 * identified by [goldenIdentifier] in the context of [emulationSpec]. 168 */ 169 fun dialogScreenshotTest( 170 goldenIdentifier: String, 171 waitForIdle: () -> Unit = {}, 172 waitingForDialog: (Dialog) -> Boolean = { _ -> false }, 173 dialogProvider: (Activity) -> Dialog, 174 ) { 175 dialogScreenshotTest( 176 activityRule, 177 screenshotRule, 178 matcher, 179 goldenIdentifier, 180 waitForIdle, 181 dialogProvider, 182 waitingForDialog, 183 ) 184 } 185 186 enum class Mode(val layoutParams: LayoutParams) { 187 WrapContent(LayoutParams(WRAP_CONTENT, WRAP_CONTENT)), 188 MatchSize(LayoutParams(MATCH_PARENT, MATCH_PARENT)), 189 MatchWidth(LayoutParams(MATCH_PARENT, WRAP_CONTENT)), 190 MatchHeight(LayoutParams(WRAP_CONTENT, MATCH_PARENT)), 191 } 192 } 193