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