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