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 =