| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| */ |
| package com.google.android.exoplayer2.testutil.truth; |
| |
| import static com.google.common.truth.Fact.fact; |
| 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.AbsoluteSizeSpan; |
| import android.text.style.AlignmentSpan; |
| import android.text.style.BackgroundColorSpan; |
| import android.text.style.ForegroundColorSpan; |
| import android.text.style.RelativeSizeSpan; |
| import android.text.style.StrikethroughSpan; |
| 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.Fact; |
| import com.google.common.truth.FailureMetadata; |
| import com.google.common.truth.Subject; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import org.checkerframework.checker.nullness.compatqual.NullableType; |
| import org.checkerframework.checker.nullness.qual.RequiresNonNull; |
| |
| /** A Truth {@link Subject} for assertions on {@link Spanned} instances containing text styling. */ |
| // TODO: add support for more Spans i.e. all those used in com.google.android.exoplayer2.text. |
| public final class SpannedSubject extends Subject { |
| |
| @Nullable private final Spanned actual; |
| |
| private SpannedSubject(FailureMetadata metadata, @Nullable Spanned actual) { |
| super(metadata, actual); |
| this.actual = actual; |
| } |
| |
| public static Factory<SpannedSubject, Spanned> spanned() { |
| return SpannedSubject::new; |
| } |
| |
| /** |
| * Convenience method to create a SpannedSubject. |
| * |
| * <p>Can be statically imported alongside other Truth {@code assertThat} methods. |
| * |
| * @param spanned The subject under test. |
| * @return An object for conducting assertions on the subject. |
| */ |
| public static SpannedSubject assertThat(@Nullable Spanned spanned) { |
| return assertAbout(spanned()).that(spanned); |
| } |
| |
| public void hasNoSpans() { |
| if (actual == null) { |
| failWithoutActual(simpleFact("Spanned must not be null")); |
| return; |
| } |
| |
| Object[] spans = actual.getSpans(0, actual.length(), Object.class); |
| if (spans.length > 0) { |
| failWithoutActual( |
| simpleFact("Expected no spans"), fact("in text", actual), actualSpansFact()); |
| } |
| } |
| |
| /** |
| * Checks that the subject has an italic span 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 hasItalicSpanBetween(int start, int end) { |
| return hasStyleSpan(start, end, Typeface.ITALIC); |
| } |
| |
| /** |
| * Checks that the subject has a bold span 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 hasBoldSpanBetween(int start, int end) { |
| return hasStyleSpan(start, end, Typeface.BOLD); |
| } |
| |
| private WithSpanFlags hasStyleSpan(int start, int end, int style) { |
| if (actual == null) { |
| failWithoutActual(simpleFact("Spanned must not be null")); |
| return ALREADY_FAILED_WITH_FLAGS; |
| } |
| |
| List<Integer> allFlags = new ArrayList<>(); |
| boolean matchingSpanFound = false; |
| for (StyleSpan span : findMatchingSpans(start, end, StyleSpan.class)) { |
| allFlags.add(actual.getSpanFlags(span)); |
| if (span.getStyle() == style) { |
| matchingSpanFound = true; |
| break; |
| } |
| } |
| if (matchingSpanFound) { |
| return check("StyleSpan (start=%s,end=%s,style=%s)", start, end, style) |
| .about(spanFlags()) |
| .that(allFlags); |
| } |
| |
| failWithExpectedSpan(start, end, StyleSpan.class, actual.toString().substring(start, end)); |
| return ALREADY_FAILED_WITH_FLAGS; |
| } |
| |
| /** |
| * Checks that the subject has bold and italic styling from {@code start} to {@code end}. |
| * |
| * <p>This can either be: |
| * |
| * <ul> |
| * <li>A single {@link StyleSpan} with {@code span.getStyle() == Typeface.BOLD_ITALIC}. |
| * <li>Two {@link StyleSpan}s, one with {@code span.getStyle() == Typeface.BOLD} and the other |
| * with {@code span.getStyle() == Typeface.ITALIC}. |
| * </ul> |
| * |
| * @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 hasBoldItalicSpanBetween(int start, int end) { |
| if (actual == null) { |
| failWithoutActual(simpleFact("Spanned must not be null")); |
| return ALREADY_FAILED_WITH_FLAGS; |
| } |
| |
| List<Integer> allFlags = new ArrayList<>(); |
| List<Integer> styles = new ArrayList<>(); |
| for (StyleSpan span : findMatchingSpans(start, end, StyleSpan.class)) { |
| allFlags.add(actual.getSpanFlags(span)); |
| styles.add(span.getStyle()); |
| } |
| if (styles.isEmpty()) { |
| failWithExpectedSpan(start, end, StyleSpan.class, actual.subSequence(start, end).toString()); |
| return ALREADY_FAILED_WITH_FLAGS; |
| } |
| |
| if (styles.contains(Typeface.BOLD_ITALIC) |
| || (styles.contains(Typeface.BOLD) && styles.contains(Typeface.ITALIC))) { |
| return check("StyleSpan (start=%s,end=%s)", start, end).about(spanFlags()).that(allFlags); |
| } |
| failWithoutActual( |
| simpleFact( |
| String.format("No matching StyleSpans found between start=%s,end=%s", start, end)), |
| fact("in text", actual.toString()), |
| fact("expected to contain either", Collections.singletonList(Typeface.BOLD_ITALIC)), |
| fact("or both", Arrays.asList(Typeface.BOLD, Typeface.ITALIC)), |
| fact("but found styles", styles)); |
| 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}. |
| * |
| * <p>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}. |
| * |
| * @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 hasUnderlineSpanBetween(int start, int end) { |
| if (actual == null) { |
| failWithoutActual(simpleFact("Spanned must not be null")); |
| return ALREADY_FAILED_WITH_FLAGS; |
| } |
| |
| List<UnderlineSpan> underlineSpans = findMatchingSpans(start, end, UnderlineSpan.class); |
| if (underlineSpans.size() == 1) { |
| return check("UnderlineSpan (start=%s,end=%s)", start, end) |
| .about(spanFlags()) |
| .that(Collections.singletonList(actual.getSpanFlags(underlineSpans.get(0)))); |
| } |
| failWithExpectedSpan(start, end, UnderlineSpan.class, actual.toString().substring(start, end)); |
| 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}. |
| * |
| * <p>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 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<StrikethroughSpan> 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}. |
| * |
| * <p>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}. |
| * |
| * <p>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<AlignmentSpan> 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}. |
| * |
| * <p>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}. |
| * |
| * <p>The color is asserted in a follow-up method call on the return {@link Colored} 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 Colored hasForegroundColorSpanBetween(int start, int end) { |
| if (actual == null) { |
| failWithoutActual(simpleFact("Spanned must not be null")); |
| return ALREADY_FAILED_COLORED; |
| } |
| |
| List<ForegroundColorSpan> foregroundColorSpans = |
| findMatchingSpans(start, end, ForegroundColorSpan.class); |
| if (foregroundColorSpans.isEmpty()) { |
| failWithExpectedSpan( |
| start, end, ForegroundColorSpan.class, actual.toString().substring(start, end)); |
| return ALREADY_FAILED_COLORED; |
| } |
| return check("ForegroundColorSpan (start=%s,end=%s)", start, end) |
| .about(foregroundColorSpans(actual)) |
| .that(foregroundColorSpans); |
| } |
| |
| /** |
| * Checks that the subject has no {@link ForegroundColorSpan}s on any of the text between {@code |
| * start} and {@code end}. |
| * |
| * <p>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}. |
| * |
| * <p>The color is asserted in a follow-up method call on the return {@link Colored} 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 Colored hasBackgroundColorSpanBetween(int start, int end) { |
| if (actual == null) { |
| failWithoutActual(simpleFact("Spanned must not be null")); |
| return ALREADY_FAILED_COLORED; |
| } |
| |
| List<BackgroundColorSpan> backgroundColorSpans = |
| findMatchingSpans(start, end, BackgroundColorSpan.class); |
| if (backgroundColorSpans.isEmpty()) { |
| failWithExpectedSpan( |
| start, end, BackgroundColorSpan.class, actual.toString().substring(start, end)); |
| return ALREADY_FAILED_COLORED; |
| } |
| return check("BackgroundColorSpan (start=%s,end=%s)", start, end) |
| .about(backgroundColorSpans(actual)) |
| .that(backgroundColorSpans); |
| } |
| |
| /** |
| * Checks that the subject has no {@link BackgroundColorSpan}s on any of the text between {@code |
| * start} and {@code end}. |
| * |
| * <p>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}. |
| * |
| * <p>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<TypefaceSpan> 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}. |
| * |
| * <p>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 AbsoluteSizeSpan} from {@code start} to {@code end}. |
| * |
| * <p>The size is asserted in a follow-up method call on the return {@link AbsoluteSized} object. |
| * |
| * @param start The start of the expected span. |
| * @param end The end of the expected span. |
| * @return A {@link AbsoluteSized} object to assert on the size of the matching spans. |
| */ |
| @CheckResult |
| public AbsoluteSized hasAbsoluteSizeSpanBetween(int start, int end) { |
| if (actual == null) { |
| failWithoutActual(simpleFact("Spanned must not be null")); |
| return ALREADY_FAILED_ABSOLUTE_SIZED; |
| } |
| |
| List<AbsoluteSizeSpan> absoluteSizeSpans = |
| findMatchingSpans(start, end, AbsoluteSizeSpan.class); |
| if (absoluteSizeSpans.isEmpty()) { |
| failWithExpectedSpan( |
| start, end, AbsoluteSizeSpan.class, actual.toString().substring(start, end)); |
| return ALREADY_FAILED_ABSOLUTE_SIZED; |
| } |
| return check("AbsoluteSizeSpan (start=%s,end=%s)", start, end) |
| .about(absoluteSizeSpans(actual)) |
| .that(absoluteSizeSpans); |
| } |
| |
| /** |
| * Checks that the subject has no {@link AbsoluteSizeSpan}s on any of the text between {@code |
| * start} and {@code end}. |
| * |
| * <p>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 hasNoAbsoluteSizeSpanBetween(int start, int end) { |
| hasNoSpansOfTypeBetween(AbsoluteSizeSpan.class, start, end); |
| } |
| |
| /** |
| * Checks that the subject has a {@link RelativeSizeSpan} from {@code start} to {@code end}. |
| * |
| * <p>The size is asserted in a follow-up method call on the return {@link RelativeSized} object. |
| * |
| * @param start The start of the expected span. |
| * @param end The end of the expected span. |
| * @return A {@link RelativeSized} object to assert on the size of the matching spans. |
| */ |
| @CheckResult |
| public RelativeSized hasRelativeSizeSpanBetween(int start, int end) { |
| if (actual == null) { |
| failWithoutActual(simpleFact("Spanned must not be null")); |
| return ALREADY_FAILED_RELATIVE_SIZED; |
| } |
| |
| List<RelativeSizeSpan> relativeSizeSpans = |
| findMatchingSpans(start, end, RelativeSizeSpan.class); |
| if (relativeSizeSpans.isEmpty()) { |
| failWithExpectedSpan( |
| start, end, RelativeSizeSpan.class, actual.toString().substring(start, end)); |
| return ALREADY_FAILED_RELATIVE_SIZED; |
| } |
| return check("RelativeSizeSpan (start=%s,end=%s)", start, end) |
| .about(relativeSizeSpans(actual)) |
| .that(relativeSizeSpans); |
| } |
| |
| /** |
| * Checks that the subject has no {@link RelativeSizeSpan}s on any of the text between {@code |
| * start} and {@code end}. |
| * |
| * <p>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 hasNoRelativeSizeSpanBetween(int start, int end) { |
| hasNoSpansOfTypeBetween(RelativeSizeSpan.class, start, end); |
| } |
| |
| /** |
| * 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); |
| } |
| |
| /** |
| * Checks that the subject has no {@link RubySpan}s on any of the text between {@code start} and |
| * {@code end}. |
| * |
| * <p>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}. |
| * |
| * @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 hasHorizontalTextInVerticalContextSpanBetween(int start, int end) { |
| if (actual == null) { |
| failWithoutActual(simpleFact("Spanned must not be null")); |
| return ALREADY_FAILED_WITH_FLAGS; |
| } |
| |
| List<HorizontalTextInVerticalContextSpan> horizontalInVerticalSpans = |
| findMatchingSpans(start, end, HorizontalTextInVerticalContextSpan.class); |
| if (horizontalInVerticalSpans.size() == 1) { |
| return check("HorizontalTextInVerticalContextSpan (start=%s,end=%s)", start, end) |
| .about(spanFlags()) |
| .that(Collections.singletonList(actual.getSpanFlags(horizontalInVerticalSpans.get(0)))); |
| } |
| failWithExpectedSpan( |
| start, |
| end, |
| HorizontalTextInVerticalContextSpan.class, |
| actual.toString().substring(start, end)); |
| 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}. |
| * |
| * <p>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}. |
| * |
| * <p>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; |
| } |
| |
| @NullableType 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"), |
| actualSpansFact()); |
| } |
| } |
| |
| private <T> List<T> findMatchingSpans(int startIndex, int endIndex, Class<T> spanClazz) { |
| if (actual == null) { |
| failWithoutActual(simpleFact("Spanned must not be null")); |
| return Collections.emptyList(); |
| } |
| |
| List<T> spans = new ArrayList<>(); |
| for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) { |
| if (actual.getSpanStart(span) == startIndex && actual.getSpanEnd(span) == endIndex) { |
| spans.add(span); |
| } |
| } |
| return Collections.unmodifiableList(spans); |
| } |
| |
| @RequiresNonNull("actual") |
| private void failWithExpectedSpan( |
| int start, int end, Class<?> spanType, String spannedSubstring) { |
| failWithoutActual( |
| simpleFact("No matching span found"), |
| fact("in text", actual), |
| fact("expected", getSpanAsString(start, end, spanType, spannedSubstring)), |
| actualSpansFact()); |
| } |
| |
| @RequiresNonNull("actual") |
| private Fact actualSpansFact() { |
| String actualSpans = getAllSpansAsString(actual); |
| if (actualSpans.isEmpty()) { |
| return Fact.simpleFact("but found no spans"); |
| } else { |
| return Fact.fact("but found", actualSpans); |
| } |
| } |
| |
| private static String getAllSpansAsString(Spanned spanned) { |
| List<String> actualSpanStrings = new ArrayList<>(); |
| for (Object span : spanned.getSpans(0, spanned.length(), Object.class)) { |
| actualSpanStrings.add(getSpanAsString(span, spanned)); |
| } |
| return TextUtils.join("\n", actualSpanStrings); |
| } |
| |
| private static String getSpanAsString(Object span, Spanned spanned) { |
| int spanStart = spanned.getSpanStart(span); |
| int spanEnd = spanned.getSpanEnd(span); |
| return getSpanAsString( |
| spanStart, spanEnd, span.getClass(), spanned.toString().substring(spanStart, spanEnd)); |
| } |
| |
| private static String getSpanAsString( |
| int start, int end, Class<?> span, String spannedSubstring) { |
| return String.format( |
| "start=%s\tend=%s\ttype=%s\tsubstring='%s'", |
| start, end, span.getSimpleName(), spannedSubstring); |
| } |
| |
| /** |
| * Allows additional assertions to be made on the flags of matching spans. |
| * |
| * <p>Identical to {@link WithSpanFlags}, but this should be returned from {@code with...()} |
| * methods while {@link WithSpanFlags} should be returned from {@code has...()} methods. |
| * |
| * <p>See Flag constants on {@link Spanned} for possible values. |
| */ |
| public interface AndSpanFlags { |
| |
| /** |
| * Checks that one of the matched spans has the expected {@code flags}. |
| * |
| * @param flags The expected flags. See SPAN_* constants on {@link Spanned} for possible values. |
| */ |
| void andFlags(int flags); |
| } |
| |
| private static final AndSpanFlags ALREADY_FAILED_AND_FLAGS = flags -> {}; |
| |
| /** |
| * Allows additional assertions to be made on the flags of matching spans. |
| * |
| * <p>Identical to {@link AndSpanFlags}, but this should be returned from {@code has...()} methods |
| * while {@link AndSpanFlags} should be returned from {@code with...()} methods. |
| */ |
| public interface WithSpanFlags { |
| |
| /** |
| * Checks that one of the matched spans has the expected {@code flags}. |
| * |
| * @param flags The expected flags. See SPAN_* constants on {@link Spanned} for possible values. |
| */ |
| void withFlags(int flags); |
| } |
| |
| private static final WithSpanFlags ALREADY_FAILED_WITH_FLAGS = flags -> {}; |
| |
| private static Factory<SpanFlagsSubject, List<Integer>> spanFlags() { |
| return SpanFlagsSubject::new; |
| } |
| |
| private static final class SpanFlagsSubject extends Subject |
| implements AndSpanFlags, WithSpanFlags { |
| |
| private final List<Integer> flags; |
| |
| private SpanFlagsSubject(FailureMetadata metadata, List<Integer> flags) { |
| super(metadata, flags); |
| this.flags = flags; |
| } |
| |
| @Override |
| public void andFlags(int flags) { |
| check("contains()").that(this.flags).contains(flags); |
| } |
| |
| @Override |
| public void withFlags(int flags) { |
| andFlags(flags); |
| } |
| } |
| |
| /** 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<AlignmentSpansSubject, List<AlignmentSpan>> alignmentSpans( |
| Spanned actualSpanned) { |
| return (FailureMetadata metadata, List<AlignmentSpan> spans) -> |
| new AlignmentSpansSubject(metadata, spans, actualSpanned); |
| } |
| |
| private static final class AlignmentSpansSubject extends Subject implements Aligned { |
| |
| private final List<AlignmentSpan> actualSpans; |
| private final Spanned actualSpanned; |
| |
| private AlignmentSpansSubject( |
| FailureMetadata metadata, List<AlignmentSpan> actualSpans, Spanned actualSpanned) { |
| super(metadata, actualSpans); |
| this.actualSpans = actualSpans; |
| this.actualSpanned = actualSpanned; |
| } |
| |
| @Override |
| public AndSpanFlags withAlignment(Alignment alignment) { |
| List<Integer> matchingSpanFlags = new ArrayList<>(); |
| List<Alignment> 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 { |
| |
| /** |
| * Checks that at least one of the matched spans has the expected {@code color}. |
| * |
| * @param color The expected color. |
| * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. |
| */ |
| AndSpanFlags withColor(@ColorInt int color); |
| } |
| |
| private static final Colored ALREADY_FAILED_COLORED = color -> ALREADY_FAILED_AND_FLAGS; |
| |
| private static Factory<ForegroundColorSpansSubject, List<ForegroundColorSpan>> |
| foregroundColorSpans(Spanned actualSpanned) { |
| return (FailureMetadata metadata, List<ForegroundColorSpan> spans) -> |
| new ForegroundColorSpansSubject(metadata, spans, actualSpanned); |
| } |
| |
| private static final class ForegroundColorSpansSubject extends Subject implements Colored { |
| |
| private final List<ForegroundColorSpan> actualSpans; |
| private final Spanned actualSpanned; |
| |
| private ForegroundColorSpansSubject( |
| FailureMetadata metadata, List<ForegroundColorSpan> actualSpans, Spanned actualSpanned) { |
| super(metadata, actualSpans); |
| this.actualSpans = actualSpans; |
| this.actualSpanned = actualSpanned; |
| } |
| |
| @Override |
| public AndSpanFlags withColor(@ColorInt int color) { |
| List<Integer> matchingSpanFlags = new ArrayList<>(); |
| // Use hex strings for comparison so the values in error messages are more human readable. |
| List<String> spanColors = new ArrayList<>(); |
| |
| for (ForegroundColorSpan span : actualSpans) { |
| spanColors.add(String.format("0x%08X", span.getForegroundColor())); |
| if (span.getForegroundColor() == color) { |
| matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); |
| } |
| } |
| |
| String expectedColorString = String.format("0x%08X", color); |
| check("foregroundColor").that(spanColors).containsExactly(expectedColorString); |
| return check("flags").about(spanFlags()).that(matchingSpanFlags); |
| } |
| } |
| |
| private static Factory<BackgroundColorSpansSubject, List<BackgroundColorSpan>> |
| backgroundColorSpans(Spanned actualSpanned) { |
| return (FailureMetadata metadata, List<BackgroundColorSpan> spans) -> |
| new BackgroundColorSpansSubject(metadata, spans, actualSpanned); |
| } |
| |
| private static final class BackgroundColorSpansSubject extends Subject implements Colored { |
| |
| private final List<BackgroundColorSpan> actualSpans; |
| private final Spanned actualSpanned; |
| |
| private BackgroundColorSpansSubject( |
| FailureMetadata metadata, List<BackgroundColorSpan> actualSpans, Spanned actualSpanned) { |
| super(metadata, actualSpans); |
| this.actualSpans = actualSpans; |
| this.actualSpanned = actualSpanned; |
| } |
| |
| @Override |
| public AndSpanFlags withColor(@ColorInt int color) { |
| List<Integer> matchingSpanFlags = new ArrayList<>(); |
| // Use hex strings for comparison so the values in error messages are more human readable. |
| List<String> spanColors = new ArrayList<>(); |
| |
| for (BackgroundColorSpan span : actualSpans) { |
| spanColors.add(String.format("0x%08X", span.getBackgroundColor())); |
| if (span.getBackgroundColor() == color) { |
| matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); |
| } |
| } |
| |
| String expectedColorString = String.format("0x%08X", color); |
| check("backgroundColor").that(spanColors).containsExactly(expectedColorString); |
| return check("flags").about(spanFlags()).that(matchingSpanFlags); |
| } |
| } |
| |
| /** 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<TypefaceSpansSubject, List<TypefaceSpan>> typefaceSpans( |
| Spanned actualSpanned) { |
| return (FailureMetadata metadata, List<TypefaceSpan> spans) -> |
| new TypefaceSpansSubject(metadata, spans, actualSpanned); |
| } |
| |
| private static final class TypefaceSpansSubject extends Subject implements Typefaced { |
| |
| private final List<TypefaceSpan> actualSpans; |
| private final Spanned actualSpanned; |
| |
| private TypefaceSpansSubject( |
| FailureMetadata metadata, List<TypefaceSpan> actualSpans, Spanned actualSpanned) { |
| super(metadata, actualSpans); |
| this.actualSpans = actualSpans; |
| this.actualSpanned = actualSpanned; |
| } |
| |
| @Override |
| public AndSpanFlags withFamily(String fontFamily) { |
| List<Integer> matchingSpanFlags = new ArrayList<>(); |
| List<@NullableType String> 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 the absolute size of a span. */ |
| public interface AbsoluteSized { |
| |
| /** |
| * Checks that at least one of the matched spans has the expected {@code size}. |
| * |
| * @param size The expected size. |
| * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. |
| */ |
| AndSpanFlags withAbsoluteSize(int size); |
| } |
| |
| private static final AbsoluteSized ALREADY_FAILED_ABSOLUTE_SIZED = |
| size -> ALREADY_FAILED_AND_FLAGS; |
| |
| private static Factory<AbsoluteSizeSpansSubject, List<AbsoluteSizeSpan>> absoluteSizeSpans( |
| Spanned actualSpanned) { |
| return (FailureMetadata metadata, List<AbsoluteSizeSpan> spans) -> |
| new AbsoluteSizeSpansSubject(metadata, spans, actualSpanned); |
| } |
| |
| private static final class AbsoluteSizeSpansSubject extends Subject implements AbsoluteSized { |
| |
| private final List<AbsoluteSizeSpan> actualSpans; |
| private final Spanned actualSpanned; |
| |
| private AbsoluteSizeSpansSubject( |
| FailureMetadata metadata, List<AbsoluteSizeSpan> actualSpans, Spanned actualSpanned) { |
| super(metadata, actualSpans); |
| this.actualSpans = actualSpans; |
| this.actualSpanned = actualSpanned; |
| } |
| |
| @Override |
| public AndSpanFlags withAbsoluteSize(int size) { |
| List<Integer> matchingSpanFlags = new ArrayList<>(); |
| List<Integer> spanSizes = new ArrayList<>(); |
| |
| for (AbsoluteSizeSpan span : actualSpans) { |
| spanSizes.add(span.getSize()); |
| if (span.getSize() == size) { |
| matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); |
| } |
| } |
| |
| check("absoluteSize").that(spanSizes).containsExactly(size); |
| return check("flags").about(spanFlags()).that(matchingSpanFlags); |
| } |
| } |
| |
| /** Allows assertions about the relative size of a span. */ |
| public interface RelativeSized { |
| /** |
| * Checks that at least one of the matched spans has the expected {@code sizeChange}. |
| * |
| * @param sizeChange The expected size change. |
| * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. |
| */ |
| AndSpanFlags withSizeChange(float sizeChange); |
| } |
| |
| private static final RelativeSized ALREADY_FAILED_RELATIVE_SIZED = |
| sizeChange -> ALREADY_FAILED_AND_FLAGS; |
| |
| private static Factory<RelativeSizeSpansSubject, List<RelativeSizeSpan>> relativeSizeSpans( |
| Spanned actualSpanned) { |
| return (FailureMetadata metadata, List<RelativeSizeSpan> spans) -> |
| new RelativeSizeSpansSubject(metadata, spans, actualSpanned); |
| } |
| |
| private static final class RelativeSizeSpansSubject extends Subject implements RelativeSized { |
| |
| private final List<RelativeSizeSpan> actualSpans; |
| private final Spanned actualSpanned; |
| |
| private RelativeSizeSpansSubject( |
| FailureMetadata metadata, List<RelativeSizeSpan> actualSpans, Spanned actualSpanned) { |
| super(metadata, actualSpans); |
| this.actualSpans = actualSpans; |
| this.actualSpanned = actualSpanned; |
| } |
| |
| @Override |
| public AndSpanFlags withSizeChange(float size) { |
| List<Integer> matchingSpanFlags = new ArrayList<>(); |
| List<Float> spanSizes = new ArrayList<>(); |
| |
| for (RelativeSizeSpan span : actualSpans) { |
| spanSizes.add(span.getSizeChange()); |
| if (span.getSizeChange() == size) { |
| matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); |
| } |
| } |
| |
| check("sizeChange").that(spanSizes).containsExactly(size); |
| 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 static 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 final 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(@Nullable 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); |
| } |
| } |
| } |
| } |