Add RubySpan support to SpannedSubject

PiperOrigin-RevId: 288280332
This commit is contained in:
ibaker 2020-01-06 12:52:00 +00:00 committed by Ian Baker
parent e55af3e3c8
commit 2b1a066339
2 changed files with 230 additions and 2 deletions

View file

@ -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}.
*
* <p>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}.
*
* <p>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}.
*
* <p>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<RubySpan> 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 <T> List<T> findMatchingSpans(int startIndex, int endIndex, Class<T> spanClazz) {
List<T> 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<RubySpansSubject, List<RubySpan>> rubySpans(Spanned actualSpanned) {
return (FailureMetadata metadata, List<RubySpan> spans) ->
new RubySpansSubject(metadata, spans, actualSpanned);
}
private static final class RubySpansSubject extends Subject implements RubyText {
private final List<RubySpan> actualSpans;
private final Spanned actualSpanned;
private RubySpansSubject(
FailureMetadata metadata, List<RubySpan> actualSpans, Spanned actualSpanned) {
super(metadata, actualSpans);
this.actualSpans = actualSpans;
this.actualSpanned = actualSpanned;
}
@Override
public AndSpanFlags withTextAndPosition(String text, @RubySpan.Position int position) {
List<Integer> matchingSpanFlags = new ArrayList<>();
List<TextAndPosition> 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);
}
}
}
}

View file

@ -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<SpannedSubject, Spanned> callback) {
return expectFailureAbout(spanned(), callback);