From 448db89446bf91b63c5351b77c7f579a65bd0447 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 8 Jan 2020 15:04:09 +0000 Subject: [PATCH] Add TypefaceSpan and hasNoFooSpanBetween() support to SpannedSubject Use these to migrate the last WebvttDecoderTest method to SpannedSubject PiperOrigin-RevId: 288688620 --- .../assets/webvtt/with_css_complex_selectors | 10 +- .../text/webvtt/WebvttDecoderTest.java | 67 ++-- .../testutil/truth/SpannedSubject.java | 206 ++++++++++- .../testutil/truth/SpannedSubjectTest.java | 334 ++++++++++++++++++ 4 files changed, 570 insertions(+), 47 deletions(-) diff --git a/library/core/src/test/assets/webvtt/with_css_complex_selectors b/library/core/src/test/assets/webvtt/with_css_complex_selectors index 62e3348ae9..64d2a516d1 100644 --- a/library/core/src/test/assets/webvtt/with_css_complex_selectors +++ b/library/core/src/test/assets/webvtt/with_css_complex_selectors @@ -20,7 +20,7 @@ STYLE id 00:00.000 --> 00:01.001 -This should be underlined and courier and violet. +This should be underlined and courier and violet. íd 00:02.000 --> 00:02.001 @@ -31,10 +31,10 @@ _id This should be courier and bold. 00:04.000 --> 00:04.001 -This shouldn't be bold. -This should be bold. +This shouldn't be bold. +This should be bold. anId 00:05.000 --> 00:05.001 -This is specific - But this is more italic +This is specific +But this is more italic diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index e07c412fd7..97e3a8f7d5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -19,17 +19,14 @@ import static com.google.android.exoplayer2.testutil.truth.SpannedSubject.assert import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; -import android.graphics.Typeface; import android.text.Layout.Alignment; import android.text.Spanned; -import android.text.style.ForegroundColorSpan; -import android.text.style.StyleSpan; -import android.text.style.TypefaceSpan; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SubtitleDecoderException; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ColorParser; import com.google.common.collect.Iterables; import com.google.common.truth.Expect; @@ -328,41 +325,39 @@ public class WebvttDecoderTest { @Test public void testWithComplexCssSelectors() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_COMPLEX_SELECTORS); - Spanned text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0); - assertThat(text.getSpans(/* start= */ 30, text.length(), ForegroundColorSpan.class)) - .hasLength(1); - assertThat( - text.getSpans(/* start= */ 30, text.length(), ForegroundColorSpan.class)[0] - .getForegroundColor()) - .isEqualTo(0xFFEE82EE); - assertThat(text.getSpans(/* start= */ 30, text.length(), TypefaceSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 30, text.length(), TypefaceSpan.class)[0].getFamily()) - .isEqualTo("courier"); + Spanned firstCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0); + assertThat(firstCueText) + .hasForegroundColorSpanBetween( + "This should be underlined and ".length(), firstCueText.length()) + .withColor(ColorParser.parseCssColor("violet")); + assertThat(firstCueText) + .hasTypefaceSpanBetween("This should be underlined and ".length(), firstCueText.length()) + .withFamily("courier"); - text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2000000); - assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)[0].getFamily()) - .isEqualTo("courier"); + Spanned secondCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2_000_000); + assertThat(secondCueText) + .hasTypefaceSpanBetween("This ".length(), secondCueText.length()) + .withFamily("courier"); + assertThat(secondCueText) + .hasNoForegroundColorSpanBetween("This ".length(), secondCueText.length()); - text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2500000); - assertThat(text.getSpans(/* start= */ 5, text.length(), StyleSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 5, text.length(), StyleSpan.class)[0].getStyle()) - .isEqualTo(Typeface.BOLD); - assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)[0].getFamily()) - .isEqualTo("courier"); + Spanned thirdCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2_500_000); + assertThat(thirdCueText).hasBoldSpanBetween("This ".length(), thirdCueText.length()); + assertThat(thirdCueText) + .hasTypefaceSpanBetween("This ".length(), thirdCueText.length()) + .withFamily("courier"); - text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 4000000); - assertThat(text.getSpans(/* start= */ 6, /* end= */ 22, StyleSpan.class)).hasLength(0); - assertThat(text.getSpans(/* start= */ 30, text.length(), StyleSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 30, text.length(), StyleSpan.class)[0].getStyle()) - .isEqualTo(Typeface.BOLD); + Spanned fourthCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 4_000_000); + assertThat(fourthCueText) + .hasNoStyleSpanBetween("This ".length(), "shouldn't be bold.".length()); + assertThat(fourthCueText) + .hasBoldSpanBetween("This shouldn't be bold.\nThis ".length(), fourthCueText.length()); - text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 5000000); - assertThat(text.getSpans(/* start= */ 9, /* end= */ 17, StyleSpan.class)).hasLength(0); - assertThat(text.getSpans(/* start= */ 19, text.length(), StyleSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 19, text.length(), StyleSpan.class)[0].getStyle()) - .isEqualTo(Typeface.ITALIC); + Spanned fifthCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 5_000_000); + assertThat(fifthCueText) + .hasNoStyleSpanBetween("This is ".length(), "This is specific".length()); + assertThat(fifthCueText) + .hasItalicSpanBetween("This is specific\n".length(), fifthCueText.length()); } @Test @@ -387,6 +382,6 @@ public class WebvttDecoderTest { } private Spanned getUniqueSpanTextAt(WebvttSubtitle sub, long timeUs) { - return (Spanned) sub.getCues(timeUs).get(0).text; + return (Spanned) Assertions.checkNotNull(sub.getCues(timeUs).get(0).text); } } 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 b6efa1e7b7..78c41a43e8 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 @@ -26,12 +26,14 @@ import android.text.TextUtils; import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; +import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; import androidx.annotation.CheckResult; import androidx.annotation.ColorInt; import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; import com.google.android.exoplayer2.text.span.RubySpan; +import com.google.android.exoplayer2.util.Util; import com.google.common.truth.FailureMetadata; import com.google.common.truth.Subject; import java.util.ArrayList; @@ -171,6 +173,19 @@ public final class SpannedSubject extends Subject { return ALREADY_FAILED_WITH_FLAGS; } + /** + * Checks that the subject has no {@link StyleSpan}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 hasNoStyleSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(StyleSpan.class, start, end); + } + /** * Checks that the subject has an {@link UnderlineSpan} from {@code start} to {@code end}. * @@ -194,6 +209,19 @@ public final class SpannedSubject extends Subject { return ALREADY_FAILED_WITH_FLAGS; } + /** + * Checks that the subject has no {@link UnderlineSpan}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 hasNoUnderlineSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(UnderlineSpan.class, start, end); + } + /** * Checks that the subject has a {@link ForegroundColorSpan} from {@code start} to {@code end}. * @@ -222,6 +250,19 @@ public final class SpannedSubject extends Subject { .that(foregroundColorSpans); } + /** + * Checks that the subject has no {@link ForegroundColorSpan}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 hasNoForegroundColorSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(ForegroundColorSpan.class, start, end); + } + /** * Checks that the subject has a {@link BackgroundColorSpan} from {@code start} to {@code end}. * @@ -250,6 +291,58 @@ public final class SpannedSubject extends Subject { .that(backgroundColorSpans); } + /** + * Checks that the subject has no {@link BackgroundColorSpan}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 hasNoBackgroundColorSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(BackgroundColorSpan.class, start, end); + } + + /** + * Checks that the subject has a {@link TypefaceSpan} from {@code start} to {@code end}. + * + *

The font is asserted in a follow-up method call on the return {@link Typefaced} object. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link Typefaced} object to assert on the font of the matching spans. + */ + @CheckResult + public Typefaced hasTypefaceSpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_TYPEFACED; + } + + List backgroundColorSpans = findMatchingSpans(start, end, TypefaceSpan.class); + if (backgroundColorSpans.isEmpty()) { + failWithExpectedSpan(start, end, TypefaceSpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_TYPEFACED; + } + return check("TypefaceSpan (start=%s,end=%s)", start, end) + .about(typefaceSpans(actual)) + .that(backgroundColorSpans); + } + + /** + * Checks that the subject has no {@link TypefaceSpan}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 hasNoTypefaceSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(TypefaceSpan.class, start, end); + } + /** * Checks that the subject has a {@link RubySpan} from {@code start} to {@code end}. * @@ -274,6 +367,19 @@ public final class SpannedSubject extends Subject { return check("RubySpan (start=%s,end=%s)", start, end).about(rubySpans(actual)).that(rubySpans); } + /** + * Checks that the subject has no {@link RubySpan}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 hasNoRubySpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(RubySpan.class, start, end); + } + /** * Checks that the subject has an {@link HorizontalTextInVerticalContextSpan} from {@code start} * to {@code end}. @@ -303,6 +409,45 @@ public final class SpannedSubject extends Subject { return ALREADY_FAILED_WITH_FLAGS; } + /** + * Checks that the subject has no {@link HorizontalTextInVerticalContextSpan}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 hasNoHorizontalTextInVerticalContextSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(HorizontalTextInVerticalContextSpan.class, start, end); + } + + /** + * Checks that the subject has no spans of type {@code spanClazz} 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. + */ + private void hasNoSpansOfTypeBetween(Class spanClazz, int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return; + } + Object[] matchingSpans = actual.getSpans(start, end, spanClazz); + if (matchingSpans.length != 0) { + failWithoutActual( + simpleFact( + String.format( + "Found unexpected %ss between start=%s,end=%s", + spanClazz.getSimpleName(), start, end)), + simpleFact("expected none"), + fact("but found", getAllSpansAsString(actual))); + } + } + private List findMatchingSpans(int startIndex, int endIndex, Class spanClazz) { List spans = new ArrayList<>(); for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) { @@ -421,8 +566,8 @@ public final class SpannedSubject extends Subject { private static final Colored ALREADY_FAILED_COLORED = color -> ALREADY_FAILED_AND_FLAGS; - private Factory> foregroundColorSpans( - Spanned actualSpanned) { + private static Factory> + foregroundColorSpans(Spanned actualSpanned) { return (FailureMetadata metadata, List spans) -> new ForegroundColorSpansSubject(metadata, spans, actualSpanned); } @@ -458,8 +603,8 @@ public final class SpannedSubject extends Subject { } } - private Factory> backgroundColorSpans( - Spanned actualSpanned) { + private static Factory> + backgroundColorSpans(Spanned actualSpanned) { return (FailureMetadata metadata, List spans) -> new BackgroundColorSpansSubject(metadata, spans, actualSpanned); } @@ -495,6 +640,55 @@ public final class SpannedSubject extends Subject { } } + /** Allows assertions about the typeface of a span. */ + public interface Typefaced { + + /** + * Checks that at least one of the matched spans has the expected {@code fontFamily}. + * + * @param fontFamily The expected font family. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. + */ + AndSpanFlags withFamily(String fontFamily); + } + + private static final Typefaced ALREADY_FAILED_TYPEFACED = color -> ALREADY_FAILED_AND_FLAGS; + + private static Factory> typefaceSpans( + Spanned actualSpanned) { + return (FailureMetadata metadata, List spans) -> + new TypefaceSpansSubject(metadata, spans, actualSpanned); + } + + private static final class TypefaceSpansSubject extends Subject implements Typefaced { + + private final List actualSpans; + private final Spanned actualSpanned; + + private TypefaceSpansSubject( + FailureMetadata metadata, List actualSpans, Spanned actualSpanned) { + super(metadata, actualSpans); + this.actualSpans = actualSpans; + this.actualSpanned = actualSpanned; + } + + @Override + public AndSpanFlags withFamily(String fontFamily) { + List matchingSpanFlags = new ArrayList<>(); + List spanFontFamilies = new ArrayList<>(); + + for (TypefaceSpan span : actualSpans) { + spanFontFamilies.add(span.getFamily()); + if (Util.areEqual(span.getFamily(), fontFamily)) { + matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); + } + } + + check("family").that(spanFontFamilies).containsExactly(fontFamily); + return check("flags").about(spanFlags()).that(matchingSpanFlags); + } + } + /** Allows assertions about a span's ruby text and its position. */ public interface RubyText { @@ -511,7 +705,7 @@ public final class SpannedSubject extends Subject { private static final RubyText ALREADY_FAILED_WITH_TEXT = (text, position) -> ALREADY_FAILED_AND_FLAGS; - private Factory> rubySpans(Spanned actualSpanned) { + private static Factory> rubySpans(Spanned actualSpanned) { return (FailureMetadata metadata, List spans) -> new RubySpansSubject(metadata, spans, actualSpanned); } @@ -544,7 +738,7 @@ public final class SpannedSubject extends Subject { return check("flags").about(spanFlags()).that(matchingSpanFlags); } - private static class TextAndPosition { + private static final class TextAndPosition { private final String text; @RubySpan.Position private final int 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 c3badd9bb9..d1ee3ee81a 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 @@ -28,6 +28,7 @@ import android.text.Spanned; import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; +import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; @@ -170,6 +171,40 @@ public class SpannedSubjectTest { assertThat(expected).factValue("but found").contains("start=" + start); } + @Test + public void noStyleSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with underline then italic spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new StyleSpan(Typeface.ITALIC), + "test with underline then ".length(), + "test with underline then italic".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoStyleSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noStyleSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with italic section"); + int start = "test with ".length(); + int end = start + "italic".length(); + spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasNoStyleSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains("Found unexpected StyleSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + @Test public void underlineSpan_success() { SpannableString spannable = SpannableString.valueOf("test with underlined section"); @@ -182,6 +217,40 @@ public class SpannedSubjectTest { .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } + @Test + public void noUnderlineSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with italic then underline spans"); + spannable.setSpan( + new StyleSpan(Typeface.ITALIC), + "test with ".length(), + "test with italic".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new UnderlineSpan(), + "test with italic then ".length(), + "test with italic then underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoUnderlineSpanBetween(0, "test with italic then".length()); + } + + @Test + public void noUnderlineSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with underline section"); + int start = "test with ".length(); + int end = start + "underline".length(); + spannable.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasNoUnderlineSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains("Found unexpected UnderlineSpans 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"); @@ -261,6 +330,43 @@ public class SpannedSubjectTest { .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); } + @Test + public void noForegroundColorSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with underline then cyan spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), + "test with underline then ".length(), + "test with underline then cyan".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoForegroundColorSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noForegroundColorSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting.that(spannable).hasNoForegroundColorSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains( + "Found unexpected ForegroundColorSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + @Test public void backgroundColorSpan_success() { SpannableString spannable = SpannableString.valueOf("test with cyan section"); @@ -340,6 +446,152 @@ public class SpannedSubjectTest { .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); } + @Test + public void noBackgroundColorSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with underline then cyan spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new BackgroundColorSpan(Color.CYAN), + "test with underline then ".length(), + "test with underline then cyan".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoBackgroundColorSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noBackgroundColorSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new BackgroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting.that(spannable).hasNoBackgroundColorSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains( + "Found unexpected BackgroundColorSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + + @Test + public void typefaceSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with courier section"); + int start = "test with ".length(); + int end = start + "courier".length(); + spannable.setSpan(new TypefaceSpan("courier"), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasTypefaceSpanBetween(start, end) + .withFamily("courier") + .andFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void typefaceSpan_wrongEndIndex() { + SpannableString spannable = SpannableString.valueOf("test with courier section"); + int start = "test with ".length(); + int end = start + "courier".length(); + spannable.setSpan(new TypefaceSpan("courier"), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + int incorrectEnd = end + 2; + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasTypefaceSpanBetween(start, incorrectEnd) + .withFamily("courier")); + assertThat(expected).factValue("expected").contains("end=" + incorrectEnd); + assertThat(expected).factValue("but found").contains("end=" + end); + } + + @Test + public void typefaceSpan_wrongFamily() { + SpannableString spannable = SpannableString.valueOf("test with courier section"); + int start = "test with ".length(); + int end = start + "courier".length(); + spannable.setSpan(new TypefaceSpan("courier"), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasTypefaceSpanBetween(start, end) + .withFamily("roboto")); + assertThat(expected).factValue("value of").contains("family"); + assertThat(expected).factValue("expected").contains("roboto"); + assertThat(expected).factValue("but was").contains("courier"); + } + + @Test + public void typefaceSpan_wrongFlags() { + SpannableString spannable = SpannableString.valueOf("test with courier section"); + int start = "test with ".length(); + int end = start + "courier".length(); + spannable.setSpan(new TypefaceSpan("courier"), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasTypefaceSpanBetween(start, end) + .withFamily("courier") + .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 noTypefaceSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with underline then courier spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new TypefaceSpan("courier"), + "test with underline then ".length(), + "test with underline then courier".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoTypefaceSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noTypefaceSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with courier section"); + int start = "test with ".length(); + int end = start + "courier".length(); + spannable.setSpan(new TypefaceSpan("courier"), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasNoTypefaceSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains("Found unexpected TypefaceSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + @Test public void rubySpan_success() { SpannableString spannable = SpannableString.valueOf("test with rubied section"); @@ -454,6 +706,44 @@ public class SpannedSubjectTest { .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); } + @Test + public void noRubySpan_success() { + SpannableString spannable = SpannableString.valueOf("test with underline then ruby spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + "test with underline then ".length(), + "test with underline then ruby".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoRubySpanBetween(0, "test with underline then".length()); + } + + @Test + public void noRubySpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with ruby section"); + int start = "test with ".length(); + int end = start + "ruby".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasNoRubySpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains("Found unexpected RubySpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + @Test public void horizontalTextInVerticalContextSpan_success() { SpannableString spannable = SpannableString.valueOf("vertical text with horizontal section"); @@ -467,6 +757,50 @@ public class SpannedSubjectTest { .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } + @Test + public void noHorizontalTextInVerticalContextSpan_success() { + SpannableString spannable = + SpannableString.valueOf("test with underline then tate-chu-yoko spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new HorizontalTextInVerticalContextSpan(), + "test with underline then ".length(), + "test with underline then tate-chu-yoko".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasNoHorizontalTextInVerticalContextSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noHorizontalTextInVerticalContextSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with tate-chu-yoko section"); + int start = "test with ".length(); + int end = start + "tate-chu-yoko".length(); + spannable.setSpan( + new HorizontalTextInVerticalContextSpan(), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasNoHorizontalTextInVerticalContextSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains( + "Found unexpected HorizontalTextInVerticalContextSpans between start=" + + (start + 1) + + ",end=" + + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + private static AssertionError expectFailure( ExpectFailure.SimpleSubjectBuilderCallback callback) { return expectFailureAbout(spanned(), callback);