1 // Copyright 2023 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.base.test.transit; 6 7 import static androidx.test.espresso.Espresso.onView; 8 import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; 9 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; 10 11 import static org.hamcrest.CoreMatchers.allOf; 12 import static org.hamcrest.CoreMatchers.any; 13 import static org.hamcrest.CoreMatchers.is; 14 15 import android.content.res.Resources; 16 import android.view.View; 17 18 import androidx.test.espresso.AmbiguousViewMatcherException; 19 import androidx.test.espresso.NoMatchingRootException; 20 import androidx.test.espresso.NoMatchingViewException; 21 import androidx.test.espresso.UiController; 22 import androidx.test.espresso.ViewAction; 23 import androidx.test.espresso.ViewInteraction; 24 import androidx.test.platform.app.InstrumentationRegistry; 25 26 import org.hamcrest.Matcher; 27 import org.hamcrest.StringDescription; 28 29 import java.util.ArrayList; 30 import java.util.regex.Pattern; 31 32 /** {@link Condition}s related to Android {@link View}s. */ 33 public class ViewConditions { 34 /** Fulfilled when a single matching View exists and is displayed. */ 35 public static class DisplayedCondition extends ExistsCondition { DisplayedCondition(Matcher<View> matcher)36 public DisplayedCondition(Matcher<View> matcher) { 37 super(allOf(matcher, isDisplayed())); 38 } 39 } 40 41 /** Fulfilled when a single matching View exists. */ 42 public static class ExistsCondition extends InstrumentationThreadCondition { 43 private final Matcher<View> mMatcher; 44 private View mViewMatched; 45 ExistsCondition(Matcher<View> matcher)46 public ExistsCondition(Matcher<View> matcher) { 47 super(); 48 this.mMatcher = matcher; 49 } 50 51 @Override buildDescription()52 public String buildDescription() { 53 return "View: " + ViewConditions.createMatcherDescription(mMatcher); 54 } 55 56 @Override check()57 public boolean check() { 58 ViewInteraction viewInteraction = onView(mMatcher); 59 try { 60 viewInteraction.perform( 61 new ViewAction() { 62 @Override 63 public Matcher<View> getConstraints() { 64 return any(View.class); 65 } 66 67 @Override 68 public String getDescription() { 69 return "check exists and consistent"; 70 } 71 72 @Override 73 public void perform(UiController uiController, View view) { 74 if (mViewMatched != null && mViewMatched != view) { 75 throw new IllegalStateException( 76 String.format( 77 "Matched a different view, was %s, now %s", 78 mViewMatched, view)); 79 } 80 mViewMatched = view; 81 } 82 }); 83 return true; 84 } catch (NoMatchingViewException 85 | NoMatchingRootException 86 | AmbiguousViewMatcherException e) { 87 if (mViewMatched != null) { 88 throw new IllegalStateException( 89 String.format( 90 "Had matched a view (%s), but now got %s", 91 mViewMatched, e.getClass().getSimpleName()), 92 e); 93 } 94 return false; 95 } 96 } 97 getViewMatched()98 public View getViewMatched() { 99 return mViewMatched; 100 } 101 } 102 103 /** Fulfilled when no matching Views exist. */ 104 public static class DoesNotExistAnymoreCondition extends InstrumentationThreadCondition { 105 private final Matcher<View> mMatcher; 106 private Matcher<View> mStricterMatcher; 107 private final ExistsCondition mExistsCondition; 108 DoesNotExistAnymoreCondition( Matcher<View> matcher, ExistsCondition existsCondition)109 public DoesNotExistAnymoreCondition( 110 Matcher<View> matcher, ExistsCondition existsCondition) { 111 super(); 112 mMatcher = matcher; 113 mExistsCondition = existsCondition; 114 } 115 116 @Override buildDescription()117 public String buildDescription() { 118 if (mStricterMatcher != null) { 119 return "No more view: " 120 + ViewConditions.createMatcherDescription(mMatcher) 121 + " that exactly " 122 + ViewConditions.createMatcherDescription(mStricterMatcher); 123 } else { 124 return "No more view: " + ViewConditions.createMatcherDescription(mMatcher); 125 } 126 } 127 128 @Override check()129 public boolean check() { 130 Matcher<View> matcherToUse; 131 if (mStricterMatcher != null) { 132 matcherToUse = mStricterMatcher; 133 } else if (mExistsCondition.getViewMatched() != null) { 134 mStricterMatcher = is(mExistsCondition.getViewMatched()); 135 rebuildDescription(); 136 matcherToUse = mStricterMatcher; 137 } else { 138 matcherToUse = mMatcher; 139 } 140 141 try { 142 onView(matcherToUse).check(doesNotExist()); 143 return true; 144 } catch (AssertionError e) { 145 return false; 146 } 147 } 148 } 149 getResourceName(int resId)150 private static String getResourceName(int resId) { 151 return InstrumentationRegistry.getInstrumentation() 152 .getContext() 153 .getResources() 154 .getResourceName(resId); 155 } 156 157 /** Generates a description for the matcher that replaces raw ids with resource names. */ createMatcherDescription(Matcher<View> matcher)158 private static String createMatcherDescription(Matcher<View> matcher) { 159 StringDescription d = new StringDescription(); 160 matcher.describeTo(d); 161 String description = d.toString(); 162 Pattern numberPattern = Pattern.compile("[0-9]+"); 163 java.util.regex.Matcher numberMatcher = numberPattern.matcher(description); 164 ArrayList<Integer> starts = new ArrayList<>(); 165 ArrayList<Integer> ends = new ArrayList<>(); 166 ArrayList<String> resourceNames = new ArrayList<>(); 167 while (numberMatcher.find()) { 168 int resourceId = Integer.parseInt(numberMatcher.group()); 169 if (resourceId > 0xFFFFFF) { 170 // Build-time Android resources have ids > 0xFFFFFF 171 starts.add(numberMatcher.start()); 172 ends.add(numberMatcher.end()); 173 String resourceDescription = createResourceDescription(resourceId); 174 resourceNames.add(resourceDescription); 175 } else { 176 resourceNames.add(numberMatcher.group()); 177 } 178 } 179 180 if (starts.size() == 0) return description; 181 182 String newDescription = description.substring(0, starts.get(0)); 183 for (int i = 0; i < starts.size(); i++) { 184 newDescription += resourceNames.get(i); 185 int nextStart = (i == starts.size() - 1) ? description.length() : starts.get(i + 1); 186 newDescription += description.substring(ends.get(i), nextStart); 187 } 188 189 return newDescription; 190 } 191 createResourceDescription(int possibleResourceId)192 private static String createResourceDescription(int possibleResourceId) { 193 try { 194 return getResourceName(possibleResourceId); 195 } catch (Resources.NotFoundException e) { 196 return String.valueOf(possibleResourceId); 197 } 198 } 199 } 200