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