1 /*
<lambda>null2  * Copyright 2021 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.glance.appwidget
18 
19 import android.graphics.PorterDuffColorFilter
20 import android.graphics.drawable.ColorDrawable
21 import android.os.Build
22 import android.view.Gravity
23 import android.view.View
24 import android.widget.FrameLayout
25 import android.widget.ImageView
26 import android.widget.LinearLayout
27 import android.widget.TextView
28 import androidx.annotation.ColorInt
29 import androidx.annotation.Px
30 import androidx.compose.ui.graphics.Color
31 import androidx.compose.ui.graphics.toArgb
32 import androidx.compose.ui.unit.Dp
33 import androidx.core.view.children
34 import androidx.glance.layout.Alignment
35 import com.google.common.truth.FailureMetadata
36 import com.google.common.truth.Subject
37 import com.google.common.truth.Truth.assertAbout
38 import com.google.common.truth.Truth.assertThat
39 import kotlin.test.assertIs
40 import kotlin.test.assertNotNull
41 import org.robolectric.Shadows.shadowOf
42 
43 internal open class ViewSubject(metaData: FailureMetadata, private val actual: View?) :
44     Subject(metaData, actual) {
45     fun hasBackgroundColor(@ColorInt color: Int) {
46         isNotNull()
47         actual!!
48         check("getBackground()").that(actual.background).isInstanceOf(ColorDrawable::class.java)
49         val background = actual.background as ColorDrawable
50         // Comparing the hex string representation is equivalent to comparing the int, and the
51         // error message is a lot more readable with the hex string if this fails.
52         check("getBackground().getColor()")
53             .that(Integer.toHexString(background.color))
54             .isEqualTo(Integer.toHexString(color))
55     }
56 
57     fun hasBackgroundColor(hexString: String) =
58         hasBackgroundColor(android.graphics.Color.parseColor(hexString))
59 
60     fun hasLayoutParamsWidth(@Px px: Int) {
61         check("getLayoutParams().width").that(actual?.layoutParams?.width).isEqualTo(px)
62     }
63 
64     fun hasLayoutParamsWidth(dp: Dp) {
65         assertNotNull(actual)
66         hasLayoutParamsWidth(dp.toPixels(actual.context))
67     }
68 
69     fun hasLayoutParamsHeight(@Px px: Int) {
70         check("getLayoutParams().height").that(actual?.layoutParams?.height).isEqualTo(px)
71     }
72 
73     fun hasLayoutParamsHeight(dp: Dp) {
74         assertNotNull(actual)
75         hasLayoutParamsHeight(dp.toPixels(actual.context))
76     }
77 
78     companion object {
79         fun views(): Factory<ViewSubject, View> {
80             return Factory<ViewSubject, View> { metadata, actual -> ViewSubject(metadata, actual) }
81         }
82 
83         fun assertThat(view: View?): ViewSubject = assertAbout(views()).that(view)
84     }
85 }
86 
87 internal open class TextViewSubject(metaData: FailureMetadata, private val actual: TextView?) :
88     ViewSubject(metaData, actual) {
hasTextColornull89     fun hasTextColor(@ColorInt color: Int) {
90         isNotNull()
91         actual!!
92         // Comparing the hex string representation is equivalent to comparing the int, and the
93         // error message is a lot more readable with the hex string if this fails.
94         check("getCurrentTextColor()")
95             .that(Integer.toHexString(actual.currentTextColor))
96             .isEqualTo(Integer.toHexString(color))
97     }
98 
hasTextColornull99     fun hasTextColor(hexString: String) = hasTextColor(android.graphics.Color.parseColor(hexString))
100 
101     companion object {
102         fun textViews(): Factory<TextViewSubject, TextView> {
103             return Factory<TextViewSubject, TextView> { metadata, actual ->
104                 TextViewSubject(metadata, actual)
105             }
106         }
107 
108         fun assertThat(view: TextView?): TextViewSubject = assertAbout(textViews()).that(view)
109     }
110 }
111 
112 internal open class ImageViewSubject(metaData: FailureMetadata, private val actual: ImageView?) :
113     ViewSubject(metaData, actual) {
hasColorFilternull114     fun hasColorFilter(@ColorInt color: Int) {
115         assertNotNull(actual)
116         val colorFilter = actual.colorFilter
117         assertIs<PorterDuffColorFilter>(colorFilter)
118 
119         check("getColorFilter().getColor()")
120             .that(Integer.toHexString(shadowOf(colorFilter).color))
121             .isEqualTo(Integer.toHexString(color))
122     }
123 
hasColorFilternull124     fun hasColorFilter(color: Color) {
125         hasColorFilter(color.toArgb())
126     }
127 
hasColorFilternull128     fun hasColorFilter(color: String) {
129         hasColorFilter(android.graphics.Color.parseColor(color))
130     }
131 
132     companion object {
imageViewsnull133         fun imageViews(): Factory<ImageViewSubject, ImageView> {
134             return Factory<ImageViewSubject, ImageView> { metadata, actual ->
135                 ImageViewSubject(metadata, actual)
136             }
137         }
138 
assertThatnull139         fun assertThat(view: ImageView?): ImageViewSubject = assertAbout(imageViews()).that(view)
140     }
141 }
142 
143 internal open class FrameLayoutSubject(
144     metaData: FailureMetadata,
145     private val actual: FrameLayout?,
146 ) : ViewSubject(metaData, actual) {
147     fun hasContentAlignment(alignment: Alignment) {
148         assertNotNull(actual)
149         if (actual.childCount == 0) {
150             return
151         }
152         check("children.getLayoutParams().gravity")
153             .that(
154                 actual.children
155                     .map { view -> assertIs<FrameLayout.LayoutParams>(view.layoutParams).gravity }
156                     .toSet()
157             )
158             .containsExactly(alignment.toGravity())
159     }
160 
161     companion object {
162         fun frameLayouts(): Factory<FrameLayoutSubject, FrameLayout> {
163             return Factory<FrameLayoutSubject, FrameLayout> { metadata, actual ->
164                 FrameLayoutSubject(metadata, actual)
165             }
166         }
167 
168         fun assertThat(view: FrameLayout?): FrameLayoutSubject =
169             assertAbout(frameLayouts()).that(view)
170     }
171 }
172 
173 internal open class LinearLayoutSubject(
174     metaData: FailureMetadata,
175     private val actual: LinearLayout?,
176 ) : ViewSubject(metaData, actual) {
hasContentAlignmentnull177     fun hasContentAlignment(alignment: Alignment.Vertical) {
178         assertNotNull(actual)
179 
180         // On S+ the ViewStub child views aren't used for rows and columns, so the alignment is set
181         // only on the outer layout.
182         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
183             check("getGravity()")
184                 .that(actual.gravity and Gravity.VERTICAL_GRAVITY_MASK)
185                 .isEqualTo(alignment.toGravity())
186             return
187         }
188 
189         if (actual.orientation == LinearLayout.VERTICAL) {
190             // LinearLayout.getGravity was introduced on Android N, prior to that, you could set the
191             // gravity, but not read it back.
192             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
193                 check("getGravity()")
194                     .that(actual.gravity and Gravity.VERTICAL_GRAVITY_MASK)
195                     .isEqualTo(alignment.toGravity())
196             }
197             return
198         }
199         if (actual.childCount == 0) {
200             return
201         }
202         check("children.getLayoutParams().gravity")
203             .that(
204                 actual.children
205                     .map { view ->
206                         assertIs<LinearLayout.LayoutParams>(view.layoutParams).gravity and
207                             Gravity.VERTICAL_GRAVITY_MASK
208                     }
209                     .toSet()
210             )
211             .containsExactly(alignment.toGravity())
212     }
213 
hasContentAlignmentnull214     fun hasContentAlignment(alignment: Alignment.Horizontal) {
215         assertNotNull(actual)
216 
217         // On S+ the ViewStub child views aren't used for rows and columns, so the alignment is set
218         // only on the outer layout.
219         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
220             check("getGravity()")
221                 .that(actual.gravity and Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)
222                 .isEqualTo(alignment.toGravity())
223             return
224         }
225 
226         if (actual.orientation == LinearLayout.HORIZONTAL) {
227             // LinearLayout.getGravity was introduced on Android N, prior to that, you could set the
228             // gravity, but not read it back.
229             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
230                 check("getGravity()")
231                     .that(actual.gravity and Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)
232                     .isEqualTo(alignment.toGravity())
233             }
234             return
235         }
236         if (actual.childCount == 0) {
237             return
238         }
239         check("children.getLayoutParams().gravity")
240             .that(
241                 actual.children
242                     .map { view ->
243                         assertIs<LinearLayout.LayoutParams>(view.layoutParams).gravity and
244                             Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK
245                     }
246                     .toSet()
247             )
248             .containsExactly(alignment.toGravity())
249     }
250 
hasContentAlignmentnull251     fun hasContentAlignment(alignment: Alignment) {
252         hasContentAlignment(alignment.horizontal)
253         hasContentAlignment(alignment.vertical)
254     }
255 
256     companion object {
linearLayoutsnull257         fun linearLayouts(): Factory<LinearLayoutSubject, LinearLayout> {
258             return Factory<LinearLayoutSubject, LinearLayout> { metadata, actual ->
259                 LinearLayoutSubject(metadata, actual)
260             }
261         }
262 
assertThatnull263         fun assertThat(view: LinearLayout?): LinearLayoutSubject =
264             assertAbout(linearLayouts()).that(view)
265     }
266 }
267 
268 internal class ColorSubject(metaData: FailureMetadata, private val actual: Color?) :
269     Subject(metaData, actual) {
270     fun isSameColorAs(string: String) {
271         isEqualTo(Color(android.graphics.Color.parseColor(string)))
272     }
273 
274     fun isSameColorAs(color: Color) {
275         isEqualTo(color)
276     }
277 
278     override fun isEqualTo(expected: Any?) {
279         assertThat(actual.toHexString()).isEqualTo(expected.toHexString())
280     }
281 
282     override fun isNotEqualTo(unexpected: Any?) {
283         assertThat(actual.toHexString()).isNotEqualTo(unexpected.toHexString())
284     }
285 
286     private fun Any?.toHexString(): Any? = if (this is Color?) this.toHexString() else this
287 
288     private fun Color?.toHexString(): String? = this?.let { Integer.toHexString(it.toArgb()) }
289 
290     companion object {
291         fun colors(): Factory<ColorSubject, Color> {
292             return Factory<ColorSubject, Color> { metadata, actual ->
293                 ColorSubject(metadata, actual)
294             }
295         }
296 
297         fun assertThat(color: Color?): ColorSubject = assertAbout(colors()).that(color)
298     }
299 }
300