Espresso recipes

Matching a view next to another view

Một layout chứa một số view nhất định, nhưng chúng không phải là duy nhất. Ví dụ button Call trong bảng Contact chẳng hạn, bạn có thể thấy có rất nhiều button Call, nhưng không có nghĩ là một button Call ứng với R.id xác định mà chúng có thể có chung một R.id.

Trong ví dụ này, TextView chứa text “7” được lặp lại trên nhiều hàng.

Thông thường, thì các view thuộc dạng on-unique như này thì nó sẽ luôn đi cùng mới 1 nhãn label bên cạnh (trái hoặc phải, hoặc trên dưới). Trong trường hợp này, thì khi muốn xác định một view non-unique nào đó bạn có thể sử dụng methord hasSibling() để xác định view nào đó:

onView(allOf(withText("7"), hasSibling(withText("item: 0"))))
    .perform(click());

Matching a view that is inside an action bar

Theo dõi ví dụ bên dưới, bạn có thể thấy ActionBar của TestActivity sẽ có 2 action bar khác nhau: Normal ActionBarMenu ActionBar. Cả 2 action bar này đều có 1 view (1 item) luôn được hiển thị trên thanh bar, còn Menu ActionBar sẽ có 1 vài item được hiển thị khi ấn vào. Trong ví dụ này, thì khi ta thực hiện click vào một item nào đó, thì phần text sẽ thay đổi tương ứng với hành động vừa thực hiện.
Đối với những item luôn hiển thị trên action bar, ta có thể check như sau:

public void testClickActionBarItem() {
    // Make sure the contextual action bar is hidden.
    onView(withId(R.id.hide_contextual_action_bar))
        .perform(click());

    // Click on the icon Save
    onView(withId(R.id.action_save))
        .perform(click());

    // Checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Save")));
}

Còn đối với việc check các item được hiển thị khi ấn vào Menu ActionBar:

public void testClickActionModeItem() {
    // Click on the contextual action bar to show icon lock.
    onView(withId(R.id.show_contextual_action_bar))
        .perform(click());

    // Click on the icon lock.
    onView((withId(R.id.action_lock)))
        .perform(click());

    // Checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Lock")));
}

Khi click 1 item thuộc overflow menu (menu tràn) thì nó sẽ phức tạp hơn so mới một normal actionbar, vì một số thiết bị thì có hardware overflow menu button – nó sẽ mở ra các item trong một option menu, hoặc một số thiết bị khác thì có software overflow menu button – nó sẽ mở ra nột normal overflow menu. Và thật may mắn là Espresso đã handle điều này cho chúng ta :))

Ví dụ cho một normal actionbar:

public void testActionBarOverflow() {
    // Make sure the contextual action bar is hidden.
    onView(withId(R.id.hide_contextual_action_bar))
        .perform(click());

    // Open the options menu OR open the overflow menu, depending on whether
    // the device has a hardware or software overflow menu button.
    openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext());

    // Click the item.
    onView(withText("World"))
        .perform(click());

    // Verify that we have really clicked on the icon by checking
    // the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("World")));
}

Và các trường hợp ta có thể nhìn thấy

  • software overflow menu

  • hardware overflow menu

Đối với contextual action bar, đơn giản là:

public void testActionModeOverflow() {
  // Show the contextual action bar.
    onView(withId(R.id.show_contextual_action_bar))
        .perform(click());

    // Open the overflow menu from contextual action mode.
    openContextualActionModeOverflowMenu();

    // Click on the item.
    onView(withText("Key"))
        .perform(click());

    // Verify that we have really clicked on the icon by
    // checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Key")));
  }
}

Asserting that a view is not displayed

Sau khi thực hiện một loạt chuỗi các hành động, và bạn chắc chắn rằng muốn xác định trạng thái của UI đang được test. Nhưng đôi khi có một số trường hợp xấu như một số view, một số thứ không được hiện ra. Hãy ghi nhớ bạn có thể chuyển bất kỳ hamcrest view matcher vào một ViewAssertion bằng cách sử dụng ViewAssertions.matches().

Trong ví dụ bên dưới, chúng ta sẽ sử dụng một matcher isDisplayed() và một phủ định matcher not():

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.Matchers.not;
...
onView(withId(R.id.bottom_left))
    .check(matches(not(isDisplayed())));

Cách sử dụng trên được sử dụng khi view của bạn vẫn là một phần của layout view. Nếu không phải, thì bạn sẽ nhận được một NoMatchingViewException và khi đó bạn nên sử dụng ViewAssertions.doesNotExist().

Asserting that a view is not present

Nếu view bị gone khỏi layout – nó có thể sảy ra khi ta thực hiện các bước chuyển tiếp nó đó như transition sang một activity khác. Và lúc này bạn nên sử dụng ViewAssertions.doesNotExist():

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
...
onView(withId(R.id.bottom_left))
    .check(doesNotExist());

Asserting that a data item is not in an adapter

Để xác định một data item nào đó không thuộc AdapterView bạn cần phải có 1 vài đặc điểm khác biệt nào đó. Chúng ta cần tìm AdapterView mong muốn và xác định xem data mà nó đang giữ. Không cần phải sử dụng onData(), thay vào đó ta có thể sử dụng onView() để tìm AdapterView và sau đó sử dụng một matcher để làm việc với data trên view.

First the matcher:

private static Matcher<View> withAdaptedData(final Matcher<Object> dataMatcher) {
    return new TypeSafeMatcher<View>() {

        @Override
        public void describeTo(Description description) {
            description.appendText("with class name: ");
            dataMatcher.describeTo(description);
        }

        @Override
        public boolean matchesSafely(View view) {
            if (!(view instanceof AdapterView)) {
                return false;
            }

            @SuppressWarnings("rawtypes")
            Adapter adapter = ((AdapterView) view).getAdapter();
            for (int i = 0; i < adapter.getCount(); i++) {
                if (dataMatcher.matches(adapter.getItem(i))) {
                    return true;
                }
            }

            return false;
        }
    };
}

Sau đó ta cần onView() để xác định AdapterView:

@SuppressWarnings("unchecked")
public void testDataItemNotInAdapter(){
    onView(withId(R.id.list))
          .check(matches(not(withAdaptedData(withItemContent("item: 168")))));
    }
}

Và assertion sẽ fail nếu "item: 168" tồn tại trong list.

Using a custom failure handler

Ta có thể thay thế FailureHandler trong mặc định trong Espresso bằng một custom error hoặc một hanlde nào đó như chụp ảnh màn hình hoặc bắn thêm thông tin debug.

Sau đây là một ví dụ xây dựng một CustomFailureHandler:

private static class CustomFailureHandler implements FailureHandler {
    private final FailureHandler delegate;

    public CustomFailureHandler(Context targetContext) {
        delegate = new DefaultFailureHandler(targetContext);
    }

    @Override
    public void handle(Throwable error, Matcher<View> viewMatcher) {
        try {
            delegate.handle(error, viewMatcher);
        } catch (NoMatchingViewException e) {
            throw new MySpecialException(e);
        }
    }
}

Một MySpecialException sẽ được bắn ra khi xuất hiện NoMatchingViewException còn với các trường hợp còn lại sẽ được hanlde bởi DefaultFailureHandler. Một CustomFailureHandler có thể được đăng ký với Espresso trong override methord setUp():

@Override
public void setUp() throws Exception {
    super.setUp();
    getActivity();
    setFailureHandler(new CustomFailureHandler(getInstrumentation()
                                              .getTargetContext()));
}

Targeting non-default windows

Như ta đã biết là đối với các version Android đời cao thì đã có hỗ trợ multiple windows. Thông thường, điều này rất rõ ràng đối với người dùng và dev, tuy nhiên trong một số trường hợp nhiều cửa sổ được hiển thị như là khi một auto-complete window được drawn lên trên main application window trong search widget chẳng hạn. Để làm đơn giản mọi thứ, theo mặc định Espresso sẽ sử dụng phương pháp heuristic để xác định / đoán cửa sổ bạn muốn tương tác. Cách này hầu như là ngon và tốt, nhưng trong một số trường hợp bạn vẫn cần phải xác định cửa sổ target cần tương tác. Bạn có thể thực hiệ việc này bằng cách providing root window matcher hoặc Root matcher:

onView(withText("South China Sea"))
    .inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
    .perform(click());

Như trong trường hợp với ViewMatchers, ta sẽ cung cấp một RootMatchers. Và tất nhiên là bạn vẫn implement các Matcher object như bình thường.

Matching a header or footer in a list view

Header và footers được add vào ListView bằng cách sử dụng method addHeaderView()addFooterView. Hãy chắc rằng Espresso.onData() biết được data object nào cần để match, và vượt qua một object data đã được đặt trước làm tham số thứ 2 của addHeaderView()addFooterView().

Ví dụ:

public static final String FOOTER = "FOOTER";
...
View footerView = layoutInflater.inflate(R.layout.list_item, listView, false);
((TextView) footerView.findViewById(R.id.item_content)).setText("count:");
((TextView) footerView.findViewById(R.id.item_size)).setText(String.valueOf(data.size()));
listView.addFooterView(footerView, FOOTER, true);

Sau đó thực hiện một matcher cho footer:

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;

@SuppressWarnings("unchecked")
public static Matcher<Object> isFooter() {
    return allOf(is(instanceOf(String.class)), is(LongListActivity.FOOTER));
}
import static android.support.test.espresso.Espresso.onData;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.sample.LongListMatchers.isFooter;

public void testClickFooter() {
    onData(isFooter())
        .perform(click());

    // ...
}

Trên đây là một số thiết lập phổ biến để bạn thực hiện e2e test. Trong phần sau mình sẽ giới thiệu một chút về Multiprocess trong Espresso.