diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java
index b161df701b..db76208ce9 100644
--- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java
+++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java
@@ -24,9 +24,11 @@ import android.graphics.Typeface;
import android.text.Layout.Alignment;
import android.text.Spanned;
import android.text.TextUtils;
+import android.text.style.AbsoluteSizeSpan;
import android.text.style.AlignmentSpan;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
+import android.text.style.RelativeSizeSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
@@ -422,6 +424,88 @@ public final class SpannedSubject extends Subject {
hasNoSpansOfTypeBetween(TypefaceSpan.class, start, end);
}
+ /**
+ * Checks that the subject has a {@link AbsoluteSizeSpan} from {@code start} to {@code end}.
+ *
+ *
The size is asserted in a follow-up method call on the return {@link AbsoluteSized} object.
+ *
+ * @param start The start of the expected span.
+ * @param end The end of the expected span.
+ * @return A {@link AbsoluteSized} object to assert on the size of the matching spans.
+ */
+ @CheckResult
+ public AbsoluteSized hasAbsoluteSizeSpanBetween(int start, int end) {
+ if (actual == null) {
+ failWithoutActual(simpleFact("Spanned must not be null"));
+ return ALREADY_FAILED_ABSOLUTE_SIZED;
+ }
+
+ List absoluteSizeSpans =
+ findMatchingSpans(start, end, AbsoluteSizeSpan.class);
+ if (absoluteSizeSpans.isEmpty()) {
+ failWithExpectedSpan(
+ start, end, AbsoluteSizeSpan.class, actual.toString().substring(start, end));
+ return ALREADY_FAILED_ABSOLUTE_SIZED;
+ }
+ return check("AbsoluteSizeSpan (start=%s,end=%s)", start, end)
+ .about(absoluteSizeSpans(actual))
+ .that(absoluteSizeSpans);
+ }
+
+ /**
+ * Checks that the subject has no {@link AbsoluteSizeSpan}s on any of the text between {@code
+ * start} and {@code end}.
+ *
+ * This fails even if the start and end indexes don't exactly match.
+ *
+ * @param start The start index to start searching for spans.
+ * @param end The end index to stop searching for spans.
+ */
+ public void hasNoAbsoluteSizeSpanBetween(int start, int end) {
+ hasNoSpansOfTypeBetween(AbsoluteSizeSpan.class, start, end);
+ }
+
+ /**
+ * Checks that the subject has a {@link RelativeSizeSpan} from {@code start} to {@code end}.
+ *
+ *
The size is asserted in a follow-up method call on the return {@link RelativeSized} object.
+ *
+ * @param start The start of the expected span.
+ * @param end The end of the expected span.
+ * @return A {@link RelativeSized} object to assert on the size of the matching spans.
+ */
+ @CheckResult
+ public RelativeSized hasRelativeSizeSpanBetween(int start, int end) {
+ if (actual == null) {
+ failWithoutActual(simpleFact("Spanned must not be null"));
+ return ALREADY_FAILED_RELATIVE_SIZED;
+ }
+
+ List relativeSizeSpans =
+ findMatchingSpans(start, end, RelativeSizeSpan.class);
+ if (relativeSizeSpans.isEmpty()) {
+ failWithExpectedSpan(
+ start, end, RelativeSizeSpan.class, actual.toString().substring(start, end));
+ return ALREADY_FAILED_RELATIVE_SIZED;
+ }
+ return check("RelativeSizeSpan (start=%s,end=%s)", start, end)
+ .about(relativeSizeSpans(actual))
+ .that(relativeSizeSpans);
+ }
+
+ /**
+ * Checks that the subject has no {@link RelativeSizeSpan}s on any of the text between {@code
+ * start} and {@code end}.
+ *
+ * This fails even if the start and end indexes don't exactly match.
+ *
+ * @param start The start index to start searching for spans.
+ * @param end The end index to stop searching for spans.
+ */
+ public void hasNoRelativeSizeSpanBetween(int start, int end) {
+ hasNoSpansOfTypeBetween(RelativeSizeSpan.class, start, end);
+ }
+
/**
* Checks that the subject has a {@link RubySpan} from {@code start} to {@code end}.
*
@@ -817,6 +901,105 @@ public final class SpannedSubject extends Subject {
}
}
+ /** Allows assertions about the absolute size of a span. */
+ public interface AbsoluteSized {
+
+ /**
+ * Checks that at least one of the matched spans has the expected {@code size}.
+ *
+ * @param size The expected size.
+ * @return A {@link WithSpanFlags} object for optional additional assertions on the flags.
+ */
+ AndSpanFlags withAbsoluteSize(int size);
+ }
+
+ private static final AbsoluteSized ALREADY_FAILED_ABSOLUTE_SIZED =
+ size -> ALREADY_FAILED_AND_FLAGS;
+
+ private static Factory> absoluteSizeSpans(
+ Spanned actualSpanned) {
+ return (FailureMetadata metadata, List spans) ->
+ new AbsoluteSizeSpansSubject(metadata, spans, actualSpanned);
+ }
+
+ private static final class AbsoluteSizeSpansSubject extends Subject implements AbsoluteSized {
+
+ private final List actualSpans;
+ private final Spanned actualSpanned;
+
+ private AbsoluteSizeSpansSubject(
+ FailureMetadata metadata, List actualSpans, Spanned actualSpanned) {
+ super(metadata, actualSpans);
+ this.actualSpans = actualSpans;
+ this.actualSpanned = actualSpanned;
+ }
+
+ @Override
+ public AndSpanFlags withAbsoluteSize(int size) {
+ List matchingSpanFlags = new ArrayList<>();
+ List spanSizes = new ArrayList<>();
+
+ for (AbsoluteSizeSpan span : actualSpans) {
+ spanSizes.add(span.getSize());
+ if (span.getSize() == size) {
+ matchingSpanFlags.add(actualSpanned.getSpanFlags(span));
+ }
+ }
+
+ check("absoluteSize").that(spanSizes).containsExactly(size);
+ return check("flags").about(spanFlags()).that(matchingSpanFlags);
+ }
+ }
+
+ /** Allows assertions about the relative size of a span. */
+ public interface RelativeSized {
+ /**
+ * Checks that at least one of the matched spans has the expected {@code sizeChange}.
+ *
+ * @param sizeChange The expected size change.
+ * @return A {@link WithSpanFlags} object for optional additional assertions on the flags.
+ */
+ AndSpanFlags withSizeChange(float sizeChange);
+ }
+
+ private static final RelativeSized ALREADY_FAILED_RELATIVE_SIZED =
+ sizeChange -> ALREADY_FAILED_AND_FLAGS;
+
+ private static Factory> relativeSizeSpans(
+ Spanned actualSpanned) {
+ return (FailureMetadata metadata, List spans) ->
+ new RelativeSizeSpansSubject(metadata, spans, actualSpanned);
+ }
+
+ private static final class RelativeSizeSpansSubject extends Subject implements RelativeSized {
+
+ private final List actualSpans;
+ private final Spanned actualSpanned;
+
+ private RelativeSizeSpansSubject(
+ FailureMetadata metadata, List actualSpans, Spanned actualSpanned) {
+ super(metadata, actualSpans);
+ this.actualSpans = actualSpans;
+ this.actualSpanned = actualSpanned;
+ }
+
+ @Override
+ public AndSpanFlags withSizeChange(float size) {
+ List matchingSpanFlags = new ArrayList<>();
+ List spanSizes = new ArrayList<>();
+
+ for (RelativeSizeSpan span : actualSpans) {
+ spanSizes.add(span.getSizeChange());
+ if (span.getSizeChange() == size) {
+ matchingSpanFlags.add(actualSpanned.getSpanFlags(span));
+ }
+ }
+
+ check("sizeChange").that(spanSizes).containsExactly(size);
+ return check("flags").about(spanFlags()).that(matchingSpanFlags);
+ }
+ }
+
/** Allows assertions about a span's ruby text and its position. */
public interface RubyText {
diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java
index e707f6c0f7..75495a4293 100644
--- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java
+++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java
@@ -26,10 +26,12 @@ import android.graphics.Typeface;
import android.text.Layout.Alignment;
import android.text.SpannableString;
import android.text.Spanned;
+import android.text.style.AbsoluteSizeSpan;
import android.text.style.AlignmentSpan;
import android.text.style.AlignmentSpan.Standard;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
+import android.text.style.RelativeSizeSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
@@ -489,6 +491,118 @@ public class SpannedSubjectTest {
checkHasNoSpanFails(new TypefaceSpan("courier"), SpannedSubject::hasNoTypefaceSpanBetween);
}
+ @Test
+ public void absoluteSizeSpan_success() {
+ SpannableString spannable =
+ createSpannable(new AbsoluteSizeSpan(/* size= */ 5), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+
+ assertThat(spannable)
+ .hasAbsoluteSizeSpanBetween(SPAN_START, SPAN_END)
+ .withAbsoluteSize(5)
+ .andFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ }
+
+ @Test
+ public void absoluteSizeSpan_wrongIndex() {
+ checkHasSpanFailsDueToIndexMismatch(
+ new AbsoluteSizeSpan(/* size= */ 5), SpannedSubject::hasAbsoluteSizeSpanBetween);
+ }
+
+ @Test
+ public void absoluteSizeSpan_wrongSize() {
+ SpannableString spannable = createSpannable(new AbsoluteSizeSpan(/* size= */ 5));
+
+ AssertionError expected =
+ expectFailure(
+ whenTesting ->
+ whenTesting
+ .that(spannable)
+ .hasAbsoluteSizeSpanBetween(SPAN_START, SPAN_END)
+ .withAbsoluteSize(4));
+
+ assertThat(expected).factValue("value of").contains("absoluteSize");
+ assertThat(expected).factValue("expected").contains("4");
+ assertThat(expected).factValue("but was").contains("5");
+ }
+
+ @Test
+ public void absoluteSizeSpan_wrongFlags() {
+ checkHasSpanFailsDueToFlagMismatch(
+ new AbsoluteSizeSpan(/* size= */ 5),
+ (subject, start, end) ->
+ subject.hasAbsoluteSizeSpanBetween(start, end).withAbsoluteSize(5));
+ }
+
+ @Test
+ public void noAbsoluteSizeSpan_success() {
+ SpannableString spannable =
+ createSpannableWithUnrelatedSpanAnd(new AbsoluteSizeSpan(/* size= */ 5));
+
+ assertThat(spannable).hasNoAbsoluteSizeSpanBetween(UNRELATED_SPAN_START, UNRELATED_SPAN_END);
+ }
+
+ @Test
+ public void noAbsoluteSizeSpan_failure() {
+ checkHasNoSpanFails(
+ new AbsoluteSizeSpan(/* size= */ 5), SpannedSubject::hasNoAbsoluteSizeSpanBetween);
+ }
+
+ @Test
+ public void relativeSizeSpan_success() {
+ SpannableString spannable =
+ createSpannable(
+ new RelativeSizeSpan(/* proportion= */ 5), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+
+ assertThat(spannable)
+ .hasRelativeSizeSpanBetween(SPAN_START, SPAN_END)
+ .withSizeChange(5)
+ .andFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ }
+
+ @Test
+ public void relativeSizeSpan_wrongIndex() {
+ checkHasSpanFailsDueToIndexMismatch(
+ new RelativeSizeSpan(/* proportion= */ 5), SpannedSubject::hasRelativeSizeSpanBetween);
+ }
+
+ @Test
+ public void relativeSizeSpan_wrongSize() {
+ SpannableString spannable = createSpannable(new RelativeSizeSpan(/* proportion= */ 5));
+
+ AssertionError expected =
+ expectFailure(
+ whenTesting ->
+ whenTesting
+ .that(spannable)
+ .hasRelativeSizeSpanBetween(SPAN_START, SPAN_END)
+ .withSizeChange(4));
+
+ assertThat(expected).factValue("value of").contains("sizeChange");
+ assertThat(expected).factValue("expected").contains("4");
+ assertThat(expected).factValue("but was").contains("5");
+ }
+
+ @Test
+ public void relativeSizeSpan_wrongFlags() {
+ checkHasSpanFailsDueToFlagMismatch(
+ new RelativeSizeSpan(/* proportion= */ 5),
+ (subject, start, end) -> subject.hasRelativeSizeSpanBetween(start, end).withSizeChange(5));
+ }
+
+ @Test
+ public void noRelativeSizeSpan_success() {
+ SpannableString spannable =
+ createSpannableWithUnrelatedSpanAnd(new RelativeSizeSpan(/* proportion= */ 5));
+
+ assertThat(spannable).hasNoRelativeSizeSpanBetween(UNRELATED_SPAN_START, UNRELATED_SPAN_END);
+ }
+
+ @Test
+ public void noRelativeSizeSpan_failure() {
+ checkHasNoSpanFails(
+ new RelativeSizeSpan(/* proportion= */ 5), SpannedSubject::hasNoRelativeSizeSpanBetween);
+ }
+
@Test
public void rubySpan_success() {
SpannableString spannable =