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 private 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).around(fontsRule).around(timeZoneRule) 60 .around(hardwareRenderingRule).around(commonRule) 61 private val isRobolectric = if (Build.FINGERPRINT.contains("robolectric")) true else false 62 63 var frameLimit = 10 64 65 override fun apply(base: Statement, description: Description): Statement { 66 val ruleToApply = if (isRobolectric) roboRule else deviceRule 67 return ruleToApply.apply(base, description) 68 } 69 70 protected fun takeScreenshot( 71 mode: Mode = Mode.WrapContent, 72 viewProvider: (ComponentActivity) -> View, 73 checkView: (ComponentActivity, View) -> Boolean = { _, _ -> false }, 74 subviewId: Int? = null, 75 ): Bitmap { 76 activityRule.scenario.onActivity { activity -> 77 // Make sure that the activity draws full screen and fits the whole display instead of 78 // the system bars. 79 val window = activity.window 80 window.setDecorFitsSystemWindows(decorFitsSystemWindows) 81 82 // Set the content. 83 val inflatedView = viewProvider(activity) 84 activity.setContentView(inflatedView, mode.layoutParams) 85 86 // Elevation/shadows is not deterministic when doing hardware rendering, so we disable 87 // it for any view in the hierarchy. 88 window.decorView.removeElevationRecursively() 89 90 activity.currentFocus?.clearFocus() 91 } 92 93 // We call onActivity again because it will make sure that our Activity is done measuring, 94 // laying out and drawing its content (that we set in the previous onActivity lambda). 95 var targetView: View? = null 96 var waitForActivity = true 97 var iterCount = 0 98 while (waitForActivity && iterCount < frameLimit) { 99 activityRule.scenario.onActivity { activity -> 100 // Check that the content is what we expected. 101 val content = activity.requireViewById<ViewGroup>(android.R.id.content) 102 assertEquals(1, content.childCount) 103 targetView = 104 fetchTargetView(content, subviewId).also { 105 waitForActivity = checkView(activity, it) 106 } 107 } 108 iterCount++ 109 } 110 111 if (waitForActivity) { 112 throw IllegalStateException( 113 "checkView returned true but frameLimit was reached. Increase the frame limit if " + 114 "more frames are required before the screenshot is taken." 115 ) 116 } 117 118 return targetView?.captureToBitmapAsync()?.get(10, TimeUnit.SECONDS) 119 ?: error("timeout while trying to capture view to bitmap") 120 } 121 122 private fun fetchTargetView(parent: ViewGroup, subviewId: Int?): View = 123 if (subviewId != null) parent.requireViewById(subviewId) else parent.getChildAt(0) 124 125 /** 126 * Compare the content of the view provided by [viewProvider] with the golden image identified 127 * by [goldenIdentifier] in the context of [emulationSpec]. 128 */ 129 fun screenshotTest( 130 goldenIdentifier: String, 131 mode: Mode = Mode.WrapContent, 132 beforeScreenshot: (ComponentActivity) -> Unit = {}, 133 subviewId: Int? = null, 134 viewProvider: (ComponentActivity) -> View, 135 ) = 136 screenshotTest( 137 goldenIdentifier, 138 mode, 139 checkView = { activity, _ -> 140 beforeScreenshot(activity) 141 false 142 }, 143 subviewId, 144 viewProvider 145 ) 146 147 /** 148 * Compare the content of the view provided by [viewProvider] with the golden image identified 149 * by [goldenIdentifier] in the context of [emulationSpec]. 150 */ 151 fun screenshotTest( 152 goldenIdentifier: String, 153 mode: Mode = Mode.WrapContent, 154 checkView: (ComponentActivity, View) -> Boolean, 155 subviewId: Int? = null, 156 viewProvider: (ComponentActivity) -> View, 157 ) { 158 val bitmap = takeScreenshot(mode, viewProvider, checkView, subviewId) 159 screenshotRule.assertBitmapAgainstGolden(bitmap, goldenIdentifier, matcher) 160 } 161 162 /** 163 * Compare the content of the dialog provided by [dialogProvider] with the golden image 164 * identified by [goldenIdentifier] in the context of [emulationSpec]. 165 */ 166 fun dialogScreenshotTest( 167 goldenIdentifier: String, 168 waitForIdle: () -> Unit = {}, 169 dialogProvider: (Activity) -> Dialog, 170 ) { 171 dialogScreenshotTest( 172 activityRule, 173 screenshotRule, 174 matcher, 175 goldenIdentifier, 176 waitForIdle, 177 dialogProvider, 178 ) 179 } 180 181 enum class Mode(val layoutParams: LayoutParams) { 182 WrapContent(LayoutParams(WRAP_CONTENT, WRAP_CONTENT)), 183 MatchSize(LayoutParams(MATCH_PARENT, MATCH_PARENT)), 184 MatchWidth(LayoutParams(MATCH_PARENT, WRAP_CONTENT)), 185 MatchHeight(LayoutParams(WRAP_CONTENT, MATCH_PARENT)), 186 } 187 } 188