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.translators 18 19 import android.content.Context 20 import android.content.res.Configuration 21 import android.graphics.Typeface 22 import android.os.Build 23 import android.text.Layout 24 import android.text.SpannedString 25 import android.text.style.AlignmentSpan 26 import android.text.style.StrikethroughSpan 27 import android.text.style.StyleSpan 28 import android.text.style.TextAppearanceSpan 29 import android.text.style.TypefaceSpan 30 import android.text.style.UnderlineSpan 31 import android.view.Gravity 32 import android.widget.LinearLayout 33 import android.widget.TextView 34 import androidx.compose.ui.graphics.Color 35 import androidx.compose.ui.unit.sp 36 import androidx.glance.GlanceModifier 37 import androidx.glance.appwidget.TextViewSubject.Companion.assertThat 38 import androidx.glance.appwidget.applyRemoteViews 39 import androidx.glance.appwidget.configurationContext 40 import androidx.glance.appwidget.nonGoneChildCount 41 import androidx.glance.appwidget.nonGoneChildren 42 import androidx.glance.appwidget.runAndTranslate 43 import androidx.glance.appwidget.runAndTranslateInRtl 44 import androidx.glance.appwidget.test.R 45 import androidx.glance.appwidget.toPixels 46 import androidx.glance.color.ColorProvider 47 import androidx.glance.layout.Column 48 import androidx.glance.layout.fillMaxWidth 49 import androidx.glance.semantics.contentDescription 50 import androidx.glance.semantics.semantics 51 import androidx.glance.text.FontFamily 52 import androidx.glance.text.FontStyle 53 import androidx.glance.text.FontWeight 54 import androidx.glance.text.Text 55 import androidx.glance.text.TextAlign 56 import androidx.glance.text.TextDecoration 57 import androidx.glance.text.TextStyle 58 import androidx.glance.unit.ColorProvider 59 import androidx.test.core.app.ApplicationProvider 60 import com.google.common.truth.Truth.assertThat 61 import kotlin.test.assertIs 62 import kotlinx.coroutines.ExperimentalCoroutinesApi 63 import kotlinx.coroutines.test.TestScope 64 import kotlinx.coroutines.test.runTest 65 import org.junit.Before 66 import org.junit.Test 67 import org.junit.runner.RunWith 68 import org.robolectric.RobolectricTestRunner 69 import org.robolectric.annotation.Config 70 71 @OptIn(ExperimentalCoroutinesApi::class) 72 @RunWith(RobolectricTestRunner::class) 73 class TextTranslatorTest { 74 75 private lateinit var fakeCoroutineScope: TestScope 76 private val context = ApplicationProvider.getApplicationContext<Context>() 77 private val lightContext = configurationContext { uiMode = Configuration.UI_MODE_NIGHT_NO } 78 private val darkContext = configurationContext { uiMode = Configuration.UI_MODE_NIGHT_YES } 79 private val displayMetrics = context.resources.displayMetrics 80 81 @Before 82 fun setUp() { 83 fakeCoroutineScope = TestScope() 84 } 85 86 @Test 87 fun canTranslateText() = 88 fakeCoroutineScope.runTest { 89 val rv = context.runAndTranslate { Text("test") } 90 val view = context.applyRemoteViews(rv) 91 92 assertIs<TextView>(view) 93 assertThat(view.text.toString()).isEqualTo("test") 94 } 95 96 @Test 97 @Config(sdk = [23, 29]) 98 fun canTranslateText_withStyleWeightAndSize() = 99 fakeCoroutineScope.runTest { 100 val rv = 101 context.runAndTranslate { 102 Text( 103 "test", 104 style = TextStyle(fontWeight = FontWeight.Medium, fontSize = 12.sp), 105 ) 106 } 107 val view = context.applyRemoteViews(rv) 108 109 assertIs<TextView>(view) 110 assertThat(view.textSize).isEqualTo(12.sp.toPixels(displayMetrics)) 111 val content = view.text as SpannedString 112 assertThat(content.toString()).isEqualTo("test") 113 content.checkSingleSpan<TextAppearanceSpan> { 114 if (Build.VERSION.SDK_INT >= 29) { 115 assertThat(it.textFontWeight).isEqualTo(FontWeight.Medium.value) 116 // Note: textStyle is always set, but to NORMAL if unspecified 117 assertThat(it.textStyle).isEqualTo(Typeface.NORMAL) 118 } else { 119 assertThat(it.textStyle).isEqualTo(Typeface.BOLD) 120 } 121 } 122 } 123 124 @Test 125 fun canTranslateText_withMonoFontFamily() = 126 fakeCoroutineScope.runTest { 127 val rv = 128 context.runAndTranslate { 129 Text( 130 "test", 131 style = TextStyle(fontFamily = FontFamily.Monospace), 132 ) 133 } 134 val view = context.applyRemoteViews(rv) 135 136 assertIs<TextView>(view) 137 val content = view.text as SpannedString 138 assertThat(content.toString()).isEqualTo("test") 139 content.checkSingleSpan<TypefaceSpan> { span -> 140 assertThat(span.family).isEqualTo("monospace") 141 } 142 } 143 144 @Test 145 fun canTranslateText_withMonoSerifFamily() = 146 fakeCoroutineScope.runTest { 147 val rv = 148 context.runAndTranslate { 149 Text( 150 "test", 151 style = TextStyle(fontFamily = FontFamily.Serif), 152 ) 153 } 154 val view = context.applyRemoteViews(rv) 155 156 assertIs<TextView>(view) 157 val content = view.text as SpannedString 158 assertThat(content.toString()).isEqualTo("test") 159 content.checkSingleSpan<TypefaceSpan> { span -> 160 assertThat(span.family).isEqualTo("serif") 161 } 162 } 163 164 @Test 165 fun canTranslateText_withSansFontFamily() = 166 fakeCoroutineScope.runTest { 167 val rv = 168 context.runAndTranslate { 169 Text( 170 "test", 171 style = TextStyle(fontFamily = FontFamily.SansSerif), 172 ) 173 } 174 val view = context.applyRemoteViews(rv) 175 176 assertIs<TextView>(view) 177 val content = view.text as SpannedString 178 assertThat(content.toString()).isEqualTo("test") 179 content.checkSingleSpan<TypefaceSpan> { span -> 180 assertThat(span.family).isEqualTo("sans-serif") 181 } 182 } 183 184 @Test 185 fun canTranslateText_withCursiveFontFamily() = 186 fakeCoroutineScope.runTest { 187 val rv = 188 context.runAndTranslate { 189 Text( 190 "test", 191 style = TextStyle(fontFamily = FontFamily.Cursive), 192 ) 193 } 194 val view = context.applyRemoteViews(rv) 195 196 assertIs<TextView>(view) 197 val content = view.text as SpannedString 198 assertThat(content.toString()).isEqualTo("test") 199 content.checkSingleSpan<TypefaceSpan> { span -> 200 assertThat(span.family).isEqualTo("cursive") 201 } 202 } 203 204 @Test 205 fun canTranslateText_withCustomFontFamily() = 206 fakeCoroutineScope.runTest { 207 val rv = 208 context.runAndTranslate { 209 Text( 210 "test", 211 style = TextStyle(fontFamily = FontFamily("casual")), 212 ) 213 } 214 val view = context.applyRemoteViews(rv) 215 216 assertIs<TextView>(view) 217 val content = view.text as SpannedString 218 assertThat(content.toString()).isEqualTo("test") 219 content.checkSingleSpan<TypefaceSpan> { span -> 220 assertThat(span.family).isEqualTo("casual") 221 } 222 } 223 224 @Test 225 fun canTranslateText_withStyleStrikeThrough() = 226 fakeCoroutineScope.runTest { 227 val rv = 228 context.runAndTranslate { 229 Text("test", style = TextStyle(textDecoration = TextDecoration.LineThrough)) 230 } 231 val view = context.applyRemoteViews(rv) 232 233 assertIs<TextView>(view) 234 val content = view.text as SpannedString 235 assertThat(content.toString()).isEqualTo("test") 236 content.checkSingleSpan<StrikethroughSpan> {} 237 } 238 239 @Test 240 fun canTranslateText_withStyleUnderline() = 241 fakeCoroutineScope.runTest { 242 val rv = 243 context.runAndTranslate { 244 Text("test", style = TextStyle(textDecoration = TextDecoration.Underline)) 245 } 246 val view = context.applyRemoteViews(rv) 247 248 assertIs<TextView>(view) 249 val content = view.text as SpannedString 250 assertThat(content.toString()).isEqualTo("test") 251 content.checkSingleSpan<UnderlineSpan> {} 252 } 253 254 @Test 255 fun canTranslateText_withStyleItalic() = 256 fakeCoroutineScope.runTest { 257 val rv = 258 context.runAndTranslate { 259 Text("test", style = TextStyle(fontStyle = FontStyle.Italic)) 260 } 261 val view = context.applyRemoteViews(rv) 262 263 assertIs<TextView>(view) 264 val content = view.text as SpannedString 265 assertThat(content.toString()).isEqualTo("test") 266 content.checkSingleSpan<StyleSpan> { assertThat(it.style).isEqualTo(Typeface.ITALIC) } 267 } 268 269 @Test 270 @Config(sdk = [23, 29]) 271 fun canTranslateText_withComplexStyle() = 272 fakeCoroutineScope.runTest { 273 val rv = 274 context.runAndTranslate { 275 Text( 276 "test", 277 style = 278 TextStyle( 279 textDecoration = 280 TextDecoration.Underline + TextDecoration.LineThrough, 281 fontStyle = FontStyle.Italic, 282 fontWeight = FontWeight.Bold, 283 ), 284 ) 285 } 286 val view = context.applyRemoteViews(rv) 287 288 assertIs<TextView>(view) 289 val content = view.text as SpannedString 290 assertThat(content.toString()).isEqualTo("test") 291 assertThat(content.getSpans(0, content.length, Any::class.java)).hasLength(4) 292 content.checkHasSingleTypedSpan<UnderlineSpan> {} 293 content.checkHasSingleTypedSpan<StrikethroughSpan> {} 294 content.checkHasSingleTypedSpan<StyleSpan> { 295 assertThat(it.style).isEqualTo(Typeface.ITALIC) 296 } 297 content.checkHasSingleTypedSpan<TextAppearanceSpan> { 298 if (Build.VERSION.SDK_INT >= 29) { 299 assertThat(it.textFontWeight).isEqualTo(FontWeight.Bold.value) 300 // Note: textStyle is always set, but to NORMAL if unspecified 301 assertThat(it.textStyle).isEqualTo(Typeface.NORMAL) 302 } else { 303 assertThat(it.textStyle).isEqualTo(Typeface.BOLD) 304 } 305 } 306 } 307 308 @Test 309 fun canTranslateText_withAlignments() = 310 fakeCoroutineScope.runTest { 311 val rv = 312 context.runAndTranslate { 313 Column(modifier = GlanceModifier.fillMaxWidth()) { 314 Text("Center", style = TextStyle(textAlign = TextAlign.Center)) 315 Text("Left", style = TextStyle(textAlign = TextAlign.Left)) 316 Text("Right", style = TextStyle(textAlign = TextAlign.Right)) 317 Text("Start", style = TextStyle(textAlign = TextAlign.Start)) 318 Text("End", style = TextStyle(textAlign = TextAlign.End)) 319 } 320 } 321 val view = context.applyRemoteViews(rv) 322 323 assertIs<LinearLayout>(view) 324 assertThat(view.nonGoneChildCount).isEqualTo(5) 325 val (center, left, right, start, end) = view.nonGoneChildren.toList() 326 assertIs<TextView>(center) 327 assertIs<TextView>(left) 328 assertIs<TextView>(right) 329 assertIs<TextView>(start) 330 assertIs<TextView>(end) 331 332 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 333 assertThat(center.horizontalGravity).isEqualTo(Gravity.CENTER_HORIZONTAL) 334 assertThat(left.horizontalGravity).isEqualTo(Gravity.LEFT) 335 assertThat(right.horizontalGravity).isEqualTo(Gravity.RIGHT) 336 assertThat(start.horizontalGravity).isEqualTo(Gravity.START) 337 assertThat(end.horizontalGravity).isEqualTo(Gravity.END) 338 } else { 339 assertIs<SpannedString>(center.text).checkSingleSpan<AlignmentSpan.Standard> { 340 assertThat(it.alignment).isEqualTo(Layout.Alignment.ALIGN_CENTER) 341 } 342 assertIs<SpannedString>(left.text).checkSingleSpan<AlignmentSpan.Standard> { 343 assertThat(it.alignment).isEqualTo(Layout.Alignment.ALIGN_NORMAL) 344 } 345 assertIs<SpannedString>(right.text).checkSingleSpan<AlignmentSpan.Standard> { 346 assertThat(it.alignment).isEqualTo(Layout.Alignment.ALIGN_OPPOSITE) 347 } 348 assertIs<SpannedString>(start.text).checkSingleSpan<AlignmentSpan.Standard> { 349 assertThat(it.alignment).isEqualTo(Layout.Alignment.ALIGN_NORMAL) 350 } 351 assertIs<SpannedString>(end.text).checkSingleSpan<AlignmentSpan.Standard> { 352 assertThat(it.alignment).isEqualTo(Layout.Alignment.ALIGN_OPPOSITE) 353 } 354 } 355 } 356 357 @Test 358 fun canTranslateText_withAlignmentsInRtl() = 359 fakeCoroutineScope.runTest { 360 val rv = 361 context.runAndTranslateInRtl { 362 Column(modifier = GlanceModifier.fillMaxWidth()) { 363 Text("Center", style = TextStyle(textAlign = TextAlign.Center)) 364 Text("Left", style = TextStyle(textAlign = TextAlign.Left)) 365 Text("Right", style = TextStyle(textAlign = TextAlign.Right)) 366 Text("Start", style = TextStyle(textAlign = TextAlign.Start)) 367 Text("End", style = TextStyle(textAlign = TextAlign.End)) 368 } 369 } 370 val view = context.applyRemoteViews(rv) 371 372 assertIs<LinearLayout>(view) 373 assertThat(view.nonGoneChildCount).isEqualTo(5) 374 val (center, left, right, start, end) = view.nonGoneChildren.toList() 375 assertIs<TextView>(center) 376 assertIs<TextView>(left) 377 assertIs<TextView>(right) 378 assertIs<TextView>(start) 379 assertIs<TextView>(end) 380 381 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 382 assertThat(center.horizontalGravity).isEqualTo(Gravity.CENTER_HORIZONTAL) 383 assertThat(left.horizontalGravity).isEqualTo(Gravity.LEFT) 384 assertThat(right.horizontalGravity).isEqualTo(Gravity.RIGHT) 385 assertThat(start.horizontalGravity).isEqualTo(Gravity.START) 386 assertThat(end.horizontalGravity).isEqualTo(Gravity.END) 387 } else { 388 assertIs<SpannedString>(center.text).checkSingleSpan<AlignmentSpan.Standard> { 389 assertThat(it.alignment).isEqualTo(Layout.Alignment.ALIGN_CENTER) 390 } 391 assertIs<SpannedString>(left.text).checkSingleSpan<AlignmentSpan.Standard> { 392 assertThat(it.alignment).isEqualTo(Layout.Alignment.ALIGN_OPPOSITE) 393 } 394 assertIs<SpannedString>(right.text).checkSingleSpan<AlignmentSpan.Standard> { 395 assertThat(it.alignment).isEqualTo(Layout.Alignment.ALIGN_NORMAL) 396 } 397 assertIs<SpannedString>(start.text).checkSingleSpan<AlignmentSpan.Standard> { 398 assertThat(it.alignment).isEqualTo(Layout.Alignment.ALIGN_NORMAL) 399 } 400 assertIs<SpannedString>(end.text).checkSingleSpan<AlignmentSpan.Standard> { 401 assertThat(it.alignment).isEqualTo(Layout.Alignment.ALIGN_OPPOSITE) 402 } 403 } 404 } 405 406 @Test 407 fun canTranslateText_withColor_fixed() = 408 fakeCoroutineScope.runTest { 409 val rv = 410 context.runAndTranslate { 411 Column { 412 Text("Blue", style = TextStyle(color = ColorProvider(Color.Blue))) 413 Text("Red", style = TextStyle(color = ColorProvider(Color.Red))) 414 } 415 } 416 val view = context.applyRemoteViews(rv) 417 418 assertIs<LinearLayout>(view) 419 assertThat(view.nonGoneChildCount).isEqualTo(2) 420 421 val (blue, red) = view.nonGoneChildren.toList() 422 assertIs<TextView>(blue) 423 assertIs<TextView>(red) 424 assertThat(blue).hasTextColor(android.graphics.Color.BLUE) 425 assertThat(red).hasTextColor(android.graphics.Color.RED) 426 } 427 428 @Config(minSdk = 29) 429 @Test 430 fun canTranslateText_withColor_resource_light() = 431 fakeCoroutineScope.runTest { 432 val rv = 433 lightContext.runAndTranslate { 434 Text("GrayResource", style = TextStyle(color = ColorProvider(R.color.my_color))) 435 } 436 val view = lightContext.applyRemoteViews(rv) 437 438 assertIs<TextView>(view) 439 assertThat(view).hasTextColor("#EEEEEE") 440 } 441 442 @Config(minSdk = 29) 443 @Test 444 fun canTranslateText_withColor_resource_dark() = 445 fakeCoroutineScope.runTest { 446 val rv = 447 darkContext.runAndTranslate { 448 Text("GrayResource", style = TextStyle(color = ColorProvider(R.color.my_color))) 449 } 450 val view = darkContext.applyRemoteViews(rv) 451 452 assertIs<TextView>(view) 453 assertThat(view).hasTextColor("#111111") 454 } 455 456 @Config(minSdk = 29) 457 @Test 458 fun canTranslateText_withColor_dayNight_light() = 459 fakeCoroutineScope.runTest { 460 val rv = 461 lightContext.runAndTranslate { 462 Text( 463 "Green day / Magenta night", 464 style = 465 TextStyle( 466 color = ColorProvider(day = Color.Green, night = Color.Magenta) 467 ) 468 ) 469 } 470 val view = lightContext.applyRemoteViews(rv) 471 472 assertIs<TextView>(view) 473 assertThat(view).hasTextColor(android.graphics.Color.GREEN) 474 } 475 476 @Config(minSdk = 29) 477 @Test 478 fun canTranslateText_withColor_dayNight_dark() = 479 fakeCoroutineScope.runTest { 480 val rv = 481 darkContext.runAndTranslate { 482 Text( 483 "Green day / Magenta night", 484 style = 485 TextStyle( 486 color = ColorProvider(day = Color.Green, night = Color.Magenta) 487 ) 488 ) 489 } 490 val view = darkContext.applyRemoteViews(rv) 491 492 assertIs<TextView>(view) 493 assertThat(view).hasTextColor(android.graphics.Color.MAGENTA) 494 } 495 496 @Test 497 fun canTranslateText_withMaxLines() = 498 fakeCoroutineScope.runTest { 499 val rv = context.runAndTranslate { Text("Max line is set", maxLines = 5) } 500 val view = context.applyRemoteViews(rv) 501 502 assertIs<TextView>(view) 503 assertThat(view.maxLines).isEqualTo(5) 504 } 505 506 @Test 507 fun canTranslateTextWithSemanticsModifier_contentDescription() = 508 fakeCoroutineScope.runTest { 509 val rv = 510 context.runAndTranslate { 511 Text( 512 text = "Max line is set", 513 maxLines = 5, 514 modifier = 515 GlanceModifier.semantics { 516 contentDescription = "Custom text description" 517 }, 518 ) 519 } 520 val view = context.applyRemoteViews(rv) 521 522 assertIs<TextView>(view) 523 assertThat(view.contentDescription).isEqualTo("Custom text description") 524 } 525 526 private val TextView.horizontalGravity 527 get() = this.gravity and Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK 528 529 // Check there is a single span, that it's of the correct type and passes the [check]. 530 private inline fun <reified T> SpannedString.checkSingleSpan(check: (T) -> Unit) { 531 val spans = getSpans(0, length, Any::class.java) 532 assertThat(spans).hasLength(1) 533 checkInstance(spans[0], check) 534 } 535 536 // Check there is a single span of the given type and that it passes the [check]. 537 private inline fun <reified T> SpannedString.checkHasSingleTypedSpan(check: (T) -> Unit) { 538 val spans = getSpans(0, length, T::class.java) 539 assertThat(spans).hasLength(1) 540 check(spans[0]) 541 } 542 543 private inline fun <reified T> checkInstance(obj: Any, check: (T) -> Unit) { 544 assertIs<T>(obj) 545 check(obj) 546 } 547 } 548