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 db1c984635..b161df701b 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 @@ -21,10 +21,13 @@ import static com.google.common.truth.Fact.simpleFact; import static com.google.common.truth.Truth.assertAbout; import android.graphics.Typeface; +import android.text.Layout.Alignment; import android.text.Spanned; import android.text.TextUtils; +import android.text.style.AlignmentSpan; import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; +import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; @@ -220,6 +223,84 @@ public final class SpannedSubject extends Subject { hasNoSpansOfTypeBetween(UnderlineSpan.class, start, end); } + /** + * Checks that the subject has an {@link StrikethroughSpan} from {@code start} to {@code end}. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. + */ + public WithSpanFlags hasStrikethroughSpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_WITH_FLAGS; + } + + List strikethroughSpans = + findMatchingSpans(start, end, StrikethroughSpan.class); + if (strikethroughSpans.size() == 1) { + return check("StrikethroughSpan (start=%s,end=%s)", start, end) + .about(spanFlags()) + .that(Collections.singletonList(actual.getSpanFlags(strikethroughSpans.get(0)))); + } + failWithExpectedSpan( + start, end, StrikethroughSpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_WITH_FLAGS; + } + + /** + * Checks that the subject has no {@link StrikethroughSpan}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 hasNoStrikethroughSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(StrikethroughSpan.class, start, end); + } + + /** + * Checks that the subject has a {@link AlignmentSpan} from {@code start} to {@code end}. + * + *

The alignment is asserted in a follow-up method call on the return {@link Aligned} object. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link Aligned} object to assert on the alignment of the matching spans. + */ + @CheckResult + public Aligned hasAlignmentSpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_ALIGNED; + } + + List alignmentSpans = findMatchingSpans(start, end, AlignmentSpan.class); + if (alignmentSpans.isEmpty()) { + failWithExpectedSpan( + start, end, AlignmentSpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_ALIGNED; + } + return check("AlignmentSpan (start=%s,end=%s)", start, end) + .about(alignmentSpans(actual)) + .that(alignmentSpans); + } + + /** + * Checks that the subject has no {@link AlignmentSpan}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 hasNoAlignmentSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(AlignmentSpan.class, start, end); + } + /** * Checks that the subject has a {@link ForegroundColorSpan} from {@code start} to {@code end}. * @@ -550,6 +631,55 @@ public final class SpannedSubject extends Subject { } } + /** Allows assertions about the alignment of a span. */ + public interface Aligned { + + /** + * Checks that at least one of the matched spans has the expected {@code alignment}. + * + * @param alignment The expected alignment. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. + */ + AndSpanFlags withAlignment(Alignment alignment); + } + + private static final Aligned ALREADY_FAILED_ALIGNED = alignment -> ALREADY_FAILED_AND_FLAGS; + + private static Factory> alignmentSpans( + Spanned actualSpanned) { + return (FailureMetadata metadata, List spans) -> + new AlignmentSpansSubject(metadata, spans, actualSpanned); + } + + private static final class AlignmentSpansSubject extends Subject implements Aligned { + + private final List actualSpans; + private final Spanned actualSpanned; + + private AlignmentSpansSubject( + FailureMetadata metadata, List actualSpans, Spanned actualSpanned) { + super(metadata, actualSpans); + this.actualSpans = actualSpans; + this.actualSpanned = actualSpanned; + } + + @Override + public AndSpanFlags withAlignment(Alignment alignment) { + List matchingSpanFlags = new ArrayList<>(); + List spanAlignments = new ArrayList<>(); + + for (AlignmentSpan span : actualSpans) { + spanAlignments.add(span.getAlignment()); + if (span.getAlignment().equals(alignment)) { + matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); + } + } + + check("alignment").that(spanAlignments).containsExactly(alignment); + return check("flags").about(spanFlags()).that(matchingSpanFlags); + } + } + /** Allows assertions about the color of a span. */ public interface Colored { 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 33a67f1c64..1c37853c4b 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 @@ -23,10 +23,13 @@ import static com.google.common.truth.ExpectFailure.expectFailureAbout; import android.graphics.Color; import android.graphics.Typeface; +import android.text.Layout.Alignment; import android.text.SpannableString; import android.text.Spanned; +import android.text.style.AlignmentSpan; import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; +import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; @@ -283,6 +286,185 @@ public class SpannedSubjectTest { assertThat(expected).factValue("but found").contains("start=" + start); } + @Test + public void strikethroughSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with crossed-out section"); + int start = "test with ".length(); + int end = start + "crossed-out".length(); + spannable.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasStrikethroughSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void noStrikethroughSpan_success() { + SpannableString spannable = + SpannableString.valueOf("test with underline then crossed-out spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new UnderlineSpan(), + "test with underline then ".length(), + "test with italic then crossed-out".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoStrikethroughSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noStrikethroughSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with crossed-out section"); + int start = "test with ".length(); + int end = start + "crossed-out".length(); + spannable.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting.that(spannable).hasNoStrikethroughSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains( + "Found unexpected StrikethroughSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + + @Test + public void alignmentSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with right-aligned section"); + int start = "test with ".length(); + int end = start + "right-aligned".length(); + spannable.setSpan( + new AlignmentSpan.Standard(Alignment.ALIGN_OPPOSITE), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasAlignmentSpanBetween(start, end) + .withAlignment(Alignment.ALIGN_OPPOSITE) + .andFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void alignmentSpan_wrongEndIndex() { + SpannableString spannable = SpannableString.valueOf("test with right-aligned section"); + int start = "test with ".length(); + int end = start + "right-aligned".length(); + spannable.setSpan( + new AlignmentSpan.Standard(Alignment.ALIGN_OPPOSITE), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + int incorrectEnd = end + 2; + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasAlignmentSpanBetween(start, incorrectEnd) + .withAlignment(Alignment.ALIGN_OPPOSITE)); + assertThat(expected).factValue("expected").contains("end=" + incorrectEnd); + assertThat(expected).factValue("but found").contains("end=" + end); + } + + @Test + public void alignmentSpan_wrongAlignment() { + SpannableString spannable = SpannableString.valueOf("test with right-aligned section"); + int start = "test with ".length(); + int end = start + "right-aligned".length(); + spannable.setSpan( + new AlignmentSpan.Standard(Alignment.ALIGN_OPPOSITE), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasAlignmentSpanBetween(start, end) + .withAlignment(Alignment.ALIGN_CENTER)); + assertThat(expected).factValue("value of").contains("alignment"); + assertThat(expected).factValue("expected").contains("ALIGN_CENTER"); + assertThat(expected).factValue("but was").contains("ALIGN_OPPOSITE"); + } + + @Test + public void alignmentSpan_wrongFlags() { + SpannableString spannable = SpannableString.valueOf("test with right-aligned section"); + int start = "test with ".length(); + int end = start + "right-aligned".length(); + spannable.setSpan( + new AlignmentSpan.Standard(Alignment.ALIGN_OPPOSITE), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasAlignmentSpanBetween(start, end) + .withAlignment(Alignment.ALIGN_OPPOSITE) + .andFlags(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected).factValue("value of").contains("flags"); + assertThat(expected) + .factValue("expected to contain") + .contains(String.valueOf(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected) + .factValue("but was") + .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + } + + @Test + public void noAlignmentSpan_success() { + SpannableString spannable = + SpannableString.valueOf("test with underline then right-aligned spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new AlignmentSpan.Standard(Alignment.ALIGN_OPPOSITE), + "test with underline then ".length(), + "test with underline then cyan".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoAlignmentSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noAlignmentSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with right-aligned section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new AlignmentSpan.Standard(Alignment.ALIGN_OPPOSITE), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasNoAlignmentSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains("Found unexpected AlignmentSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + @Test public void foregroundColorSpan_success() { SpannableString spannable = SpannableString.valueOf("test with cyan section");