/* * Copyright (c) 2017-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.litho.testing.viewtree; import javax.annotation.Nullable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.List; import java.util.regex.Pattern; import android.annotation.TargetApi; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import com.facebook.litho.ComponentHost; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import org.robolectric.Shadows; import org.robolectric.shadows.ShadowCanvas; /** * A collection of useful predicates over Android views for tests */ final class ViewPredicates { private ViewPredicates() {} /** * Returns a predicate that returns true if the applied on view's text is equal to the given * text. * substring. * @param predicate the predicate with which to test the text * @return the predicate */ public static Predicate<View> hasTextMatchingPredicate(final Predicate<String> predicate) { return new Predicate<View>() { @Override public boolean apply(final View input) { if (predicate.apply(extractString(input))) { return true; } if (input instanceof ComponentHost) { return ComponentQueries.hasTextMatchingPredicate((ComponentHost) input, predicate); } return false; } }; } /** * Returns a predicate that returns true if the applied on view's text is equal to the given * text. * substring. * @param text the text to check * @return the predicate */ public static Predicate<View> hasText(final String text) { return hasTextMatchingPredicate(Predicates.equalTo(text)); } public static Predicate<View> hasTag(final int tagId, final Object tagValue) { return new Predicate<View>() { @Override public boolean apply(final View input) { final Object tag = input.getTag(tagId); return tag != null && tag.equals(tagValue); } }; } public static Predicate<View> hasContentDescription(final String contentDescription) { return new Predicate<View>() { @Override public boolean apply(final View input) { if (input instanceof ComponentHost) { final List<CharSequence> contentDescriptions = ((ComponentHost) input).getContentDescriptions(); return contentDescriptions.contains(contentDescription); } return contentDescription.equals(input.getContentDescription()); } }; } public static Predicate<View> hasVisibleText(final String text) { return Predicates.and(isVisible(), hasText(text)); } public static Predicate<View> hasVisibleTextWithTag( final String text, final int tagId, final Object tagValue) { return Predicates.and(hasVisibleText(text), hasTag(tagId, tagValue)); } public static Predicate<View> matchesText(final String text) { final Pattern pattern = Pattern.compile(text); return new Predicate<View>() { @Override public boolean apply(final View input) { if (pattern.matcher(extractString(input)).find()) { return true; } if (input instanceof ComponentHost) { return ComponentQueries.matchesPattern((ComponentHost) input, pattern); } return false; } }; } public static Predicate<View> hasVisibleMatchingText(final String text) { return Predicates.and(isVisible(), matchesText(text)); } public static Predicate<View> isVisible() { return new Predicate<View>() { @Override public boolean apply(final View input) { return input.getVisibility() == View.VISIBLE; } }; } @SuppressWarnings("unchecked") public static Predicate<View> isClass(final Class<? extends View> clazz) { return (Predicate<View>) (Predicate<?>) Predicates.instanceOf(clazz); } /** * Tries to extract the description of a drawn drawable from a canvas */ static String getDrawnDrawableDescription(final Drawable drawable) { final Canvas canvas = new Canvas(); drawable.draw(canvas); final ShadowCanvas shadowCanvas = Shadows.shadowOf(canvas); return shadowCanvas.getDescription(); } private static String extractString(final View view) { if (!(view instanceof TextView)) { return ""; } final CharSequence text = ((TextView) view).getText(); return text != null ? text.toString() : ""; } public static Predicate<View> hasDrawable(final Drawable drawable) { return new Predicate<View>() { @Override public boolean apply(@Nullable final View input) { if (input instanceof ImageView) { final Drawable imageDrawable = ((ImageView) input).getDrawable(); if (drawable instanceof BitmapDrawable && !(imageDrawable instanceof BitmapDrawable)) { return false; } } if (input instanceof ComponentHost) { return ComponentQueries.hasDrawable((ComponentHost) input, drawable); } final String drawnDrawableDescription = getDrawnDrawableDescription(drawable); return !drawnDrawableDescription.isEmpty() && getDrawnViewDescription(input).contains(drawnDrawableDescription); } }; } public static Predicate<View> hasVisibleDrawable(final Drawable drawable) { return Predicates.and(isVisible(), hasDrawable(drawable)); } /** @return A Predicate which is true if the view is visible and has the given id. */ public static Predicate<View> hasVisibleId(final int viewId) { return Predicates.and(isVisible(), hasId(viewId)); } /** * Tries to extract the description of a drawn view from a canvas * * Since Robolectric can screw up {@link View#draw}, this uses reflection to call * {@link View#onDraw} and give you a canvas that has all the information drawn into it. * This is useful for asserting some view draws something specific to a canvas. * * @param view the view to draw */ @TargetApi(Build.VERSION_CODES.KITKAT) private static String getDrawnViewDescription(View view) { final Canvas canvas = new Canvas(); view.draw(canvas); final ShadowCanvas shadowCanvas = Shadows.shadowOf(canvas); if (!shadowCanvas.getDescription().isEmpty()) { return shadowCanvas.getDescription(); } try { final Method onDraw = view.getClass().getDeclaredMethod("onDraw", Canvas.class); onDraw.setAccessible(true); onDraw.invoke(view, canvas); final ShadowCanvas shadowCanvas2 = Shadows.shadowOf(canvas); return shadowCanvas2.getDescription(); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(e); } } public static Predicate<View> hasId(final int id) { return new Predicate<View>() { @Override public boolean apply(final View input) { return input.getId() == id; } }; } }