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);