1 /*
2  * Copyright 2020 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 androidx.compose.ui.test
18 
19 import android.os.Build
20 import androidx.activity.ComponentActivity
21 import androidx.compose.foundation.background
22 import androidx.compose.foundation.layout.Box
23 import androidx.compose.foundation.layout.Column
24 import androidx.compose.foundation.layout.Row
25 import androidx.compose.foundation.layout.padding
26 import androidx.compose.foundation.layout.size
27 import androidx.compose.material.AlertDialog
28 import androidx.compose.material.Text
29 import androidx.compose.runtime.Composable
30 import androidx.compose.testutils.assertContainsColor
31 import androidx.compose.testutils.assertDoesNotContainColor
32 import androidx.compose.testutils.assertPixels
33 import androidx.compose.ui.Modifier
34 import androidx.compose.ui.graphics.Color
35 import androidx.compose.ui.platform.testTag
36 import androidx.compose.ui.test.junit4.createAndroidComposeRule
37 import androidx.compose.ui.unit.Density
38 import androidx.compose.ui.unit.Dp
39 import androidx.compose.ui.unit.IntOffset
40 import androidx.compose.ui.unit.IntSize
41 import androidx.compose.ui.unit.dp
42 import androidx.compose.ui.window.Dialog
43 import androidx.compose.ui.window.Popup
44 import androidx.test.filters.MediumTest
45 import androidx.test.filters.SdkSuppress
46 import com.google.common.truth.Truth.assertThat
47 import kotlin.math.roundToInt
48 import org.junit.Ignore
49 import org.junit.Rule
50 import org.junit.Test
51 import org.junit.runner.RunWith
52 import org.junit.runners.Parameterized
53 
54 @MediumTest
55 @RunWith(Parameterized::class)
56 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
57 class BitmapCapturingTest(val config: TestConfig) {
58     data class TestConfig(val activityClass: Class<out ComponentActivity>)
59 
60     companion object {
61         @JvmStatic
62         @Parameterized.Parameters(name = "{0}")
createTestSetnull63         fun createTestSet(): List<TestConfig> =
64             listOf(
65                 TestConfig(ComponentActivity::class.java),
66                 TestConfig(CustomComposeHostActivity::class.java)
67             )
68     }
69 
70     @get:Rule val rule = createAndroidComposeRule(config.activityClass)
71 
72     private val rootTag = "Root"
73     private val tagTopLeft = "TopLeft"
74     private val tagTopRight = "TopRight"
75     private val tagBottomLeft = "BottomLeft"
76     private val tagBottomRight = "BottomRight"
77 
78     private val colorTopLeft = Color.Red
79     private val colorTopRight = Color.Blue
80     private val colorBottomLeft = Color.Green
81     private val colorBottomRight = Color.Yellow
82     private val colorBg = Color.Black
83 
84     @Test
85     fun captureIndividualRects_checkSizeAndColors() {
86         composeCheckerboard()
87 
88         var calledCount = 0
89         rule.onNodeWithTag(tagTopLeft).captureToImage().assertPixels(
90             expectedSize = IntSize(100, 50)
91         ) {
92             calledCount++
93             colorTopLeft
94         }
95         assertThat(calledCount).isEqualTo((100 * 50))
96 
97         rule.onNodeWithTag(tagTopRight).captureToImage().assertPixels(
98             expectedSize = IntSize(100, 50)
99         ) {
100             colorTopRight
101         }
102         rule.onNodeWithTag(tagBottomLeft).captureToImage().assertPixels(
103             expectedSize = IntSize(100, 50)
104         ) {
105             colorBottomLeft
106         }
107         rule.onNodeWithTag(tagBottomRight).captureToImage().assertPixels(
108             expectedSize = IntSize(100, 50)
109         ) {
110             colorBottomRight
111         }
112     }
113 
114     @Test
captureRootContainer_checkSizeAndColorsnull115     fun captureRootContainer_checkSizeAndColors() {
116         composeCheckerboard()
117 
118         rule.onNodeWithTag(rootTag).captureToImage().assertPixels(
119             expectedSize = IntSize(200, 100)
120         ) {
121             expectedColorProvider(it)
122         }
123     }
124 
125     @Test
126     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P) // b/163023027
captureDialog_verifyBackgroundnull127     fun captureDialog_verifyBackground() {
128         // Test that we are really able to capture dialogs to bitmap.
129         setContent {
130             AlertDialog(onDismissRequest = {}, confirmButton = {}, backgroundColor = Color.Red)
131         }
132 
133         rule.onNode(isDialog()).captureToImage().assertContainsColor(Color.Red)
134     }
135 
136     @Ignore // b/266737024
137     @Test
capturePopup_verifyBackgroundnull138     fun capturePopup_verifyBackground() {
139         setContent { Box { Popup { Box(Modifier.background(Color.Red)) { Text("Hello") } } } }
140 
141         rule.onNode(isPopup()).captureToImage().assertContainsColor(Color.Red)
142     }
143 
144     @Test
captureComposable_withPopUp_verifyBackgroundnull145     fun captureComposable_withPopUp_verifyBackground() {
146         setContent {
147             Box(Modifier.testTag(rootTag).size(300.dp).background(Color.Yellow)) {
148                 Popup { Box(Modifier.background(Color.Red)) { Text("Hello") } }
149             }
150         }
151 
152         rule
153             .onNodeWithTag(rootTag)
154             .captureToImage()
155             .assertContainsColor(Color.Yellow)
156             .assertDoesNotContainColor(Color.Red)
157     }
158 
159     @Test
captureComposable_withDialog_verifyBackgroundnull160     fun captureComposable_withDialog_verifyBackground() {
161         setContent {
162             Box(Modifier.testTag(rootTag).size(300.dp).background(Color.Yellow)) {
163                 Dialog({}) { Box(Modifier.size(300.dp).background(Color.Red)) { Text("Hello") } }
164             }
165         }
166         rule
167             .onNodeWithTag(rootTag)
168             .captureToImage()
169             .assertContainsColor(Color.Yellow)
170             .assertDoesNotContainColor(Color.Red)
171     }
172 
173     @Test
capturePopup_verifySizenull174     fun capturePopup_verifySize() {
175         val boxSize = 200.dp
176         val boxSizePx = boxSize.toPixel(rule.density).roundToInt()
177         setContent { Box { Popup { Box(Modifier.size(boxSize)) { Text("Hello") } } } }
178 
179         rule.onNode(isPopup()).captureToImage().let {
180             assertThat(IntSize(it.width, it.height)).isEqualTo(IntSize(boxSizePx, boxSizePx))
181         }
182     }
183 
Dpnull184     private fun Dp.toPixel(density: Density) = this.value * density.density
185 
186     private fun expectedColorProvider(pos: IntOffset): Color {
187         if (pos.y < 50) {
188             if (pos.x < 100) {
189                 return colorTopLeft
190             } else if (pos.x < 200) {
191                 return colorTopRight
192             }
193         } else if (pos.y < 100) {
194             if (pos.x < 100) {
195                 return colorBottomLeft
196             } else if (pos.x < 200) {
197                 return colorBottomRight
198             }
199         }
200         throw IllegalArgumentException("Expected color undefined for position $pos")
201     }
202 
composeCheckerboardnull203     private fun composeCheckerboard() {
204         with(rule.density) {
205             setContent {
206                 Box(Modifier.background(colorBg)) {
207                     Box(Modifier.padding(top = 20.toDp()).background(colorBg)) {
208                         Column(Modifier.testTag(rootTag)) {
209                             Row {
210                                 Box(
211                                     Modifier.testTag(tagTopLeft)
212                                         .size(100.toDp(), 50.toDp())
213                                         .background(color = colorTopLeft)
214                                 )
215                                 Box(
216                                     Modifier.testTag(tagTopRight)
217                                         .size(100.toDp(), 50.toDp())
218                                         .background(colorTopRight)
219                                 )
220                             }
221                             Row {
222                                 Box(
223                                     Modifier.testTag(tagBottomLeft)
224                                         .size(100.toDp(), 50.toDp())
225                                         .background(colorBottomLeft)
226                                 )
227                                 Box(
228                                     Modifier.testTag(tagBottomRight)
229                                         .size(100.toDp(), 50.toDp())
230                                         .background(colorBottomRight)
231                                 )
232                             }
233                         }
234                     }
235                 }
236             }
237         }
238     }
239 
setContentnull240     private fun setContent(content: @Composable () -> Unit) {
241         when (val activity = rule.activity) {
242             is CustomComposeHostActivity -> activity.setContent(content)
243             else -> rule.setContent(content)
244         }
245     }
246 }
247