Espresso之onView()&onData()

總目錄:Espresso從開始到…

如果你看了上一篇中對一些常用方法的介紹,想必現在已經可以對Espresso正常進行使用了,從這裏開始我們開始看一些“簡單”的東西。瞭解一下這三部曲是什麼。

onView(ViewMatcher)
    .perform(ViewAction)
    .check(ViewAssertion);

從onView()&onData()開始

作爲一個程序猿自然不能滿足於三部曲,雖然不能什麼都一清二楚,但是最差也要知道大概的流程吧。不然都不好意思說自己用過Espresso。所以與 Espresso 的故事就是從 Ctrl 打開 onView()開始了。
這裏直接進入 Espresso 類, 這裏主要有幾個常用的靜態工具函數

函數名 功能
pressBack() 返回鍵
closeSoftKeyboard() 關閉軟鍵盤
openActionBarOverflowOrOptionsMenu() 菜單鍵
openContextualActionModeOverflowMenu(); 實體鍵盤菜單鍵

還有幾個registerIdlingResourcesunregisterIdlingResources等關於IdlingResources的函數。以及本文的關鍵onView()onData(),在這裏分別生成了ViewInteractionDataInteraction

 public static ViewInteraction onView(final Matcher<View> viewMatcher) {
    return BASE.plus(new ViewInteractionModule(viewMatcher)).viewInteraction();
  }

注:onView 在這裏使用了 Dagger 框架 ,由於筆者沒有使用過該框架,在這裏就不多贅述,感興趣的可以自行查閱。

   public static DataInteraction onData(Matcher<? extends Object> dataMatcher) {
    return new DataInteraction(dataMatcher);
  }

ViewInteraction

ViewInteraction中的公共函數非常少,只有四個:

函數名 功能
perform() 執行ViewAction操作
check() 檢查ViewAssertion
inRoot() 確定目標view所在的root
withFailureHandler() 提供錯誤處理方式

1.perform()

我們還是按照三部曲的順序進行先看perform()

public ViewInteraction perform(final ViewAction... viewActions) {
    checkNotNull(viewActions);
    for (ViewAction action : viewActions) {
        doPerform(action);
    }
    return this;
  }

從方法的形參ViewAction... viewActions我們可以知道perform()是支持同時執行多個操作的,但是會通過doPerform(action)按照順序依次執行。
到這裏問題就來了,如果按照三部曲的理解來說,現在應該開始對控件執行操作了,但是需要操作的控件在哪?我們至今沒有看到,難道在onView()初始化的過程中已經將View檢索出來存儲爲成員變量了?ok,我們來看一下 ViewInteraction 有哪些成員變量:

 //用於進行簡單UI操作的工具
  private final UiController uiController;
  
 //用於查找View的工具
  private final ViewFinder viewFinder;
  
  //執行已提交 runnable 任務的對象
  private final Executor mainThreadExecutor;
  
  //錯誤處理機制與 withFailureHandler() 有關
  private volatile FailureHandler failureHandler;
  
  //view的匹配器(我們在onView(viewMatcher)傳入的)
  private final Matcher<View> viewMatcher;
  
  //缺點查詢 view 的 root 與 inRoot() 有關
  private final AtomicReference<Matcher<Root>> rootMatcherRef;

好吧,現實並不是想象的那樣,ViewInteraction 並沒有存儲 view ,裏面只有用於查找 view 的工具(ViewFinder)和材料(Matcher<View>)。看來答案需要在接下來的doPerform(action)中尋找了。讓我們看一下代碼:

private void doPerform(final ViewAction viewAction) {
    checkNotNull(viewAction);
    final Matcher<? extends View> constraints = checkNotNull(viewAction.getConstraints());
    runSynchronouslyOnUiThread(new Runnable() {

      @Override
      public void run() {
        uiController.loopMainThreadUntilIdle();
        View targetView = viewFinder.getView();
        Log.i(TAG, String.format(
            "Performing '%s' action on view %s", viewAction.getDescription(), viewMatcher));
        if (!constraints.matches(targetView)) {
          // TODO(user): update this to describeMismatch once hamcrest is updated to new
          StringDescription stringDescription = new StringDescription(new StringBuilder(
              "Action will not be performed because the target view "
              + "does not match one or more of the following constraints:\n"));
          constraints.describeTo(stringDescription);
          stringDescription.appendText("\nTarget view: ")
              .appendValue(HumanReadables.describe(targetView));

          if (viewAction instanceof ScrollToAction
              && isDescendantOfA(isAssignableFrom((AdapterView.class))).matches(targetView)) {
            stringDescription.appendText(
                "\nFurther Info: ScrollToAction on a view inside an AdapterView will not work. "
                + "Use Espresso.onData to load the view.");
          }

          throw new PerformException.Builder()
            .withActionDescription(viewAction.getDescription())
            .withViewDescription(viewMatcher.toString())
            .withCause(new RuntimeException(stringDescription.toString()))
            .build();
        } else {
          viewAction.perform(uiController, targetView);
        }
      }
    });
  }

函數開始,先對viewAction的可看性進行檢查,並獲取viewAction操作對 view 限制條件 constraints (絕大多數操作只能在相對應的控件上進行操作),然後的操作在runSynchronouslyOnUiThread()中進行。
View targetView = viewFinder.getView();從字面上看應該是用來鎖定目標 view 。接下來進行判斷目標view是否符合constraints 要求,符合要求則正式進行perform(),不符合要求的話,會打印日誌提示,當前空間無法執行本操作,並且判斷是否是由於onView()&onData()的錯誤使用造成。


perform()的使用基本介紹完了。現在,我們找出我們想要的View targetView = viewFinder.getView();跟蹤進去,看一下 view 是怎麼來的:

public View getView() throws AmbiguousViewMatcherException, NoMatchingViewException {
    checkMainThread();
    final Predicate<View> matcherPredicate = new MatcherPredicateAdapter<View>(
        checkNotNull(viewMatcher));

    View root = rootViewProvider.get();
    Iterator<View> matchedViewIterator = Iterables.filter(
        breadthFirstViewTraversal(root),
        matcherPredicate).iterator();

    View matchedView = null;

    while (matchedViewIterator.hasNext()) {
      if (matchedView != null) {
        // Ambiguous!
        throw new AmbiguousViewMatcherException.Builder()
            .withViewMatcher(viewMatcher)
            .withRootView(root)
            .withView1(matchedView)
            .withView2(matchedViewIterator.next())
            .withOtherAmbiguousViews(Iterators.toArray(matchedViewIterator, View.class))
            .build();
      } else {
        matchedView = matchedViewIterator.next();
      }
    }
    if (null == matchedView) {
      final Predicate<View> adapterViewPredicate = new MatcherPredicateAdapter<View>(
          ViewMatchers.isAssignableFrom(AdapterView.class));
      List<View> adapterViews = Lists.newArrayList(
          Iterables.filter(breadthFirstViewTraversal(root), adapterViewPredicate).iterator());
      if (adapterViews.isEmpty()) {
        throw new NoMatchingViewException.Builder()
            .withViewMatcher(viewMatcher)
            .withRootView(root)
            .build();
      }
      String warning = String.format("\nIf the target view is not part of the view hierarchy, you "
        + "may need to use Espresso.onData to load it from one of the following AdapterViews:%s"
        , Joiner.on("\n- ").join(adapterViews));
      throw new NoMatchingViewException.Builder()
          .withViewMatcher(viewMatcher)
          .withRootView(root)
          .withAdapterViews(adapterViews)
          .withAdapterViewWarning(Optional.of(warning))
          .build();
    } else {
      return matchedView;
    }
  }

首先使用 inRoot() 設置的條件rootViewProvider獲取需要的根佈局,如果沒有使用inRoot() ,會獲取默認根佈局。
View root = rootViewProvider.get();
將根佈局使用breadthFirstViewTraversal()打算成單個的view
如圖的佈局,打散後爲 Root, A, R, U, B, D, G, N
view
ViewsMatcher 匹配後的匹配結果,會存儲在matchedViewIterator
Iterator<View> matchedViewIterator = Iterables.filter(breadthFirstViewTraversal(root),matcherPredicate).iterator();
正常情況下的匹配結果matchedViewIterator 的 size 應該爲 1,這樣才符合onView()匹配結果有且僅有一個的特點,否則拋出有多個匹配結果的異常AmbiguousViewMatcherException:

  • Multiple Ambiguous Views found for matcher
  • matches multiple views in the hierarchy

如果沒有任何匹配結果,則會拋出異常NoMatchingViewException

  • No views in hierarchy found matching

並判斷是否可能由於錯用onData()&onView導致(也就是當前佈局是否存在AdapterView),額外拋出提示:

  • If the target view is not part of the view hierarchy, you may need to use Espresso.onData to load it from one of the following AdapterViews

如果以上異常全沒有出現,那麼恭喜了,我們期盼了許久的唯一的 view 可以從matchedViewIterator中取出進行操作了。


由於在perform()中使用 for() 循環,依次執行每一個 ViewAction ,且 view 是在每次執行中單獨匹配,所以如果你在perform()中執行多個操作,請注意一下每個操作都是完全獨立的,不要寫出下面這種代碼,不要問我爲什麼知道的 T-T

onView(withText("string1"))
                .perform(replaceText("string2"),closeSoftKeyboard());

這裏onView()的 Matcher 爲withText("string1")也就是帶有 text 爲 “string1” 的控件,然後replaceText("string2")將控件中的 “string1” 修改爲 “string2”。一直到這裏都沒有問題,但是當開始在perform()中執行closeSoftKeyboard())時電腦就一臉懵逼了,你他喵的讓我去找 “string1” (這裏還是執行onView()中的 matcher),但是你開始前給我換成 “string2” 是幾個意思。就像你拿着萌妹的照片去見網友,到了地方發現只有一羣壯漢一樣,節哀。

2.check()

看完perform()之後再看check()就會感覺異常的簡單:

  public ViewInteraction check(final ViewAssertion viewAssert) {
    checkNotNull(viewAssert);
    runSynchronouslyOnUiThread(new Runnable() {
      @Override
      public void run() {
        uiController.loopMainThreadUntilIdle();

        View targetView = null;
        NoMatchingViewException missingViewException = null;
        try {
          targetView = viewFinder.getView();
        } catch (NoMatchingViewException nsve) {
          missingViewException = nsve;
        }
        viewAssert.check(targetView, missingViewException);
      }
    });
    return this;
  }

還是檢查開始checkNotNull(viewAssert);然後在runSynchronouslyOnUiThread中開始執行操作。接下來的代碼就非常乾脆了直接targetView = viewFinder.getView();,找到就開始執行check(),找不到就拋出NoMatchingViewException

不知道是否注意到了無論check()還是perform()都會在runSynchronouslyOnUiThread的開始執行代碼uiController.loopMainThreadUntilIdle();,這裏就是等待主線程空閒的操作,這裏就不深入去看了。

3.inRoot()

現在來看一下輔助函數inRoot(),這個函數非常簡單:

  /**
   * Makes this ViewInteraction scoped to the root selected by the given root matcher.
   */
  public ViewInteraction inRoot(Matcher<Root> rootMatcher) {
    this.rootMatcherRef.set(checkNotNull(rootMatcher));
    return this;
  }

將當前的rootMatcher存儲到AtomicReference<Matcher<Root>> rootMatcherRef中(注:使用原子性對象引用,在多線程情況下進行對象的更新可以確保一致性,暫時理解爲一種存儲形式吧)
這個函數簡單的我一臉的黑人問號,不過沒關係,我們繼續挖。
首先我們看一下rootMatcherRef在哪裏用過,在 Espresso類中找是不現實了,因爲這裏只有定義,根本沒有任何使用的跡象。不過在我們尋找 view 的時候ewFinder.getView();的最開始有一段用來確認 root 的代碼:

View root = rootViewProvider.get();

我們打開 RootViewProvider 類,走!看代碼!

private final AtomicReference<Matcher<Root>> rootMatcherRef;

在 RootViewProvider 中有 rootMatcherRef 同樣的原子性引用,這裏是不是和我們想要看的 inRoot() 有關係呢。關係到引用是不是同一個引用的問題,我們就只能到最開始的構造去找了。
再次打開onView(),跟蹤到new ViewInteractionModule(viewMatcher),看一下他的成員函數

private final AtomicReference<Matcher<Root>> rootMatcher =

      new AtomicReference<Matcher<Root>>(RootMatchers.DEFAULT);

所有的源頭找到了,然後這裏構造的AtomicReferenceDaggerBaseLayerComponent 類的內部類 ViewInteractionComponentImpl 類 中初始化到 ViewInteraction 的各個成員中。
看到這裏inRoot()的作用和運行過程,相信你應該有個大概的印象了,他正式產生作用就是在我們最開始看到的

View root = rootViewProvider.get();

在這裏使用和查找 view 類似的方式獲取了當前視圖的所有 root 並篩選出有且僅有一個的結果:

FindRootResult findResult = findRoot(rootMatcher);

然後確認當前的 root is ready,並返回root對應的decorView,用於參與後續操作(提取佈局內所有view)

//檢查當前root狀態是否可用
isReady(findResult.needle)
//返回root對應的decorView
findResult.needle.getDecorView()

4.withFailureHandler()

設置對於拋出異常的處理方式,處理方式需要實現接口 FailureHandler,裏面只有一個函數handle()用來完成異常處理。
這裏看一下默認的異常處理方式

public void handle(Throwable error, Matcher<View> viewMatcher) {
    if (error instanceof EspressoException || error instanceof AssertionFailedError
        || error instanceof AssertionError) {
      throw propagate(getUserFriendlyError(error, viewMatcher));
    } else {
      throw propagate(error);
    }
  }

函數對Espresso的異常常規異常分別處理打印日誌,但是在真正使用中這樣的日誌效果並不是非常的好,所以可以根據情況自定義 FailureHandler,增加更詳細的日誌,或者增加截屏功能等。

DataInteraction

DataInteraction的公共函數有七個,除去perform()check()之外,有五個輔助函數:

函數名 功能
atPosition() 選中匹配的Adapter的第幾項(出現重複匹配時使用)
inAdapterView 選擇滿足adapterMatcher的adapterview來執行onData操作
inRoot 鎖定指定的root
onChildView 匹配”onData所匹配的item視圖”中的指定子視圖
usingAdapterViewProtocol 對於不符合常規的AdapterView自定義協議

下面依次來看一下

1.perform()

本來感覺和onData()onView()時完全並列的兩條主線,直到看了onData()的代碼,發現,原來他是個弟弟:

  public ViewInteraction perform(ViewAction... actions) {

       AdapterDataLoaderAction adapterDataLoaderAction = load();

       return onView(makeTargetMatcher(adapterDataLoaderAction))
            .inRoot(rootMatcher)
            .perform(actions);

  }

這裏把他整體分爲兩個部分吧第一部分load()先略過去,我們先看第二部分,直接將onView搬過來了。inRoot()perform()在上文說的夠多了,這裏主要看makeTargetMatcher ()

private Matcher<View> makeTargetMatcher(AdapterDataLoaderAction adapterDataLoaderAction) 
{
        Matcher<View> targetView = displayingData(adapterMatcher, dataMatcher, 
adapterViewProtocol,
        adapterDataLoaderAction);
    if (childViewMatcher.isPresent()) {
        targetView = allOf(childViewMatcher.get(), isDescendantOfA(targetView));
    }
    return targetView;
  }

這裏沒有太特別的操作,只是區分了一下onChildView()帶來的影響。具體在onChildView()中會進行分析。
我們看一下生成匹配器的函數displayingData(),內部和常規的 TypeSafeMatcher沒有什麼區別,這裏我們主要看matchesSafely()函數:
()

public boolean matchesSafely(View view) {
        ViewParent parent = view.getParent();
        while (parent != null && !(parent instanceof AdapterView)) {
          parent = parent.getParent();
        }
        if (parent != null && adapterMatcher.matches(parent)) {
          Optional<AdaptedData> data = adapterViewProtocol.getDataRenderedByView(
              (AdapterView<? extends Adapter>) parent, view);
          if (data.isPresent()) {
            return adapterDataLoaderAction.getAdaptedData().opaqueToken.equals(
                data.get().opaqueToken);
          }
        }
        return false;
      }

開始檢測view 的ViewParent 是否爲 AdapterView ,如果不是直接擡走下一位,如果是則通過getDataRenderedByView()獲取當前view對應的 data ,並將它與 objectMatcher 匹配的結果進行比較(objectMatcher在 load()中進行)。
可能你還沒有了解自定義matcher ,這裏解釋一下 ,形參view就是上文提到的getView()函數中breadthFirstViewTraversal(root)的結果(排序後的視圖內所有view),依次輸入匹配。
看到這裏perform()的工作流程就可以明白一二了:因爲onView()是用來定位視圖內的控件,所以第一步肯定是將目標視圖移動到視圖內,然後才能進行第二步的view選擇和操作。
現在看一下第一步中,是如何進行view的移動的。
AdapterDataLoaderAction adapterDataLoaderAction = load();
現在看一下load()函數:

  private AdapterDataLoaderAction load() {
    AdapterDataLoaderAction adapterDataLoaderAction =
       new AdapterDataLoaderAction(dataMatcher, atPosition, adapterViewProtocol);
    onView(adapterMatcher)
      .inRoot(rootMatcher)
      .perform(adapterDataLoaderAction);
    return adapterDataLoaderAction;
  }

這裏用來將 目標view 移動到視圖內用的還是onView(),注意看,這裏匹配的控件是 AdapterView,因爲AdapterView 一定是在視圖範圍內的,而且,view 是 AdapterView的子視圖,所以這裏用onView()對AdapterView進行操作沒有問題。
我們看一下這裏進行了什麼操作。由於AdapterDataLoaderAction implements ViewAction所以這裏我們直接看perform()函數:

  public void perform(UiController uiController, View view) {
  //第一部分
    AdapterView<? extends Adapter> adapterView = (AdapterView<? extends Adapter>) view;
    List<AdapterViewProtocol.AdaptedData> matchedDataItems = Lists.newArrayList();
    for (AdapterViewProtocol.AdaptedData data : adapterViewProtocol.getDataInAdapterView(
        adapterView)) {
      if (dataToLoadMatcher.matches(data.getData())) {
        matchedDataItems.add(data);
      }
    }
    //第二部分
    if (matchedDataItems.size() == 0) {
      StringDescription dataMatcherDescription = new StringDescription();
      dataToLoadMatcher.describeTo(dataMatcherDescription);
      if (matchedDataItems.isEmpty()) {
        dataMatcherDescription.appendText(" contained values: ");
          dataMatcherDescription.appendValue(
              adapterViewProtocol.getDataInAdapterView(adapterView));
        throw new PerformException.Builder()
          .withActionDescription(this.getDescription())
          .withViewDescription(HumanReadables.describe(view))
          .withCause(new RuntimeException("No data found matching: " + dataMatcherDescription))
          .build();
      }
    }
    //第三部分
    synchronized (dataLock) {
      checkState(!performed, "perform called 2x!");
      performed = true;
      if (atPosition.isPresent()) {
        int matchedDataItemsSize = matchedDataItems.size() - 1;
        if (atPosition.get() > matchedDataItemsSize) {
          throw new PerformException.Builder()
            .withActionDescription(this.getDescription())
            .withViewDescription(HumanReadables.describe(view))
            .withCause(new RuntimeException(String.format(
                "There are only %d elements that matched but requested %d element.",
                matchedDataItemsSize, atPosition.get())))
            .build();
        } else {
          adaptedData = matchedDataItems.get(atPosition.get());
        }
      } else {
        if (matchedDataItems.size() != 1) {
          StringDescription dataMatcherDescription = new StringDescription();
          dataToLoadMatcher.describeTo(dataMatcherDescription);
          throw new PerformException.Builder()
            .withActionDescription(this.getDescription())
            .withViewDescription(HumanReadables.describe(view))
            .withCause(new RuntimeException("Multiple data elements " +
                "matched: " + dataMatcherDescription + ". Elements: " + matchedDataItems))
            .build();
        } else {
          adaptedData = matchedDataItems.get(0);
        }
      }
    }
    //第四部分
    int requestCount = 0;
    while (!adapterViewProtocol.isDataRenderedWithinAdapterView(adapterView, adaptedData)) {
      if (requestCount > 1) {
        if ((requestCount % 50) == 0) {
          // sometimes an adapter view will receive an event that will block its attempts to scroll.
          adapterView.invalidate();
          adapterViewProtocol.makeDataRenderedWithinAdapterView(adapterView, adaptedData);
        }
      } else {
        adapterViewProtocol.makeDataRenderedWithinAdapterView(adapterView, adaptedData);
      }
      uiController.loopMainThreadForAtLeast(100);
      requestCount++;
    }
  }

由於代碼比較比較長,這裏分爲幾部分來看:
第一部分:view 強制轉換爲 AdapterView ,取出 data 與dataToLoadMatcher()進行匹配,將所有匹配成功的結果 存儲到 matchedDataItems中。
第二部分:如果matchedDataItems爲空,及沒有任何匹配數據,則拋出異常。
第三部分:這裏會根據是否使用了atPosition()產生區別。如果使用了則會返回matchedDataItems.get(atPosition.get())類似於 List().get(atPosition),和常規使用List一樣,這裏會判斷是否“指針超限”。如果沒有使用,就需要看matchedDataItems.size()如果正好爲 0 ,可以直接返回結果,否則就會拋出Multiple data elements的異常。
第四部分:這裏就是將 目標view 滑動到視圖內的操作。這裏有註釋

// sometimes an adapter view will receive an event that will block its attempts to scroll.

這裏不會無限制的進行嘗試操作,如果超過限制會放棄本次操作。當然這不是我們想看到的,我們繼續看代碼makeDataRenderedWithinAdapterView()(這裏就只貼關鍵代碼了)

//第一部分
((AbsListView) adapterView).smoothScrollToPositionFromTop(position,
                adapterView.getPaddingTop(), 0);
                
    ......
                
 //第二部分  
if (adapterView instanceof AdapterViewAnimator) {
    if (adapterView instanceof AdapterViewFlipper) {
        ((AdapterViewFlipper) adapterView).stopFlipping();
    }
        ((AdapterViewAnimator) adapterView).setDisplayedChild(position);
        moved = true;
}

第一部分就是滑動到指定位置的主體函數,第二部分是關於動畫處理的操作,這裏就不介紹了(筆者還沒有動畫相關內容)。現在着重看第一部分。
跟蹤進去,主要代碼只有一句:

 AbsPositionScroller.startWithOffset();

繼續跟蹤到 AbsListView().AbsPositionScroller 類,這裏已經是ListView,就不貼代碼了,在這裏說一下流程:
首先在startWithOffset()中會做一些判定和預處理,並計算需要滑動的參數,然後postOnAnimation(this)(因爲AbsPositionScroller implement Runnable)開始運行run()在這裏進行不同情況的判斷正式開始滑動操作。
到此perform()介紹完畢,說了這麼多總結起來就兩部:1、將 目標View 移動到視圖中;2、調用 onView

2.check()

  public ViewInteraction check(ViewAssertion assertion) {
     AdapterDataLoaderAction adapterDataLoaderAction = load();
     return onView(makeTargetMatcher(adapterDataLoaderAction))
        .inRoot(rootMatcher)
        .check(assertion);
  }

直接貼出代碼,相信你一定馬上明白了,和perform整體操作完全一樣,這裏就不多加介紹了。

3.inRoot()

inRoot() 就是直接調用 ViewInteraction 看一下上面 check()中的 return 就明白了。

4.inAdapterView()

看一下它的使用吧,首先是在 上面提到的load()perform()&check()調用的第一個函數)中

    onView(adapterMatcher)
      .inRoot(rootMatcher)
      .perform(adapterDataLoaderAction);

直接放在onView()中用來匹配 adapterView ,再就是 displayingData()中用來作爲匹配 view 的保障。(詳細見上文perform())

public boolean matchesSafely(View view) {

    ......
    
    if (parent != null && adapterMatcher.matches(parent)) {
             Optional<AdaptedData> data =  adapterViewProtocol.getDataRenderedByView(
                 (AdapterView<? extends Adapter>) parent, view);
             if (data.isPresent()) {
                    return adapterDataLoaderAction.getAdaptedData().opaqueToken.equals(
                              data.get().opaqueToken);
          }
    }
}

4.atPosition()

AdapterDataLoaderAction.perform()的第三部分中(詳見上文):

adaptedData = matchedDataItems.get(atPosition.get());

如果使用dataMatcher匹配的結果多於一個,則需要atPosition來進行甄別,確定唯一結果,否則報錯。(可以在onData()中不篩選任何data,添加空dataMatcher,直接用atPosition(),當然,這種方法不推薦)

5.usingAdapterViewProtocol()

本函數中涉及的AdapterViewProtocol是整個onData的中心,本文中使用的makeDataRenderedWithinAdapterView()
AdapterDataLoaderAction.perform()
等函數都直接或者間接的使用了默認的AdapterViewProtocol,它是Espresso訪問AdapterView的中間橋樑。
常規情況下,默認的AdapterViewProtocol可以滿足使用情況,不需要使用,如果您想了解推薦你看一下Espresso的進階: AdapterViewProtocol,大神寫的非常詳細。

總結

本文是筆者按照自己看代碼的思路寫的,整體可能會比較繁瑣,也會有思考不到位的地方,請見諒,這裏是對於上文提到的一些關鍵點的總結:

  1. onData()&onView只是起到初始化的作用,真正的定位view操作實際在perform()&check中執行。
  2. 不要把perform()&check定位view想的太複雜,就是將所有view排序後一個一個進行匹配嘗試。所以針對線型佈局中這種順序定死的view,也可以自定義matcher排號選擇第幾個。
  3. 每個perform()&check()都是單獨進行 定位view 的操作,後續操作必須考慮上一個操作帶來的影響。
  4. 同一個perform()執行多個操作,和連續使用多個perform()的效果是相同的,必須注意操作的前後影響
  5. 使用withFailureHandler()自定義新的FailureHandler,根據需求增加log屏幕截圖,單純的原始log無法滿足需求。
  6. 對於 AdapterView 推薦使用 onData() ,哪怕是視圖可見的 view 。不然當AdapterView的數據源發生變化時,你就可以去哭了。
  7. onData() 是爲了適應AdapterView 而封裝後的 onView()
函數名 功能
atPosition() 選中匹配的Adapter的第幾項(出現重複匹配時使用)
inAdapterView 選擇滿足adapterMatcher的adapterview來執行onData操作
inRoot 鎖定指定的root
onChildView 匹配”onData所匹配的item視圖”中的指定子視圖
usingAdapterViewProtocol 對於不符合常規的AdapterView自定義協議

最後,不要盲目的使用錄製軟件,錄製後的代碼需要檢查一遍有沒有“坑”。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章