blob: 2b382f8b999faceb909ce420b57127508906a24e [file] [log] [blame]
Aurimas Liutikasac5fe7c2018-03-06 14:40:53 -08001/*
2 * Copyright (C) 2017 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
17package androidx.wear.widget.drawer;
18
Aurimas Liutikasb7eeda42018-07-10 11:57:16 -070019import static androidx.test.espresso.Espresso.onView;
20import static androidx.test.espresso.action.ViewActions.click;
21import static androidx.test.espresso.action.ViewActions.swipeDown;
22import static androidx.test.espresso.assertion.ViewAssertions.matches;
23import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
24import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
25import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
26import static androidx.test.espresso.matcher.ViewMatchers.withId;
27import static androidx.test.espresso.matcher.ViewMatchers.withParent;
28import static androidx.test.espresso.matcher.ViewMatchers.withText;
Aurimas Liutikasac5fe7c2018-03-06 14:40:53 -080029import static androidx.wear.widget.util.AsyncViewActions.waitForMatchingView;
30
31import static org.hamcrest.Matchers.allOf;
32import static org.hamcrest.Matchers.is;
33import static org.hamcrest.Matchers.not;
34import static org.junit.Assert.assertNotNull;
35import static org.junit.Assert.assertTrue;
36import static org.mockito.Matchers.any;
37import static org.mockito.Mockito.mock;
38import static org.mockito.Mockito.verify;
39
40import android.content.Intent;
Aurimas Liutikasac5fe7c2018-03-06 14:40:53 -080041import android.view.Menu;
42import android.view.MenuItem;
43import android.view.MenuItem.OnMenuItemClickListener;
44import android.view.View;
45import android.widget.ImageView;
46import android.widget.TextView;
47
Aurimas Liutikasd75a46682018-03-13 13:55:58 -070048import androidx.recyclerview.widget.RecyclerView;
Aurimas Liutikasb7eeda42018-07-10 11:57:16 -070049import androidx.test.espresso.PerformException;
50import androidx.test.espresso.UiController;
51import androidx.test.espresso.ViewAction;
52import androidx.test.espresso.util.HumanReadables;
53import androidx.test.espresso.util.TreeIterables;
54import androidx.test.filters.LargeTest;
55import androidx.test.rule.ActivityTestRule;
56import androidx.test.runner.AndroidJUnit4;
Aurimas Liutikasd75a46682018-03-13 13:55:58 -070057import androidx.wear.test.R;
58import androidx.wear.widget.drawer.DrawerTestActivity.DrawerStyle;
59
Aurimas Liutikasac5fe7c2018-03-06 14:40:53 -080060import org.hamcrest.Description;
61import org.hamcrest.Matcher;
62import org.hamcrest.TypeSafeMatcher;
63import org.junit.Before;
64import org.junit.Rule;
65import org.junit.Test;
66import org.junit.runner.RunWith;
67import org.mockito.Mock;
68import org.mockito.MockitoAnnotations;
69
70import java.util.concurrent.TimeoutException;
71
72import javax.annotation.Nullable;
73
74/**
75 * Espresso tests for {@link WearableDrawerLayout}.
76 */
77@LargeTest
78@RunWith(AndroidJUnit4.class)
79public class WearableDrawerLayoutEspressoTest {
80
81 private static final long MAX_WAIT_MS = 4000;
82
83 @Rule public final ActivityTestRule<DrawerTestActivity> activityRule =
84 new ActivityTestRule<>(
85 DrawerTestActivity.class, true /* touchMode */, false /* initialLaunch*/);
86
87 private final Intent mSinglePageIntent =
88 new DrawerTestActivity.Builder().setStyle(DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE)
89 .build();
90 @Mock WearableNavigationDrawerView.OnItemSelectedListener mNavDrawerItemSelectedListener;
91
92 @Before
93 public void setUp() {
94 MockitoAnnotations.initMocks(this);
95 }
96
97 @Test
98 public void openingNavigationDrawerDoesNotCloseActionDrawer() {
99 // GIVEN a drawer layout with a peeking action and navigation drawer
100 activityRule.launchActivity(mSinglePageIntent);
101 DrawerTestActivity activity = activityRule.getActivity();
102 WearableDrawerView actionDrawer =
103 (WearableDrawerView) activity.findViewById(R.id.action_drawer);
104 WearableDrawerView navigationDrawer =
105 (WearableDrawerView) activity.findViewById(R.id.navigation_drawer);
106 assertTrue(actionDrawer.isPeeking());
107 assertTrue(navigationDrawer.isPeeking());
108
109 // WHEN the top drawer is opened
110 openDrawer(navigationDrawer);
111 onView(withId(R.id.navigation_drawer))
112 .perform(
113 waitForMatchingView(
114 allOf(withId(R.id.navigation_drawer), isOpened(true)),
115 MAX_WAIT_MS));
116
117 // THEN the action drawer should still be peeking
118 assertTrue(actionDrawer.isPeeking());
119 }
120
121 @Test
122 public void swipingDownNavigationDrawerDoesNotCloseActionDrawer() {
123 // GIVEN a drawer layout with a peeking action and navigation drawer
124 activityRule.launchActivity(mSinglePageIntent);
125 onView(withId(R.id.action_drawer)).check(matches(isPeeking()));
126 onView(withId(R.id.navigation_drawer)).check(matches(isPeeking()));
127
128 // WHEN the top drawer is opened by swiping down
129 onView(withId(R.id.drawer_layout)).perform(swipeDown());
130 onView(withId(R.id.navigation_drawer))
131 .perform(
132 waitForMatchingView(
133 allOf(withId(R.id.navigation_drawer), isOpened(true)),
134 MAX_WAIT_MS));
135
136 // THEN the action drawer should still be peeking
137 onView(withId(R.id.action_drawer)).check(matches(isPeeking()));
138 }
139
140
141 @Test
142 public void firstNavDrawerItemShouldBeSelectedInitially() {
143 // GIVEN a top drawer
144 // WHEN it is first opened
145 activityRule.launchActivity(mSinglePageIntent);
146 onView(withId(R.id.drawer_layout)).perform(swipeDown());
147 onView(withId(R.id.navigation_drawer))
148 .perform(
149 waitForMatchingView(
150 allOf(withId(R.id.navigation_drawer), isOpened(true)),
151 MAX_WAIT_MS));
152
153 // THEN the text should display "0".
154 onView(withId(R.id.ws_nav_drawer_text)).check(matches(withText("0")));
155 }
156
157 @Test
158 public void selectingNavItemChangesTextAndClosedDrawer() {
159 // GIVEN an open top drawer
160 activityRule.launchActivity(mSinglePageIntent);
161 onView(withId(R.id.drawer_layout)).perform(swipeDown());
162 onView(withId(R.id.navigation_drawer))
163 .perform(
164 waitForMatchingView(
165 allOf(withId(R.id.navigation_drawer), isOpened(true)),
166 MAX_WAIT_MS));
167
168 // WHEN the second item is selected
169 onView(withId(R.id.ws_nav_drawer_icon_1)).perform(click());
170
171 // THEN the text should display "1" and it should close.
172 onView(withId(R.id.ws_nav_drawer_text))
173 .perform(
174 waitForMatchingView(
175 allOf(withId(R.id.ws_nav_drawer_text), withText("1")),
176 MAX_WAIT_MS));
177 onView(withId(R.id.navigation_drawer))
178 .perform(
179 waitForMatchingView(
180 allOf(withId(R.id.navigation_drawer), isClosed(true)),
181 MAX_WAIT_MS));
182 }
183
184 @Test
185 public void programmaticallySelectingNavItemChangesTextInSinglePage() {
186 // GIVEN an open top drawer
187 activityRule.launchActivity(new DrawerTestActivity.Builder()
188 .setStyle(DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE)
189 .openTopDrawerInOnCreate()
190 .build());
191 final WearableNavigationDrawerView navDrawer =
192 activityRule.getActivity().findViewById(R.id.navigation_drawer);
193 navDrawer.addOnItemSelectedListener(mNavDrawerItemSelectedListener);
194
195 // WHEN the second item is selected programmatically
196 selectNavItem(navDrawer, 1);
197
198 // THEN the text should display "1" and the listener should be notified.
199 onView(withId(R.id.ws_nav_drawer_text))
200 .check(matches(withText("1")));
201 verify(mNavDrawerItemSelectedListener).onItemSelected(1);
202 }
203
204 @Test
205 public void programmaticallySelectingNavItemChangesTextInMultiPage() {
206 // GIVEN an open top drawer
207 activityRule.launchActivity(new DrawerTestActivity.Builder()
208 .setStyle(DrawerStyle.BOTH_DRAWER_NAV_MULTI_PAGE)
209 .openTopDrawerInOnCreate()
210 .build());
211 final WearableNavigationDrawerView navDrawer =
212 activityRule.getActivity().findViewById(R.id.navigation_drawer);
213 navDrawer.addOnItemSelectedListener(mNavDrawerItemSelectedListener);
214
215 // WHEN the second item is selected programmatically
216 selectNavItem(navDrawer, 1);
217
218 // THEN the text should display "1" and the listener should be notified.
219 onView(allOf(withId(R.id.ws_navigation_drawer_item_text), isDisplayed()))
220 .check(matches(withText("1")));
221 verify(mNavDrawerItemSelectedListener).onItemSelected(1);
222 }
223
224 @Test
225 public void navDrawerShouldOpenWhenCalledInOnCreate() {
226 // GIVEN an activity which calls openDrawer(Gravity.TOP) in onCreate
227 // WHEN it is launched
228 activityRule.launchActivity(
229 new DrawerTestActivity.Builder()
230 .setStyle(DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE)
231 .openTopDrawerInOnCreate()
232 .build());
233
234 // THEN the nav drawer should be open
235 onView(withId(R.id.navigation_drawer)).check(matches(isOpened(true)));
236 }
237
238 @Test
239 public void actionDrawerShouldOpenWhenCalledInOnCreate() {
240 // GIVEN an activity with only an action drawer which is opened in onCreate
241 // WHEN it is launched
242 activityRule.launchActivity(
243 new DrawerTestActivity.Builder()
244 .setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE)
245 .openBottomDrawerInOnCreate()
246 .build());
247
248 // THEN the action drawer should be open
249 onView(withId(R.id.action_drawer)).check(matches(isOpened(true)));
250 }
251
252 @Test
253 public void navDrawerShouldOpenWhenCalledInOnCreateAndThenCloseWhenRequested() {
254 // GIVEN an activity which calls openDrawer(Gravity.TOP) in onCreate, then closes it
255 // WHEN it is launched
256 activityRule.launchActivity(
257 new DrawerTestActivity.Builder()
258 .setStyle(DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE)
259 .openTopDrawerInOnCreate()
260 .closeFirstDrawerOpened()
261 .build());
262
263 // THEN the nav drawer should be open and then close
264 onView(withId(R.id.navigation_drawer))
265 .check(matches(isOpened(true)))
266 .perform(
267 waitForMatchingView(
268 allOf(withId(R.id.navigation_drawer), isClosed(true)),
269 MAX_WAIT_MS));
270 }
271
272 @Test
273 public void openedNavDrawerShouldPreventSwipeToClose() {
274 // GIVEN an activity which calls openDrawer(Gravity.TOP) in onCreate
275 activityRule.launchActivity(
276 new DrawerTestActivity.Builder()
277 .setStyle(DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE)
278 .openTopDrawerInOnCreate()
279 .build());
280
281 // THEN the view should prevent swipe to close
282 onView(withId(R.id.navigation_drawer)).check(matches(not(allowsSwipeToClose())));
283 }
284
285 @Test
286 public void closedNavDrawerShouldNotPreventSwipeToClose() {
287 // GIVEN an activity which doesn't start with the nav drawer open
288 activityRule.launchActivity(mSinglePageIntent);
289
290 // THEN the view should allow swipe to close
291 onView(withId(R.id.navigation_drawer)).check(matches(allowsSwipeToClose()));
292 }
293
294 @Test
295 public void scrolledDownActionDrawerCanScrollUpWhenReOpened() {
296 // GIVEN a freshly launched activity
297 activityRule.launchActivity(mSinglePageIntent);
298 WearableActionDrawerView actionDrawer =
299 (WearableActionDrawerView) activityRule.getActivity()
300 .findViewById(R.id.action_drawer);
301 RecyclerView recyclerView = (RecyclerView) actionDrawer.getDrawerContent();
302
303 // WHEN the action drawer is opened and scrolled to the last item (Item 6)
304 openDrawer(actionDrawer);
305 scrollToPosition(recyclerView, 5);
306 onView(withId(R.id.action_drawer))
307 .perform(
308 waitForMatchingView(allOf(withId(R.id.action_drawer), isOpened(true)),
309 MAX_WAIT_MS))
310 .perform(
311 waitForMatchingView(allOf(withText("Item 6"), isCompletelyDisplayed()),
312 MAX_WAIT_MS));
313 // and then it is peeked
314 peekDrawer(actionDrawer);
315 onView(withId(R.id.action_drawer))
316 .perform(waitForMatchingView(allOf(withId(R.id.action_drawer), isPeeking()),
317 MAX_WAIT_MS));
318 // and re-opened
319 openDrawer(actionDrawer);
320 onView(withId(R.id.action_drawer))
321 .perform(
322 waitForMatchingView(allOf(withId(R.id.action_drawer), isOpened(true)),
323 MAX_WAIT_MS));
324
325 // THEN item 6 should be visible, but swiping down should scroll up, not close the drawer.
326 onView(withText("Item 6")).check(matches(isDisplayed()));
327 onView(withId(R.id.action_drawer)).perform(swipeDown()).check(matches(isOpened(true)));
328 }
329
330 @Test
331 public void actionDrawerPeekIconShouldNotBeNull() {
332 // GIVEN a drawer layout with a peeking action drawer whose menu is initialized in XML
333 activityRule.launchActivity(mSinglePageIntent);
334 DrawerTestActivity activity = activityRule.getActivity();
335 ImageView peekIconView =
336 (ImageView) activity
337 .findViewById(R.id.ws_action_drawer_peek_action_icon);
338 // THEN its peek icon should not be null
339 assertNotNull(peekIconView.getDrawable());
340 }
341
342 @Test
343 public void tappingActionDrawerPeekIconShouldTriggerFirstAction() {
344 // GIVEN a drawer layout with a peeking action drawer, title, and mock click listener
345 activityRule.launchActivity(
346 new DrawerTestActivity.Builder()
347 .setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE)
348 .build());
349 WearableActionDrawerView actionDrawer =
350 (WearableActionDrawerView) activityRule.getActivity()
351 .findViewById(R.id.action_drawer);
352 OnMenuItemClickListener mockClickListener = mock(OnMenuItemClickListener.class);
353 actionDrawer.setOnMenuItemClickListener(mockClickListener);
354 // WHEN the action drawer peek view is tapped
355 onView(withId(R.id.ws_drawer_view_peek_container))
356 .perform(waitForMatchingView(
357 allOf(
358 withId(R.id.ws_drawer_view_peek_container),
359 isCompletelyDisplayed()),
360 MAX_WAIT_MS))
361 .perform(click());
362 // THEN its click listener should be notified
363 verify(mockClickListener).onMenuItemClick(any(MenuItem.class));
364 }
365
366 @Test
367 public void tappingActionDrawerPeekIconShouldTriggerFirstActionAfterItWasOpened() {
368 // GIVEN a drawer layout with an open action drawer with a title, and mock click listener
369 activityRule.launchActivity(
370 new DrawerTestActivity.Builder()
371 .setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE)
372 .openBottomDrawerInOnCreate()
373 .build());
374 WearableActionDrawerView actionDrawer =
375 (WearableActionDrawerView) activityRule.getActivity()
376 .findViewById(R.id.action_drawer);
377 OnMenuItemClickListener mockClickListener = mock(OnMenuItemClickListener.class);
378 actionDrawer.setOnMenuItemClickListener(mockClickListener);
379
380 // WHEN the action drawer is closed to its peek state and then tapped
381 peekDrawer(actionDrawer);
382 onView(withId(R.id.action_drawer))
383 .perform(waitForMatchingView(allOf(withId(R.id.action_drawer), isPeeking()),
384 MAX_WAIT_MS));
385 actionDrawer.getPeekContainer().callOnClick();
386
387 // THEN its click listener should be notified
388 verify(mockClickListener).onMenuItemClick(any(MenuItem.class));
389 }
390
391 @Test
392 public void changingActionDrawerItemShouldUpdateView() {
393 // GIVEN a drawer layout with an open action drawer
394 activityRule.launchActivity(
395 new DrawerTestActivity.Builder()
396 .setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE)
397 .openBottomDrawerInOnCreate()
398 .build());
399 WearableActionDrawerView actionDrawer =
400 activityRule.getActivity().findViewById(R.id.action_drawer);
401 final MenuItem secondItem = actionDrawer.getMenu().getItem(1);
402
403 // WHEN its second item is changed
404 actionDrawer.post(new Runnable() {
405 @Override
406 public void run() {
407 secondItem.setTitle("Modified item");
408 }
409 });
410
411 // THEN the new item should be displayed
412 onView(withText("Modified item")).check(matches(isDisplayed()));
413 }
414
415 @Test
416 public void removingActionDrawerItemShouldUpdateView() {
417 // GIVEN a drawer layout with an open action drawer
418 activityRule.launchActivity(
419 new DrawerTestActivity.Builder()
420 .setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE)
421 .openBottomDrawerInOnCreate()
422 .build());
423 final WearableActionDrawerView actionDrawer =
424 activityRule.getActivity().findViewById(R.id.action_drawer);
425 MenuItem secondItem = actionDrawer.getMenu().getItem(1);
426 final int itemId = secondItem.getItemId();
427 final String title = secondItem.getTitle().toString();
428 final int initialSize = getChildByType(actionDrawer, RecyclerView.class)
429 .getAdapter()
430 .getItemCount();
431
432 // WHEN its second item is removed
433 actionDrawer.post(new Runnable() {
434 @Override
435 public void run() {
436 actionDrawer.getMenu().removeItem(itemId);
437 }
438 });
439
440 // THEN it should decrease by 1 in size and it should no longer contain the item's text
441 onView(allOf(withParent(withId(R.id.action_drawer)), isAssignableFrom(RecyclerView.class)))
442 .perform(waitForRecyclerToBeSize(initialSize - 1, MAX_WAIT_MS))
443 .perform(waitForMatchingView(recyclerWithoutText(is(title)), MAX_WAIT_MS));
444 }
445
446 @Test
447 public void addingActionDrawerItemShouldUpdateView() {
448 // GIVEN a drawer layout with an open action drawer
449 activityRule.launchActivity(
450 new DrawerTestActivity.Builder()
451 .setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE)
452 .openBottomDrawerInOnCreate()
453 .build());
454 final WearableActionDrawerView actionDrawer =
455 activityRule.getActivity().findViewById(R.id.action_drawer);
456
457 RecyclerView recycler = getChildByType(actionDrawer, RecyclerView.class);
458 final RecyclerView.LayoutManager layoutManager = recycler.getLayoutManager();
459 final int initialSize = recycler.getAdapter().getItemCount();
460
461 // WHEN an item is added and the view is scrolled down (to make sure the view is created)
462 actionDrawer.post(new Runnable() {
463 @Override
464 public void run() {
465 actionDrawer.getMenu().add(0, 42, Menu.NONE, "New Item");
466 layoutManager.scrollToPosition(initialSize);
467 }
468 });
469
470 // THEN it should decrease by 1 in size and the there should be a view with the item's text
471 onView(allOf(withParent(withId(R.id.action_drawer)), isAssignableFrom(RecyclerView.class)))
472 .perform(waitForRecyclerToBeSize(initialSize + 1, MAX_WAIT_MS))
473 .perform(waitForMatchingView(withText("New Item"), MAX_WAIT_MS));
474 }
475
476 private void scrollToPosition(final RecyclerView recyclerView, final int position) {
477 recyclerView.post(new Runnable() {
478 @Override
479 public void run() {
480 recyclerView.scrollToPosition(position);
481 }
482 });
483 }
484
485 private void selectNavItem(final WearableNavigationDrawerView navDrawer, final int index) {
486 navDrawer.post(new Runnable() {
487 @Override
488 public void run() {
489 navDrawer.setCurrentItem(index, false);
490 }
491 });
492 }
493
494 private void peekDrawer(final WearableDrawerView drawer) {
495 drawer.post(new Runnable() {
496 @Override
497 public void run() {
498 drawer.getController().peekDrawer();
499 }
500 });
501 }
502
503 private void openDrawer(final WearableDrawerView drawer) {
504 drawer.post(new Runnable() {
505 @Override
506 public void run() {
507 drawer.getController().openDrawer();
508 }
509 });
510 }
511
512 private static TypeSafeMatcher<View> isOpened(final boolean isOpened) {
513 return new TypeSafeMatcher<View>() {
514 @Override
515 public void describeTo(Description description) {
516 description.appendText("is opened == " + isOpened);
517 }
518
519 @Override
520 public boolean matchesSafely(View view) {
521 return ((WearableDrawerView) view).isOpened() == isOpened;
522 }
523 };
524 }
525
526 private static TypeSafeMatcher<View> isClosed(final boolean isClosed) {
527 return new TypeSafeMatcher<View>() {
528 @Override
529 protected boolean matchesSafely(View view) {
530 WearableDrawerView drawer = (WearableDrawerView) view;
531 return drawer.isClosed() == isClosed;
532 }
533
534 @Override
535 public void describeTo(Description description) {
536 description.appendText("is closed");
537 }
538 };
539 }
540
541 private TypeSafeMatcher<View> isPeeking() {
542 return new TypeSafeMatcher<View>() {
543 @Override
544 protected boolean matchesSafely(View view) {
545 WearableDrawerView drawer = (WearableDrawerView) view;
546 return drawer.isPeeking();
547 }
548
549 @Override
550 public void describeTo(Description description) {
551 description.appendText("is peeking");
552 }
553 };
554 }
555
556 private TypeSafeMatcher<View> allowsSwipeToClose() {
557 return new TypeSafeMatcher<View>() {
558 @Override
559 protected boolean matchesSafely(View view) {
560 return !view.canScrollHorizontally(-2) && !view.canScrollHorizontally(2);
561 }
562
563 @Override
564 public void describeTo(Description description) {
565 description.appendText("can be swiped closed");
566 }
567 };
568 }
569
570 /**
571 * Returns a {@link TypeSafeMatcher} that returns {@code true} when the {@link RecyclerView}
572 * does not contain a {@link TextView} with text matched by {@code textMatcher}.
573 */
574 private TypeSafeMatcher<View> recyclerWithoutText(final Matcher<String> textMatcher) {
575 return new TypeSafeMatcher<View>() {
576
577 @Override
578 public void describeTo(Description description) {
579 description.appendText("Without recycler text ");
580 textMatcher.describeTo(description);
581 }
582
583 @Override
584 public boolean matchesSafely(View view) {
585 if (!(view instanceof RecyclerView)) {
586 return false;
587 }
588
589 RecyclerView recycler = ((RecyclerView) view);
590 if (recycler.isAnimating()) {
591 // While the RecyclerView is animating, it will return null ViewHolders and we
592 // won't be able to tell whether the item has been removed or not.
593 return false;
594 }
595
596 for (int i = 0; i < recycler.getAdapter().getItemCount(); i++) {
597 RecyclerView.ViewHolder holder = recycler.findViewHolderForAdapterPosition(i);
598 if (holder != null) {
599 TextView text = getChildByType(holder.itemView, TextView.class);
600 if (text != null && textMatcher.matches(text.getText())) {
601 return false;
602 }
603 }
604 }
605
606 return true;
607 }
608 };
609 }
610
611 /**
612 * Waits for the {@link RecyclerView} to contain {@code targetCount} items, up to {@code millis}
613 * milliseconds. Throws exception if the time limit is reached before reaching the desired
614 * number of items.
615 */
616 public ViewAction waitForRecyclerToBeSize(final int targetCount, final long millis) {
617 return new ViewAction() {
618 @Override
619 public Matcher<View> getConstraints() {
620 return isAssignableFrom(RecyclerView.class);
621 }
622
623 @Override
624 public String getDescription() {
625 return "Waiting for recycler to be size=" + targetCount;
626 }
627
628 @Override
629 public void perform(UiController uiController, View view) {
630 if (!(view instanceof RecyclerView)) {
631 return;
632 }
633
634 RecyclerView recycler = (RecyclerView) view;
635 uiController.loopMainThreadUntilIdle();
636 final long startTime = System.currentTimeMillis();
637 final long endTime = startTime + millis;
638 do {
639 if (recycler.getAdapter().getItemCount() == targetCount) {
640 return;
641 }
642 uiController.loopMainThreadForAtLeast(100); // at least 3 frames
643 } while (System.currentTimeMillis() < endTime);
644
645 // timeout happens
646 throw new PerformException.Builder()
647 .withActionDescription(this.getDescription())
648 .withViewDescription(HumanReadables.describe(view))
649 .withCause(new TimeoutException())
650 .build();
651 }
652 };
653 }
654
655 /**
656 * Returns the first child of {@code root} to be an instance of class {@code T}, or {@code null}
657 * if none were found.
658 */
659 @Nullable
660 private <T> T getChildByType(View root, Class<T> classOfChildToFind) {
661 for (View child : TreeIterables.breadthFirstViewTraversal(root)) {
662 if (classOfChildToFind.isInstance(child)) {
663 return (T) child;
664 }
665 }
666
667 return null;
668 }
669}