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 16144a170b..55e2117e04 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 @@ -30,6 +30,7 @@ import android.text.style.UnderlineSpan; import androidx.annotation.CheckResult; import androidx.annotation.ColorInt; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.text.span.RubySpan; import com.google.common.truth.FailureMetadata; import com.google.common.truth.Subject; import java.util.ArrayList; @@ -194,7 +195,7 @@ public final class SpannedSubject extends Subject { } /** - * Checks that the subject as a {@link ForegroundColorSpan} from {@code start} to {@code end}. + * Checks that the subject has a {@link ForegroundColorSpan} from {@code start} to {@code end}. * *

The color is asserted in a follow-up method call on the return {@link Colored} object. * @@ -222,7 +223,7 @@ public final class SpannedSubject extends Subject { } /** - * Checks that the subject as a {@link ForegroundColorSpan} from {@code start} to {@code end}. + * Checks that the subject has a {@link BackgroundColorSpan} from {@code start} to {@code end}. * *

The color is asserted in a follow-up method call on the return {@link Colored} object. * @@ -249,6 +250,30 @@ public final class SpannedSubject extends Subject { .that(backgroundColorSpans); } + /** + * Checks that the subject has a {@link RubySpan} from {@code start} to {@code end}. + * + *

The ruby-text is asserted in a follow-up method call on the return {@link RubyText} object. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link Colored} object to assert on the color of the matching spans. + */ + @CheckResult + public RubyText hasRubySpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_WITH_TEXT; + } + + List rubySpans = findMatchingSpans(start, end, RubySpan.class); + if (rubySpans.isEmpty()) { + failWithExpectedSpan(start, end, RubySpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_WITH_TEXT; + } + return check("RubySpan (start=%s,end=%s)", start, end).about(rubySpans(actual)).that(rubySpans); + } + private List findMatchingSpans(int startIndex, int endIndex, Class spanClazz) { List spans = new ArrayList<>(); for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) { @@ -440,4 +465,92 @@ public final class SpannedSubject extends Subject { return check("flags").about(spanFlags()).that(matchingSpanFlags); } } + + /** Allows assertions about a span's ruby text and its position. */ + public interface RubyText { + + /** + * Checks that at least one of the matched spans has the expected {@code text}. + * + * @param text The expected text. + * @param position The expected position of the text. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. + */ + AndSpanFlags withTextAndPosition(String text, @RubySpan.Position int position); + } + + private static final RubyText ALREADY_FAILED_WITH_TEXT = + (text, position) -> ALREADY_FAILED_AND_FLAGS; + + private Factory> rubySpans(Spanned actualSpanned) { + return (FailureMetadata metadata, List spans) -> + new RubySpansSubject(metadata, spans, actualSpanned); + } + + private static final class RubySpansSubject extends Subject implements RubyText { + + private final List actualSpans; + private final Spanned actualSpanned; + + private RubySpansSubject( + FailureMetadata metadata, List actualSpans, Spanned actualSpanned) { + super(metadata, actualSpans); + this.actualSpans = actualSpans; + this.actualSpanned = actualSpanned; + } + + @Override + public AndSpanFlags withTextAndPosition(String text, @RubySpan.Position int position) { + List matchingSpanFlags = new ArrayList<>(); + List spanTextsAndPositions = new ArrayList<>(); + for (RubySpan span : actualSpans) { + spanTextsAndPositions.add(new TextAndPosition(span.rubyText, span.position)); + if (span.rubyText.equals(text)) { + matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); + } + } + check("rubyTextAndPosition") + .that(spanTextsAndPositions) + .containsExactly(new TextAndPosition(text, position)); + return check("flags").about(spanFlags()).that(matchingSpanFlags); + } + + private static class TextAndPosition { + private final String text; + @RubySpan.Position private final int position; + + private TextAndPosition(String text, int position) { + this.text = text; + this.position = position; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TextAndPosition that = (TextAndPosition) o; + if (position != that.position) { + return false; + } + return text.equals(that.text); + } + + @Override + public int hashCode() { + int result = text.hashCode(); + result = 31 * result + position; + return result; + } + + @Override + public String toString() { + return String.format("{text='%s',position=%s}", text, position); + } + } + } } 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 c33a1128a0..3eb0509eb4 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 @@ -30,6 +30,7 @@ import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.text.span.RubySpan; import com.google.common.truth.ExpectFailure; import org.junit.Test; import org.junit.runner.RunWith; @@ -338,6 +339,120 @@ public class SpannedSubjectTest { .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); } + @Test + public void rubySpan_success() { + SpannableString spannable = SpannableString.valueOf("test with rubied section"); + int start = "test with ".length(); + int end = start + "rubied".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasRubySpanBetween(start, end) + .withTextAndPosition("ruby text", RubySpan.POSITION_OVER) + .andFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void rubySpan_wrongEndIndex() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + int incorrectEnd = end + 2; + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasRubySpanBetween(start, incorrectEnd) + .withTextAndPosition("ruby text", RubySpan.POSITION_OVER)); + assertThat(expected).factValue("expected").contains("end=" + incorrectEnd); + assertThat(expected).factValue("but found").contains("end=" + end); + } + + @Test + public void rubySpan_wrongText() { + SpannableString spannable = SpannableString.valueOf("test with rubied section"); + int start = "test with ".length(); + int end = start + "rubied".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasRubySpanBetween(start, end) + .withTextAndPosition("incorrect text", RubySpan.POSITION_OVER)); + assertThat(expected).factValue("value of").contains("rubyTextAndPosition"); + assertThat(expected).factValue("expected").contains("text='incorrect text'"); + assertThat(expected).factValue("but was").contains("text='ruby text'"); + } + + @Test + public void rubySpan_wrongPosition() { + SpannableString spannable = SpannableString.valueOf("test with rubied section"); + int start = "test with ".length(); + int end = start + "rubied".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasRubySpanBetween(start, end) + .withTextAndPosition("ruby text", RubySpan.POSITION_UNDER)); + assertThat(expected).factValue("value of").contains("rubyTextAndPosition"); + assertThat(expected).factValue("expected").contains("position=" + RubySpan.POSITION_UNDER); + assertThat(expected).factValue("but was").contains("position=" + RubySpan.POSITION_OVER); + } + + @Test + public void rubySpan_wrongFlags() { + SpannableString spannable = SpannableString.valueOf("test with rubied section"); + int start = "test with ".length(); + int end = start + "rubied".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasRubySpanBetween(start, end) + .withTextAndPosition("ruby text", RubySpan.POSITION_OVER) + .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)); + } + private static AssertionError expectFailure( ExpectFailure.SimpleSubjectBuilderCallback callback) { return expectFailureAbout(spanned(), callback);