/* * 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 android.graphics.drawable.Drawable; import android.view.View; import android.widget.TextView; import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; import org.assertj.core.api.AbstractAssert; import org.assertj.core.api.Java6Assertions; import org.robolectric.RuntimeEnvironment; import static com.facebook.litho.testing.viewtree.ViewExtractors.GET_TEXT_FUNCTION; import static com.facebook.litho.testing.viewtree.ViewPredicates.hasTextMatchingPredicate; import static com.facebook.litho.testing.viewtree.ViewPredicates.hasVisibleId; import static com.facebook.litho.testing.viewtree.ViewPredicates.isVisible; /** * Assertions which require checking an entire view tree * * NOTE: Assertions looking for visible attributes are limited to checking the visibility of the * nodes, but do not check actual layout. So a visible view might have 0 pixels available for it * in actual app code and still pass the checks done here */ public final class ViewTreeAssert extends AbstractAssert<ViewTreeAssert, ViewTree> { private ViewTreeAssert(final ViewTree actual) { super(actual, ViewTreeAssert.class); } public static ViewTreeAssert assertThat(final ViewTree actual) { return new ViewTreeAssert(actual); } /** * Tests if any view in the hierarchy under the root, for which the path is visible, has the * requested piece of text as its text * * @param text the text to search for * @return the assertions object */ public ViewTreeAssert hasVisibleText(final String text) { final ImmutableList<View> path = getPathToVisibleText(text); Java6Assertions.assertThat(path) .overridingErrorMessage(path == null ? getHasVisibleTextErrorMessage(text) : "") .isNotNull(); return this; } private String getHasVisibleTextErrorMessage(final String text) { String errorMsg = String.format( "Cannot find text \"%s\" in view hierarchy:%n%s. ", text, actual.makeString(GET_TEXT_FUNCTION)); final ImmutableList<View> similarPath = getPathToVisibleSimilarText(text); if (similarPath != null) { errorMsg += String.format( "\nHowever, a near-match was found: \"%s\"", GET_TEXT_FUNCTION.apply(similarPath.get(similarPath.size() - 1))); } else { errorMsg += "\nNo near-match was found."; } return errorMsg; } /** * Tests if any view in the hierarchy under the root, for which the path is visible, has the * requested piece of text as its text and has a tag set on that TextView with the given tag id * and tag value. * * @param text the text to search for * @param tagId the tag to look for on the TextView containing the searched text * @param tagValue the expected value of the tag associated with tagId * @return the assertions object */ public ViewTreeAssert hasVisibleTextWithTag(final String text, final int tagId, final Object tagValue) { final ImmutableList<View> path = getPathToVisibleTextWithTag(text, tagId, tagValue); Java6Assertions.assertThat(path) .overridingErrorMessage( "Cannot find text \"%s\" with tagId \"%d\" and value:%s in view hierarchy:%n%s", text, tagId, tagValue.toString(), actual.makeString(GET_TEXT_FUNCTION)) .isNotNull(); return this; } /** * Tests if any view has visible text identified by the resource id * * @param resourceId resource id of the text * @return the assertions object */ public ViewTreeAssert hasVisibleText(final int resourceId) { return hasVisibleText( RuntimeEnvironment .application .getResources() .getString(resourceId)); } /** * Tests that all views in the hierarchy under the root, for which the path is visible, do not * have text equal to the given string * * @param text the text to search for * @return the assertions object */ public ViewTreeAssert doesNotHaveVisibleText(final String text) { final ImmutableList<View> path = getPathToVisibleText(text); Java6Assertions.assertThat(path) .overridingErrorMessage( "Found text \"%s\" in view hierarchy for path: %s", text, makeString(path)) .isNull(); return this; } /** * Tests if any view hierarchy under the root has the given view tag and value. * @param tagId the id to look for * @param tagValue the value that the id should have * @return the assertions object */ public ViewTreeAssert hasViewTag(final int tagId, final Object tagValue) { final ImmutableList<View> path = getPathToViewTag(tagId, tagValue); Java6Assertions.assertThat(path) .overridingErrorMessage( "Cannot find tag id \"%d\" with tag value \"%s\" in view hierarchy:%n%s", tagId, tagValue, actual.makeString(ViewExtractors.generateGetViewTagFunction(tagId))) .isNotNull(); return this; } /** * Tests if any view hierarchy under the root has the given contentDescription. * @param contentDescription the contentDescription to search for * @return the assertions object */ public ViewTreeAssert hasContentDescription(final String contentDescription) { final ImmutableList<View> path = getPathToContentDescription(contentDescription); Java6Assertions.assertThat(path) .overridingErrorMessage( "Cannot find content description \"%s\" in view hierarchy:%n%s", contentDescription, actual.makeString(ViewExtractors.GET_CONTENT_DESCRIPTION_FUNCTION)) .isNotNull(); return this; } /** * Tests that all views in the hierarchy under the root, for which the path is visible, do not * have text equal to the string matching the given resource id * * @param resourceId resource id of the text * @return the assertions object */ public ViewTreeAssert doesNotHaveVisibleText(final int resourceId) { return doesNotHaveVisibleText( RuntimeEnvironment .application .getResources() .getString(resourceId)); } /** * Tests if any view hierarchy under the root has the given contentDescription. * @param resourceId the resId of the contentDescription to search for * @return the assertions object */ public ViewTreeAssert hasContentDescription(final int resourceId) { return hasContentDescription( RuntimeEnvironment .application .getResources() .getString(resourceId)); } private ImmutableList<View> getPathToVisibleSimilarText(final String text) { return actual.findChild( Predicates.and( isVisible(), hasTextMatchingPredicate(new Predicate<String>() { @Override public boolean apply(@Nullable final String input) { final int maxEditDistance = Math.max(3, text.length() / 4); return LevenshteinDistance.getLevenshteinDistance(text, input, maxEditDistance) <= maxEditDistance; } })), ViewPredicates.isVisible()); } private ImmutableList<View> getPathToVisibleText(final String text) { return actual.findChild( ViewPredicates.hasVisibleText(text), ViewPredicates.isVisible()); } private ImmutableList<View> getPathToVisibleTextWithTag(final String text, final int tagId, final Object tagValue) { return actual.findChild( ViewPredicates.hasVisibleTextWithTag(text, tagId, tagValue), ViewPredicates.isVisible()); } private ImmutableList<View> getPathToViewTag(final int tagId, final Object tagValue) { return actual.findChild(ViewPredicates.hasTag(tagId, tagValue)); } private ImmutableList<View> getPathToContentDescription(final String contentDescription) { return actual.findChild(ViewPredicates.hasContentDescription(contentDescription)); } /** * Tests if any view in the hierarchy under the root, for which the path is visible, has text that * matches the given regular expression * * @param pattern the regular expression to match against * @return the assertions object */ public ViewTreeAssert hasVisibleTextMatching(final String pattern) { final ImmutableList<View> path = getPathToVisibleMatchingText(pattern); Java6Assertions.assertThat(path) .overridingErrorMessage( "Cannot find text matching \"%s\" in view hierarchy:%n%s", pattern, actual.makeString(GET_TEXT_FUNCTION)) .isNotNull(); return this; } /** * Tests that all views in the hierarchy under the root, for which the path is visible, do not * have text that matches against the given regular expression * * @param pattern the regular expression to match against * @return the assertions object( */ public ViewTreeAssert doesNotHaveVisibleTextMatching(final String pattern) { final ImmutableList<View> path = getPathToVisibleMatchingText(pattern); Java6Assertions.assertThat(path) .overridingErrorMessage( "Found pattern \"%s\" in view hierarchy for path: %s", pattern, makeString(path)) .isNull(); return this; } /** * Tests that all views in the hierarchy under the root, for which the path is visible, do not * have any text appearing on them * * @return the assertions object */ public ViewTreeAssert doesNotHaveVisibleText() { final ImmutableList<View> path = getPathToVisibleMatchingText(".+"); Java6Assertions.assertThat(path) .overridingErrorMessage( "Found text \"%s\" in view hierarchy for path: %s", getTextProof(path), makeString(path)) .isNull(); return this; } private String getTextProof(@Nullable final ImmutableList<View> path) { if (path == null) { return ""; } final View last = path.get(path.size() - 1); return ((TextView) last).getText().toString(); } private ImmutableList<View> getPathToVisibleMatchingText(final String pattern) { return actual.findChild( ViewPredicates.hasVisibleMatchingText(pattern), ViewPredicates.isVisible()); } private String makeString(final Iterable<View> path) { return path != null ? Joiner.on(" -> ").join(path) : ""; } /** * Tests if any view in the hierarchy under the root, for which the path is visible, is displaying * the requested drawable by the given resource id. * * For this assertion to work, Robolectric must be immediately available and be able to load the * drawable corresponding to this resource id. * * @param resourceId the resource id of the drawable to look for * @return the assertions object */ public ViewTreeAssert hasVisibleDrawable(final int resourceId) { hasVisibleDrawable( RuntimeEnvironment .application .getResources() .getDrawable(resourceId) ); return this; } /** * Tests if any view in the hierarchy under the root, for which the path is visible, is displaying * the requested drawable * * @param drawable the drawable to look for * @return the assertions object */ public ViewTreeAssert hasVisibleDrawable(final Drawable drawable) { final ImmutableList<View> path = getPathToVisibleWithDrawable(drawable); Java6Assertions.assertThat(path) .overridingErrorMessage( "Did not find drawable %s in view hierarchy:%n%s", drawable, actual.makeString(ViewExtractors.GET_DRAWABLE_FUNCTION)) .isNotNull(); return this; } /** * Tests all views in the hierarchy under the root, for which the path is visible, do not have * the requested drawable by the given resource id. * For this assertion to work, Robolectric must be immediately available and be able to load the * drawable corresponding to this resource id. * * @param resourceId the resource id of the drawable to look for * @return the assertions object */ public ViewTreeAssert doesNotHaveVisibleDrawable(final int resourceId) { doesNotHaveVisibleDrawable( RuntimeEnvironment .application .getResources() .getDrawable(resourceId) ); return this; } /** * Tests all views in the hierarchy under the root, for which the path is visible, are not * displaying the requested drawable * * @param drawable the drawable to look for * @return the assertions object */ public ViewTreeAssert doesNotHaveVisibleDrawable(final Drawable drawable) { final ImmutableList<View> path = getPathToVisibleWithDrawable(drawable); Java6Assertions.assertThat(path) .overridingErrorMessage( "Found drawable %s in view hierarchy:%n%s", drawable, actual.makeString(ViewExtractors.GET_DRAWABLE_FUNCTION)) .isNull(); return this; } /** Whether there is a visible view in the hierarchy with the given id. */ public ViewTreeAssert hasVisibleViewWithId(final int viewId) { final ImmutableList<View> path = getPathToVisibleWithId(viewId); Java6Assertions.assertThat(path) .overridingErrorMessage( "Did not find visible view with id \"%s=%d\":%n%s", ViewTreeUtil.getResourceName(viewId), viewId, actual.makeString(ViewExtractors.GET_VIEW_ID_FUNCTION)) .isNotNull(); return this; } /** Whether there is not a visible view in the hierarchy with the given id. */ public ViewTreeAssert doesNotHaveVisibleViewWithId(final int viewId) { final ImmutableList<View> path = getPathToVisibleWithId(viewId); Java6Assertions.assertThat(path) .overridingErrorMessage( "Found visible view with id \"%s=%d\":%n%s", ViewTreeUtil.getResourceName(viewId), viewId, actual.makeString(ViewExtractors.GET_VIEW_ID_FUNCTION)) .isNull(); return this; } public <V extends View> ViewTreeAssert hasVisible(final Class<V> clazz, final Predicate<V> predicate) { final Predicate<View> conjunction = Predicates.and( Predicates.instanceOf(clazz), ViewPredicates.isVisible(), (Predicate<View>) predicate); final ImmutableList<View> path = actual.findChild( conjunction, ViewPredicates.isVisible()); Java6Assertions.assertThat(path) .overridingErrorMessage( "Did not find view for which given predicate is true in view hierarchy:%n%s", actual.makeString(null)) .isNotNull(); return this; } public <V extends View> ViewTreeAssert doesNotHaveVisible( final Class<V> clazz, final Predicate<V> predicate) { final Predicate<View> conjunction = Predicates.and( Predicates.instanceOf(clazz), ViewPredicates.isVisible(), (Predicate<View>) predicate); final ImmutableList<View> path = actual.findChild( conjunction, ViewPredicates.isVisible()); Java6Assertions.assertThat(path) .overridingErrorMessage( "Found a view for which given predicate is true in view hierarchy:%n%s", actual.makeString(null)) .isNull(); return this; } private ImmutableList<View> getPathToVisibleWithDrawable(final Drawable drawable) { return actual.findChild( ViewPredicates.hasVisibleDrawable(drawable), ViewPredicates.isVisible()); } private ImmutableList<View> getPathToVisibleWithId(final int viewId) { return actual.findChild(hasVisibleId(viewId), isVisible()); } }