1 /* 2 * Copyright 2019 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 package androidx.compose.material 17 18 import android.os.Build 19 import android.os.Build.VERSION.SDK_INT 20 import androidx.compose.foundation.layout.Box 21 import androidx.compose.foundation.layout.wrapContentSize 22 import androidx.compose.runtime.mutableStateOf 23 import androidx.compose.testutils.assertAgainstGolden 24 import androidx.compose.ui.Alignment 25 import androidx.compose.ui.Modifier 26 import androidx.compose.ui.focus.FocusRequester 27 import androidx.compose.ui.focus.focusRequester 28 import androidx.compose.ui.input.InputMode 29 import androidx.compose.ui.input.InputModeManager 30 import androidx.compose.ui.platform.LocalInputModeManager 31 import androidx.compose.ui.platform.testTag 32 import androidx.compose.ui.state.ToggleableState 33 import androidx.compose.ui.test.captureToImage 34 import androidx.compose.ui.test.isToggleable 35 import androidx.compose.ui.test.junit4.createComposeRule 36 import androidx.compose.ui.test.onNodeWithTag 37 import androidx.compose.ui.test.performMouseInput 38 import androidx.compose.ui.test.performTouchInput 39 import androidx.test.ext.junit.runners.AndroidJUnit4 40 import androidx.test.filters.MediumTest 41 import androidx.test.filters.SdkSuppress 42 import androidx.test.platform.app.InstrumentationRegistry 43 import androidx.test.screenshot.AndroidXScreenshotTestRule 44 import org.junit.After 45 import org.junit.Ignore 46 import org.junit.Rule 47 import org.junit.Test 48 import org.junit.runner.RunWith 49 50 @MediumTest 51 @RunWith(AndroidJUnit4::class) 52 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) 53 class CheckboxScreenshotTest { 54 55 @get:Rule val rule = createComposeRule() 56 57 @get:Rule val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL) 58 59 // TODO(b/267253920): Add a compose test API to set/reset InputMode. 60 @After resetTouchModenull61 fun resetTouchMode() = 62 with(InstrumentationRegistry.getInstrumentation()) { 63 if (SDK_INT < 33) setInTouchMode(true) else resetInTouchMode() 64 } 65 66 val wrap = Modifier.wrapContentSize(Alignment.TopStart) 67 68 // TODO: this test tag as well as Boxes inside testa are temporarty, remove then b/157687898 69 // is fixed 70 private val wrapperTestTag = "checkboxWrapper" 71 72 @Test checkBoxTest_checkednull73 fun checkBoxTest_checked() { 74 rule.setMaterialContent { 75 Box(wrap.testTag(wrapperTestTag)) { Checkbox(checked = true, onCheckedChange = {}) } 76 } 77 assertToggeableAgainstGolden("checkbox_checked") 78 } 79 80 @Test checkBoxTest_uncheckednull81 fun checkBoxTest_unchecked() { 82 rule.setMaterialContent { 83 Box(wrap.testTag(wrapperTestTag)) { 84 Checkbox(modifier = wrap, checked = false, onCheckedChange = {}) 85 } 86 } 87 assertToggeableAgainstGolden("checkbox_unchecked") 88 } 89 90 @Test 91 @Ignore("b/355413615") checkBoxTest_pressednull92 fun checkBoxTest_pressed() { 93 rule.setMaterialContent { 94 Box(wrap.testTag(wrapperTestTag)) { 95 Checkbox(modifier = wrap, checked = false, onCheckedChange = {}) 96 } 97 } 98 99 rule.onNode(isToggleable()).performTouchInput { down(center) } 100 101 // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for 102 // synchronization. Instead just wait until after the ripples are finished animating. 103 Thread.sleep(300) 104 105 assertToggeableAgainstGolden("checkbox_pressed") 106 } 107 108 @Test checkBoxTest_indeterminatenull109 fun checkBoxTest_indeterminate() { 110 rule.setMaterialContent { 111 Box(wrap.testTag(wrapperTestTag)) { 112 TriStateCheckbox( 113 state = ToggleableState.Indeterminate, 114 modifier = wrap, 115 onClick = {} 116 ) 117 } 118 } 119 assertToggeableAgainstGolden("checkbox_indeterminate") 120 } 121 122 @Test checkBoxTest_disabled_checkednull123 fun checkBoxTest_disabled_checked() { 124 rule.setMaterialContent { 125 Box(wrap.testTag(wrapperTestTag)) { 126 Checkbox(modifier = wrap, checked = true, enabled = false, onCheckedChange = {}) 127 } 128 } 129 assertToggeableAgainstGolden("checkbox_disabled_checked") 130 } 131 132 @Test checkBoxTest_disabled_uncheckednull133 fun checkBoxTest_disabled_unchecked() { 134 rule.setMaterialContent { 135 Box(wrap.testTag(wrapperTestTag)) { 136 Checkbox(modifier = wrap, checked = false, enabled = false, onCheckedChange = {}) 137 } 138 } 139 assertToggeableAgainstGolden("checkbox_disabled_unchecked") 140 } 141 142 @Test checkBoxTest_disabled_indeterminatenull143 fun checkBoxTest_disabled_indeterminate() { 144 rule.setMaterialContent { 145 Box(wrap.testTag(wrapperTestTag)) { 146 TriStateCheckbox( 147 state = ToggleableState.Indeterminate, 148 enabled = false, 149 modifier = wrap, 150 onClick = {} 151 ) 152 } 153 } 154 assertToggeableAgainstGolden("checkbox_disabled_indeterminate") 155 } 156 157 @Test checkBoxTest_unchecked_animateToCheckednull158 fun checkBoxTest_unchecked_animateToChecked() { 159 val isChecked = mutableStateOf(false) 160 rule.setMaterialContent { 161 Box(wrap.testTag(wrapperTestTag)) { 162 Checkbox( 163 modifier = wrap, 164 checked = isChecked.value, 165 onCheckedChange = { isChecked.value = it } 166 ) 167 } 168 } 169 170 rule.mainClock.autoAdvance = false 171 172 // Because Ripples are drawn on the RenderThread, it is hard to synchronize them with 173 // Compose animations, so instead just manually change the value instead of triggering 174 // and trying to screenshot a ripple 175 rule.runOnIdle { isChecked.value = true } 176 177 rule.mainClock.advanceTimeByFrame() 178 rule.waitForIdle() // Wait for measure 179 rule.mainClock.advanceTimeBy(milliseconds = 80) 180 181 assertToggeableAgainstGolden("checkbox_animateToChecked") 182 } 183 184 @Test checkBoxTest_checked_animateToUncheckednull185 fun checkBoxTest_checked_animateToUnchecked() { 186 val isChecked = mutableStateOf(true) 187 rule.setMaterialContent { 188 Box(wrap.testTag(wrapperTestTag)) { 189 Checkbox( 190 modifier = wrap, 191 checked = isChecked.value, 192 onCheckedChange = { isChecked.value = it } 193 ) 194 } 195 } 196 197 rule.mainClock.autoAdvance = false 198 199 // Because Ripples are drawn on the RenderThread, it is hard to synchronize them with 200 // Compose animations, so instead just manually change the value instead of triggering 201 // and trying to screenshot a ripple 202 rule.runOnIdle { isChecked.value = false } 203 204 rule.mainClock.advanceTimeByFrame() 205 rule.waitForIdle() // Wait for measure 206 rule.mainClock.advanceTimeBy(milliseconds = 80) 207 208 assertToggeableAgainstGolden("checkbox_animateToUnchecked") 209 } 210 211 @Test checkBoxTest_hovernull212 fun checkBoxTest_hover() { 213 rule.setMaterialContent { 214 Box(wrap.testTag(wrapperTestTag)) { 215 Checkbox(modifier = wrap, checked = true, onCheckedChange = {}) 216 } 217 } 218 219 rule.onNode(isToggleable()).performMouseInput { enter(center) } 220 221 rule.waitForIdle() 222 223 assertToggeableAgainstGolden("checkbox_hover") 224 } 225 226 @Test checkBoxTest_focusnull227 fun checkBoxTest_focus() { 228 val focusRequester = FocusRequester() 229 var localInputModeManager: InputModeManager? = null 230 231 rule.setMaterialContent { 232 localInputModeManager = LocalInputModeManager.current 233 Box(wrap.testTag(wrapperTestTag)) { 234 Checkbox( 235 modifier = wrap.focusRequester(focusRequester), 236 checked = true, 237 onCheckedChange = {} 238 ) 239 } 240 } 241 242 rule.runOnIdle { 243 localInputModeManager!!.requestInputMode(InputMode.Keyboard) 244 focusRequester.requestFocus() 245 } 246 247 rule.waitForIdle() 248 249 assertToggeableAgainstGolden("checkbox_focus") 250 } 251 assertToggeableAgainstGoldennull252 private fun assertToggeableAgainstGolden(goldenName: String) { 253 // TODO: replace with find(isToggeable()) after b/157687898 is fixed 254 rule 255 .onNodeWithTag(wrapperTestTag) 256 .captureToImage() 257 .assertAgainstGolden(screenshotRule, goldenName) 258 } 259 } 260