1 /*
2  * 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 @file:Suppress("DEPRECATION")
18 
19 package com.google.accompanist.themeadapter.material
20 
21 import android.view.ContextThemeWrapper
22 import androidx.annotation.StyleRes
23 import androidx.appcompat.app.AppCompatActivity
24 import androidx.compose.foundation.shape.CornerSize
25 import androidx.compose.foundation.shape.CutCornerShape
26 import androidx.compose.foundation.shape.RoundedCornerShape
27 import androidx.compose.material.LocalContentColor
28 import androidx.compose.material.MaterialTheme
29 import androidx.compose.material.Typography
30 import androidx.compose.runtime.Composable
31 import androidx.compose.runtime.CompositionLocalProvider
32 import androidx.compose.ui.geometry.Size
33 import androidx.compose.ui.platform.LocalContext
34 import androidx.compose.ui.platform.LocalDensity
35 import androidx.compose.ui.res.colorResource
36 import androidx.compose.ui.test.junit4.createAndroidComposeRule
37 import androidx.compose.ui.text.font.Font
38 import androidx.compose.ui.text.font.FontFamily
39 import androidx.compose.ui.text.font.FontWeight
40 import androidx.compose.ui.text.font.toFontFamily
41 import androidx.compose.ui.unit.Density
42 import androidx.compose.ui.unit.Dp
43 import androidx.compose.ui.unit.TextUnit
44 import androidx.compose.ui.unit.dp
45 import androidx.compose.ui.unit.em
46 import androidx.compose.ui.unit.sp
47 import androidx.test.filters.SdkSuppress
48 import com.google.accompanist.themeadapter.core.FontFamilyWithWeight
49 import com.google.accompanist.themeadapter.material.test.R
50 import org.junit.Assert.assertEquals
51 import org.junit.Assert.assertNotEquals
52 import org.junit.Assert.assertNotNull
53 import org.junit.Assert.assertNull
54 import org.junit.Assert.assertTrue
55 import org.junit.Rule
56 import org.junit.Test
57 
58 /**
59  * Class which contains the majority of the tests. This class is extended
60  * in both the `androidTest` and `test` source sets for setup of the relevant
61  * test runner.
62  */
63 abstract class BaseMdcThemeTest<T : AppCompatActivity>(
64     activityClass: Class<T>
65 ) {
66     @get:Rule
67     val composeTestRule = createAndroidComposeRule(activityClass)
68 
69     @Test
<lambda>null70     fun colors() = composeTestRule.setContent {
71         MdcTheme {
72             val color = MaterialTheme.colors
73 
74             assertEquals(colorResource(R.color.aquamarine), color.primary)
75             assertEquals(colorResource(R.color.royal_blue), color.primaryVariant)
76             assertEquals(colorResource(R.color.midnight_blue), color.onPrimary)
77 
78             assertEquals(colorResource(R.color.dark_golden_rod), color.secondary)
79             assertEquals(colorResource(R.color.slate_gray), color.onSecondary)
80             assertEquals(colorResource(R.color.blue_violet), color.secondaryVariant)
81 
82             assertEquals(colorResource(R.color.spring_green), color.surface)
83             assertEquals(colorResource(R.color.navy), color.onSurface)
84 
85             assertEquals(colorResource(R.color.dark_salmon), color.error)
86             assertEquals(colorResource(R.color.beige), color.onError)
87 
88             assertEquals(colorResource(R.color.light_coral), color.background)
89             assertEquals(colorResource(R.color.orchid), color.onBackground)
90 
91             // MdcTheme updates the LocalContentColor to match the calculated onBackground
92             assertEquals(colorResource(R.color.orchid), LocalContentColor.current)
93         }
94     }
95 
96     @Test
<lambda>null97     fun shapes() = composeTestRule.setContent {
98         MdcTheme {
99             val shapes = MaterialTheme.shapes
100             val density = LocalDensity.current
101 
102             shapes.small.run {
103                 assertTrue(this is CutCornerShape)
104                 assertEquals(4f, topStart.toPx(density))
105                 assertEquals(9.dp.scaleToPx(density), topEnd.toPx(density))
106                 assertEquals(5f, bottomEnd.toPx(density))
107                 assertEquals(3.dp.scaleToPx(density), bottomStart.toPx(density))
108             }
109             shapes.medium.run {
110                 assertTrue(this is RoundedCornerShape)
111                 assertEquals(12.dp.scaleToPx(density), topStart.toPx(density))
112                 assertEquals(12.dp.scaleToPx(density), topEnd.toPx(density))
113                 assertEquals(12.dp.scaleToPx(density), bottomEnd.toPx(density))
114                 assertEquals(12.dp.scaleToPx(density), bottomStart.toPx(density))
115             }
116             shapes.large.run {
117                 assertTrue(this is CutCornerShape)
118                 assertEquals(0f, topStart.toPx(density))
119                 assertEquals(0f, topEnd.toPx(density))
120                 assertEquals(0f, bottomEnd.toPx(density))
121                 assertEquals(0f, bottomStart.toPx(density))
122             }
123         }
124     }
125 
126     @Test
<lambda>null127     fun type() = composeTestRule.setContent {
128         MdcTheme {
129             val typography = MaterialTheme.typography
130             val density = LocalDensity.current
131 
132             val rubik300 = Font(R.font.rubik_300).toFontFamily()
133             val rubik400 = Font(R.font.rubik_400).toFontFamily()
134             val rubik500 = Font(R.font.rubik_500).toFontFamily()
135             val sansSerif = FontFamilyWithWeight(FontFamily.SansSerif)
136             val sansSerifLight = FontFamilyWithWeight(FontFamily.SansSerif, FontWeight.Light)
137             val sansSerifBlack = FontFamilyWithWeight(FontFamily.SansSerif, FontWeight.Black)
138             val serif = FontFamilyWithWeight(FontFamily.Serif)
139             val cursive = FontFamilyWithWeight(FontFamily.Cursive)
140             val monospace = FontFamilyWithWeight(FontFamily.Monospace)
141 
142             typography.h1.run {
143                 assertTextUnitEquals(97.54.sp, fontSize, density)
144                 assertTextUnitEquals((-0.0015).em, letterSpacing, density)
145                 assertEquals(rubik300, fontFamily)
146             }
147 
148             assertNotNull(typography.h2.shadow)
149             typography.h2.shadow!!.run {
150                 assertEquals(colorResource(R.color.olive_drab), color)
151                 assertEquals(4.43f, offset.x)
152                 assertEquals(8.19f, offset.y)
153                 assertEquals(2.13f, blurRadius)
154             }
155 
156             typography.h3.run {
157                 assertEquals(sansSerif.fontFamily, fontFamily)
158                 assertEquals(sansSerif.weight, fontWeight)
159             }
160 
161             typography.h4.run {
162                 assertEquals(sansSerifLight.fontFamily, fontFamily)
163                 assertEquals(sansSerifLight.weight, fontWeight)
164             }
165 
166             typography.h5.run {
167                 assertEquals(sansSerifBlack.fontFamily, fontFamily)
168                 assertEquals(sansSerifBlack.weight, fontWeight)
169             }
170 
171             typography.h6.run {
172                 assertEquals(serif.fontFamily, fontFamily)
173                 assertEquals(serif.weight, fontWeight)
174             }
175 
176             typography.body1.run {
177                 assertTextUnitEquals(16.26.sp, fontSize, density)
178                 assertTextUnitEquals(0.005.em, letterSpacing, density)
179                 assertEquals(rubik400, fontFamily)
180                 assertNull(shadow)
181             }
182 
183             typography.body2.run {
184                 assertEquals(cursive.fontFamily, fontFamily)
185                 assertEquals(cursive.weight, fontWeight)
186             }
187 
188             typography.subtitle1.run {
189                 assertEquals(monospace.fontFamily, fontFamily)
190                 assertEquals(monospace.weight, fontWeight)
191                 assertTextUnitEquals(0.em, letterSpacing, density)
192             }
193 
194             typography.subtitle2.run {
195                 assertEquals(FontFamily.SansSerif, fontFamily)
196             }
197 
198             typography.button.run {
199                 assertEquals(rubik500, fontFamily)
200             }
201 
202             typography.caption.run {
203                 assertEquals(FontFamily.SansSerif, fontFamily)
204                 assertTextUnitEquals(0.04.em, letterSpacing, density)
205             }
206 
207             typography.overline.run {
208                 assertEquals(FontFamily.SansSerif, fontFamily)
209             }
210         }
211     }
212 
213     @Test
214     @SdkSuppress(minSdkVersion = 23) // XML font families with >1 fonts are only supported on API 23+
<lambda>null215     fun type_rubik_family_api23() = composeTestRule.setContent {
216         val rubik = FontFamily(
217             Font(R.font.rubik_300, FontWeight.W300),
218             Font(R.font.rubik_400, FontWeight.W400),
219             Font(R.font.rubik_500, FontWeight.W500),
220             Font(R.font.rubik_700, FontWeight.W700),
221         )
222         WithThemeOverlay(R.style.ThemeOverlay_MdcThemeTest_DefaultFontFamily_Rubik) {
223             MdcTheme(setDefaultFontFamily = true) {
224                 MaterialTheme.typography.assertFontFamilies(expected = rubik)
225             }
226         }
227         WithThemeOverlay(R.style.ThemeOverlay_MdcThemeTest_DefaultAndroidFontFamily_Rubik) {
228             MdcTheme(setDefaultFontFamily = true) {
229                 MaterialTheme.typography.assertFontFamilies(expected = rubik)
230             }
231         }
232     }
233 
234     @Test
<lambda>null235     fun type_rubik_fixed400() = composeTestRule.setContent {
236         val rubik400 = Font(R.font.rubik_400, FontWeight.W400).toFontFamily()
237         WithThemeOverlay(R.style.ThemeOverlay_MdcThemeTest_DefaultFontFamily_Rubik400) {
238             MdcTheme(setDefaultFontFamily = true) {
239                 MaterialTheme.typography.assertFontFamilies(expected = rubik400)
240             }
241         }
242         WithThemeOverlay(R.style.ThemeOverlay_MdcThemeTest_DefaultAndroidFontFamily_Rubik400) {
243             MdcTheme(setDefaultFontFamily = true) {
244                 MaterialTheme.typography.assertFontFamilies(expected = rubik400)
245             }
246         }
247     }
248 
249     @Test
<lambda>null250     fun type_rubik_fixed700_withTextAppearances() = composeTestRule.setContent {
251         val rubik700 = Font(R.font.rubik_700, FontWeight.W700).toFontFamily()
252         WithThemeOverlay(
253             R.style.ThemeOverlay_MdcThemeTest_DefaultFontFamilies_Rubik700_WithTextAppearances
254         ) {
255             MdcTheme {
256                 MaterialTheme.typography.assertFontFamilies(
257                     expected = rubik700,
258                     notEquals = true
259                 )
260             }
261         }
262     }
263 }
264 
Dpnull265 private fun Dp.scaleToPx(density: Density): Float {
266     val dp = this
267     return with(density) { dp.toPx() }
268 }
269 
assertTextUnitEqualsnull270 private fun assertTextUnitEquals(expected: TextUnit, actual: TextUnit, density: Density) {
271     if (expected.javaClass == actual.javaClass) {
272         // If the expected and actual are the same type, compare the raw values with a
273         // delta to account for float inaccuracy
274         assertEquals(expected.value, actual.value, 0.001f)
275     } else {
276         // Otherwise we need to flatten to a px to compare the values. Again using a
277         // delta to account for float inaccuracy
278         with(density) { assertEquals(expected.toPx(), actual.toPx(), 0.001f) }
279     }
280 }
281 
toPxnull282 private fun CornerSize.toPx(density: Density) = toPx(Size.Unspecified, density)
283 
284 internal fun Typography.assertFontFamilies(
285     expected: FontFamily,
286     notEquals: Boolean = false
287 ) {
288     if (notEquals) assertNotEquals(expected, h1.fontFamily) else assertEquals(expected, h1.fontFamily)
289     if (notEquals) assertNotEquals(expected, h2.fontFamily) else assertEquals(expected, h2.fontFamily)
290     if (notEquals) assertNotEquals(expected, h3.fontFamily) else assertEquals(expected, h3.fontFamily)
291     if (notEquals) assertNotEquals(expected, h4.fontFamily) else assertEquals(expected, h4.fontFamily)
292     if (notEquals) assertNotEquals(expected, h5.fontFamily) else assertEquals(expected, h5.fontFamily)
293     if (notEquals) assertNotEquals(expected, h6.fontFamily) else assertEquals(expected, h6.fontFamily)
294     if (notEquals) assertNotEquals(expected, subtitle1.fontFamily) else assertEquals(expected, subtitle1.fontFamily)
295     if (notEquals) assertNotEquals(expected, subtitle2.fontFamily) else assertEquals(expected, subtitle2.fontFamily)
296     if (notEquals) assertNotEquals(expected, body1.fontFamily) else assertEquals(expected, body1.fontFamily)
297     if (notEquals) assertNotEquals(expected, body2.fontFamily) else assertEquals(expected, body2.fontFamily)
298     if (notEquals) assertNotEquals(expected, button.fontFamily) else assertEquals(expected, button.fontFamily)
299     if (notEquals) assertNotEquals(expected, caption.fontFamily) else assertEquals(expected, caption.fontFamily)
300     if (notEquals) assertNotEquals(expected, overline.fontFamily) else assertEquals(expected, overline.fontFamily)
301 }
302 
303 /**
304  * Function which applies an Android theme overlay to the current context.
305  */
306 @Composable
WithThemeOverlaynull307 fun WithThemeOverlay(
308     @StyleRes themeOverlayId: Int,
309     content: @Composable () -> Unit,
310 ) {
311     val themedContext = ContextThemeWrapper(LocalContext.current, themeOverlayId)
312     CompositionLocalProvider(LocalContext provides themedContext, content = content)
313 }
314