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