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