Home | History | Annotate | Download | only in base
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *   http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.google.android.apps.common.testing.ui.espresso.base;
     18 
     19 import static com.google.android.apps.common.testing.ui.espresso.util.TreeIterables.breadthFirstViewTraversal;
     20 import static com.google.common.base.Preconditions.checkNotNull;
     21 import static com.google.common.base.Preconditions.checkState;
     22 
     23 import com.google.android.apps.common.testing.ui.espresso.AmbiguousViewMatcherException;
     24 import com.google.android.apps.common.testing.ui.espresso.NoMatchingViewException;
     25 import com.google.android.apps.common.testing.ui.espresso.ViewFinder;
     26 import com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers;
     27 import com.google.common.base.Joiner;
     28 import com.google.common.base.Optional;
     29 import com.google.common.base.Predicate;
     30 import com.google.common.collect.Iterables;
     31 import com.google.common.collect.Iterators;
     32 import com.google.common.collect.Lists;
     33 
     34 import android.os.Looper;
     35 import android.view.View;
     36 import android.widget.AdapterView;
     37 
     38 import org.hamcrest.Matcher;
     39 
     40 import java.util.Iterator;
     41 import java.util.List;
     42 
     43 import javax.inject.Inject;
     44 import javax.inject.Provider;
     45 
     46 /**
     47  * Implementation of {@link ViewFinder}.
     48  */
     49 // TODO(user): in the future we may want to collect stats here about the size of the view
     50 // hierarchy, average matcher execution time, warn when matchers take too long to execute, etc.
     51 public final class ViewFinderImpl implements ViewFinder {
     52 
     53   private final Matcher<View> viewMatcher;
     54   private final Provider<View> rootViewProvider;
     55 
     56   @Inject
     57   ViewFinderImpl(Matcher<View> viewMatcher, Provider<View> rootViewProvider) {
     58     this.viewMatcher = viewMatcher;
     59     this.rootViewProvider = rootViewProvider;
     60   }
     61 
     62   @Override
     63   public View getView() throws AmbiguousViewMatcherException, NoMatchingViewException {
     64     checkMainThread();
     65     final Predicate<View> matcherPredicate = new MatcherPredicateAdapter<View>(
     66         checkNotNull(viewMatcher));
     67 
     68     View root = rootViewProvider.get();
     69     Iterator<View> matchedViewIterator = Iterables.filter(
     70         breadthFirstViewTraversal(root),
     71         matcherPredicate).iterator();
     72 
     73     View matchedView = null;
     74 
     75     while (matchedViewIterator.hasNext()) {
     76       if (matchedView != null) {
     77         // Ambiguous!
     78         throw new AmbiguousViewMatcherException.Builder()
     79             .withViewMatcher(viewMatcher)
     80             .withRootView(root)
     81             .withView1(matchedView)
     82             .withView2(matchedViewIterator.next())
     83             .withOtherAmbiguousViews(Iterators.toArray(matchedViewIterator, View.class))
     84             .build();
     85       } else {
     86         matchedView = matchedViewIterator.next();
     87       }
     88     }
     89     if (null == matchedView) {
     90       final Predicate<View> adapterViewPredicate = new MatcherPredicateAdapter<View>(
     91           ViewMatchers.isAssignableFrom(AdapterView.class));
     92       List<View> adapterViews = Lists.newArrayList(
     93           Iterables.filter(breadthFirstViewTraversal(root), adapterViewPredicate).iterator());
     94       if (adapterViews.isEmpty()) {
     95         throw new NoMatchingViewException.Builder()
     96             .withViewMatcher(viewMatcher)
     97             .withRootView(root)
     98             .build();
     99       }
    100 
    101       String warning = String.format("\nIf the target view is not part of the view hierarchy, you "
    102         + "may need to use Espresso.onData to load it from one of the following AdapterViews:%s"
    103         , Joiner.on("\n- ").join(adapterViews));
    104       throw new NoMatchingViewException.Builder()
    105           .withViewMatcher(viewMatcher)
    106           .withRootView(root)
    107           .withAdapterViews(adapterViews)
    108           .withAdapterViewWarning(Optional.of(warning))
    109           .build();
    110     } else {
    111       return matchedView;
    112     }
    113   }
    114 
    115   private void checkMainThread() {
    116     checkState(Thread.currentThread().equals(Looper.getMainLooper().getThread()),
    117         "Executing a query on the view hierarchy outside of the main thread (on: %s)",
    118         Thread.currentThread().getName());
    119   }
    120 
    121   private static class MatcherPredicateAdapter<T> implements Predicate<T> {
    122     private final Matcher<? super T> matcher;
    123 
    124     private MatcherPredicateAdapter(Matcher<? super T> matcher) {
    125       this.matcher = checkNotNull(matcher);
    126     }
    127 
    128     @Override
    129     public boolean apply(T input) {
    130       return matcher.matches(input);
    131     }
    132   }
    133 }
    134