1 /*
<lambda>null2  * Copyright 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  *      https://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 com.google.accompanist.testharness
18 
19 import android.content.res.Configuration
20 import android.content.res.Configuration.UI_MODE_NIGHT_MASK
21 import android.content.res.Configuration.UI_MODE_NIGHT_NO
22 import android.content.res.Configuration.UI_MODE_NIGHT_YES
23 import androidx.activity.ComponentActivity
24 import androidx.compose.foundation.background
25 import androidx.compose.foundation.layout.Box
26 import androidx.compose.foundation.layout.requiredSize
27 import androidx.compose.foundation.layout.size
28 import androidx.compose.foundation.text.BasicText
29 import androidx.compose.runtime.Composable
30 import androidx.compose.runtime.CompositionLocalProvider
31 import androidx.compose.ui.Modifier
32 import androidx.compose.ui.graphics.Color
33 import androidx.compose.ui.layout.LayoutCoordinates
34 import androidx.compose.ui.layout.onGloballyPositioned
35 import androidx.compose.ui.platform.LocalConfiguration
36 import androidx.compose.ui.platform.LocalDensity
37 import androidx.compose.ui.platform.LocalLayoutDirection
38 import androidx.compose.ui.res.stringResource
39 import androidx.compose.ui.test.junit4.createAndroidComposeRule
40 import androidx.compose.ui.test.onNodeWithText
41 import androidx.compose.ui.unit.Dp
42 import androidx.compose.ui.unit.DpSize
43 import androidx.compose.ui.unit.LayoutDirection
44 import androidx.compose.ui.unit.dp
45 import androidx.core.os.LocaleListCompat
46 import androidx.test.ext.junit.runners.AndroidJUnit4
47 import androidx.test.filters.SdkSuppress
48 import com.google.accompanist.testharness.test.R
49 import org.junit.Assert.assertEquals
50 import org.junit.Assert.assertNotEquals
51 import org.junit.Rule
52 import org.junit.Test
53 import org.junit.runner.RunWith
54 import java.util.Locale
55 
56 @RunWith(AndroidJUnit4::class)
57 class TestHarnessTest {
58     @get:Rule
59     val composeTestRule = createAndroidComposeRule<ComponentActivity>()
60 
61     @Test
62     fun size_SmallerThanOuterBox_measuredWidthIsCorrect() {
63         var width = 0.dp
64         composeTestRule.setContent {
65             Box(Modifier.requiredSize(300.dp)) {
66                 TestHarness(size = DpSize(200.dp, 200.dp)) {
67                     BoxOfSize(200.dp, onWidth = { width = it })
68                 }
69             }
70         }
71         composeTestRule.waitForIdle()
72 
73         val ratio = width / 200.dp
74         assertEquals(ratio, 1f, 0.01f)
75     }
76 
77     @Test
78     fun size_BiggerThanOuterBox_measuredWidthIsCorrect() {
79         var width = 0.dp
80         composeTestRule.setContent {
81             Box(Modifier.requiredSize(100.dp)) {
82                 TestHarness(size = DpSize(200.dp, 200.dp)) {
83                     BoxOfSize(200.dp, onWidth = { width = it })
84                 }
85             }
86         }
87         composeTestRule.waitForIdle()
88 
89         val ratio = width / 200.dp
90         assertEquals(ratio, 1f, 0.01f)
91     }
92 
93     @Test
94     fun size_ExtremelyBig_measuredWidthIsCorrect() {
95         var width = 0.dp
96         composeTestRule.setContent {
97             TestHarness(size = DpSize(10000.dp, 10000.dp)) {
98                 BoxOfSize(10000.dp, onWidth = { width = it })
99             }
100         }
101         composeTestRule.waitForIdle()
102 
103         val ratio = width / 10000.dp
104         assertEquals(ratio, 1f, 0.01f)
105     }
106 
107     @Test
108     fun darkMode_enabled() {
109         var darkMode: Int = -1
110         composeTestRule.setContent {
111             TestHarness(darkMode = true) {
112                 darkMode = LocalConfiguration.current.uiMode
113             }
114         }
115         composeTestRule.waitForIdle()
116 
117         assertEquals(darkMode and UI_MODE_NIGHT_MASK, UI_MODE_NIGHT_YES)
118     }
119 
120     @Test
121     fun darkMode_disabled() {
122         var darkMode: Int = -1
123         composeTestRule.setContent {
124             TestHarness(darkMode = false) {
125                 darkMode = LocalConfiguration.current.uiMode
126             }
127         }
128         composeTestRule.waitForIdle()
129 
130         assertEquals(darkMode and UI_MODE_NIGHT_MASK, UI_MODE_NIGHT_NO)
131     }
132 
133     @Test
134     @SdkSuppress(minSdkVersion = 24)
135     fun locales_api24_allLocalesApplied() {
136         val expectedLocales = LocaleListCompat.create(Locale.CANADA, Locale.ITALY)
137         lateinit var locales: LocaleListCompat
138         composeTestRule.setContent {
139             TestHarness(locales = expectedLocales) {
140                 locales = LocaleListCompat.wrap(LocalConfiguration.current.locales)
141             }
142         }
143 
144         composeTestRule.waitForIdle()
145 
146         // All locales are expected in Sdk>=24
147         assertEquals(expectedLocales, locales)
148     }
149 
150     @Test
151     fun usLocale_usesCorrectResource() {
152         composeTestRule.setContent {
153             TestHarness(locales = LocaleListCompat.forLanguageTags("us")) {
154                 BasicText(text = stringResource(R.string.this_is_content, "abc"))
155             }
156         }
157         composeTestRule.onNodeWithText("This is content\nabc").assertExists()
158     }
159 
160     @Test
161     fun arLocale_usesCorrectResource() {
162         composeTestRule.setContent {
163             TestHarness(locales = LocaleListCompat.forLanguageTags("ar")) {
164                 BasicText(text = stringResource(R.string.this_is_content, "abc"))
165             }
166         }
167         composeTestRule.onNodeWithText("هذا مضمون \nabc").assertExists()
168     }
169 
170     @Test
171     fun layoutDirection_RtlLocale_usesOverride() {
172         lateinit var direction: LayoutDirection
173         val initialLocale = LocaleListCompat.create(Locale("ar")) // Arabic
174         val initialLayoutDirection = LayoutDirection.Ltr
175 
176         // Given test harness setting an RTL Locale but it also forcing the opposite
177         // layout direction
178         composeTestRule.setContent {
179             TestHarness(
180                 layoutDirection = initialLayoutDirection,
181                 locales = initialLocale
182             ) {
183                 direction = LocalLayoutDirection.current
184             }
185         }
186         composeTestRule.waitForIdle()
187 
188         // The used locale should be the one overriden with the test harness, ignoring the Locale's.
189         assertEquals(initialLayoutDirection, direction)
190     }
191 
192     @Test
193     fun layoutDirection_default_RtlLocale() {
194         lateinit var direction: LayoutDirection
195         val initialLocale = LocaleListCompat.create(Locale("ar")) // Arabic
196 
197         // Given an initial layout direction, when the test harness sets an RTL Locale and doesn't
198         // force the layout direction
199         composeTestRule.setContent {
200             TestHarness(
201                 layoutDirection = null,
202                 locales = initialLocale
203             ) {
204                 direction = LocalLayoutDirection.current
205             }
206         }
207         composeTestRule.waitForIdle()
208 
209         // The used locale should be the Locale's.
210         assertEquals(LayoutDirection.Rtl, direction)
211     }
212 
213     @Test
214     fun layoutDirection_default_usesLocales() {
215         lateinit var direction: LayoutDirection
216         val initialLocale = LocaleListCompat.create(Locale("ar")) // Arabic
217         val initialLayoutDirection = LayoutDirection.Ltr
218 
219         // Given no layout direction, when the test harness sets an RTL Locale with an initial
220         // LTR direction
221         composeTestRule.setContent {
222             CompositionLocalProvider(
223                 LocalLayoutDirection provides initialLayoutDirection
224             ) {
225                 TestHarness(
226                     layoutDirection = null,
227                     locales = initialLocale
228                 ) {
229                     direction = LocalLayoutDirection.current
230                 }
231             }
232         }
233         composeTestRule.waitForIdle()
234 
235         // The default should be the one provided by the Locale
236         assertNotEquals(initialLayoutDirection, direction)
237     }
238 
239     @Test
240     fun layoutDirection_setLtr() {
241         lateinit var direction: LayoutDirection
242         val initialLayoutDirection = LayoutDirection.Rtl
243         val expected = LayoutDirection.Ltr
244 
245         // Given a content with an initial RTL layout direction, when the test harness overrides it
246         composeTestRule.setContent {
247             CompositionLocalProvider(
248                 LocalLayoutDirection provides initialLayoutDirection
249             ) {
250                 TestHarness(layoutDirection = expected) {
251                     direction = LocalLayoutDirection.current
252                 }
253             }
254         }
255         composeTestRule.waitForIdle()
256 
257         // The direction should be the one forced by the test harness
258         assertEquals(expected, direction)
259     }
260 
261     @Test
262     fun layoutDirection_setRtl() {
263         lateinit var direction: LayoutDirection
264         val initialLayoutDirection = LayoutDirection.Ltr
265         val expected = LayoutDirection.Rtl
266 
267         // Given a content with an initial RTL layout direction, when the test harness overrides it
268         composeTestRule.setContent {
269             CompositionLocalProvider(
270                 LocalLayoutDirection provides initialLayoutDirection
271             ) {
272                 TestHarness(layoutDirection = expected) {
273                     direction = LocalLayoutDirection.current
274                 }
275             }
276         }
277         composeTestRule.waitForIdle()
278 
279         // The direction should be the one forced by the test harness
280         assertEquals(expected, direction)
281     }
282 
283     @Test
284     fun layoutDirection_default_followsLocaleLtr() {
285         lateinit var direction: LayoutDirection
286 
287         // Given an initial layout direction and no overrides
288         composeTestRule.setContent {
289             CompositionLocalProvider(
290                 LocalConfiguration provides Configuration().apply {
291                     setLocale(Locale.ENGLISH)
292                 }
293             ) {
294                 TestHarness(layoutDirection = null) {
295                     direction = LocalLayoutDirection.current
296                 }
297             }
298         }
299         composeTestRule.waitForIdle()
300 
301         // The direction should be set by the Locale
302         assertEquals(LayoutDirection.Ltr, direction)
303     }
304 
305     @Test
306     fun layoutDirection_default_followsLocaleRtl() {
307         lateinit var direction: LayoutDirection
308 
309         // Given an initial layout direction and no overrides
310         composeTestRule.setContent {
311             CompositionLocalProvider(
312                 LocalConfiguration provides Configuration().apply {
313                     setLocale(Locale("ar"))
314                 }
315             ) {
316                 TestHarness(layoutDirection = null) {
317                     direction = LocalLayoutDirection.current
318                 }
319             }
320         }
321         composeTestRule.waitForIdle()
322 
323         // The direction should be set by the Locale
324         assertEquals(LayoutDirection.Rtl, direction)
325     }
326 
327     @Test
328     fun fontScale() {
329         val expectedFontScale = 5f
330         var fontScale = 0f
331         // Given
332         composeTestRule.setContent {
333             TestHarness(fontScale = expectedFontScale) {
334                 fontScale = LocalConfiguration.current.fontScale
335             }
336         }
337 
338         composeTestRule.waitForIdle()
339 
340         assertEquals(expectedFontScale, fontScale)
341     }
342 
343     @Test
344     @SdkSuppress(minSdkVersion = 31)
345     fun fontWeightAdjustment() {
346         val expectedFontWeightAdjustment = 10
347         var fontWeightAdjustment = 0
348         composeTestRule.setContent {
349             TestHarness(fontWeightAdjustment = expectedFontWeightAdjustment) {
350                 fontWeightAdjustment = LocalConfiguration.current.fontWeightAdjustment
351             }
352         }
353 
354         composeTestRule.waitForIdle()
355 
356         assertEquals(expectedFontWeightAdjustment, fontWeightAdjustment)
357     }
358 
359     @Test
360     @SdkSuppress(minSdkVersion = 23) // SCREENLAYOUT_ROUND_YES supported on API 23+
361     fun isScreenRound() {
362         var defaultRound: Boolean? = null
363         var forcedRound: Boolean? = null
364         var forcedNotRound: Boolean? = null
365 
366         composeTestRule.setContent {
367             defaultRound = LocalConfiguration.current.isScreenRound
368             TestHarness(isScreenRound = false) {
369                 TestHarness(isScreenRound = true) {
370                     forcedRound = LocalConfiguration.current.isScreenRound
371                     TestHarness(isScreenRound = false) {
372                         forcedNotRound = LocalConfiguration.current.isScreenRound
373                     }
374                 }
375             }
376         }
377 
378         assertEquals(
379             composeTestRule.activity.resources.configuration.isScreenRound,
380             defaultRound
381         )
382         assertEquals(true, forcedRound)
383         assertEquals(false, forcedNotRound)
384     }
385 
386     @Composable
387     private fun BoxOfSize(size: Dp, onWidth: (Dp) -> Unit) {
388         val localDensity = LocalDensity.current
389         Box(
390             Modifier
391                 .size(size)
392                 .background(color = Color.Black)
393                 .onGloballyPositioned { it: LayoutCoordinates ->
394                     onWidth(with(localDensity) { it.size.width.toDp() })
395                 }
396         )
397     }
398 }
399