blob: cca9c8089048f8e73e4b543e64fb56532329206d [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.android_webview.test.devui;
import static androidx.test.espresso.Espresso.onData;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withSpinnerText;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anything;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.collection.IsMapContaining.hasEntry;
import static org.chromium.android_webview.test.devui.DeveloperUiTestUtils.withCount;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.SystemClock;
import android.support.test.InstrumentationRegistry;
import android.support.test.rule.ActivityTestRule;
import android.view.MotionEvent;
import android.view.View;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.IntDef;
import androidx.test.espresso.DataInteraction;
import androidx.test.filters.MediumTest;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.android_webview.common.AwSwitches;
import org.chromium.android_webview.common.DeveloperModeUtils;
import org.chromium.android_webview.common.Flag;
import org.chromium.android_webview.devui.FlagsFragment;
import org.chromium.android_webview.devui.MainActivity;
import org.chromium.android_webview.devui.R;
import org.chromium.android_webview.services.DeveloperUiService;
import org.chromium.android_webview.test.AwJUnit4ClassRunner;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.Feature;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import java.util.Map;
/**
* UI tests for {@link FlagsFragment}.
* <p>
* These tests should not be batched to make sure that the DeveloperUiService is killed
* after each test, leaving a clean state.
*/
@RunWith(AwJUnit4ClassRunner.class)
public class FlagsFragmentTest {
@Rule
public ActivityTestRule mRule =
new ActivityTestRule<MainActivity>(MainActivity.class, false, false);
@Before
public void setUp() throws Exception {
Intent intent = new Intent();
intent.putExtra(MainActivity.FRAGMENT_ID_INTENT_EXTRA, MainActivity.FRAGMENT_ID_FLAGS);
mRule.launchActivity(intent);
}
@After
public void tearDown() {
// Make sure to clear shared preferences between tests to avoid any saved state.
DeveloperUiService.clearSharedPrefsForTesting(InstrumentationRegistry.getTargetContext());
}
private CallbackHelper getFlagUiSearchBarListener() {
final CallbackHelper helper = new CallbackHelper();
FlagsFragment.setFilterListener(() -> { helper.notifyCalled(); });
return helper;
}
private static Matcher<View> withHintText(final Matcher<String> stringMatcher) {
return new TypeSafeMatcher<View>() {
@Override
public boolean matchesSafely(View view) {
if (!(view instanceof EditText)) {
return false;
}
String hint = ((EditText) view).getHint().toString();
return stringMatcher.matches(hint);
}
@Override
public void describeTo(Description description) {
description.appendText("with hint text: ");
stringMatcher.describeTo(description);
}
};
}
private static Matcher<View> withHintText(final String expectedHint) {
return withHintText(is(expectedHint));
}
@IntDef({CompoundDrawable.START, CompoundDrawable.TOP, CompoundDrawable.END,
CompoundDrawable.BOTTOM})
private @interface CompoundDrawable {
int START = 0;
int TOP = 1;
int END = 2;
int BOTTOM = 3;
}
private static Matcher<View> compoundDrawableVisible(@CompoundDrawable int position) {
return new TypeSafeMatcher<View>() {
@Override
public boolean matchesSafely(View view) {
if (!(view instanceof TextView)) {
return false;
}
Drawable[] compoundDrawables = ((TextView) view).getCompoundDrawablesRelative();
Drawable endDrawable = compoundDrawables[position];
return endDrawable != null;
}
@Override
public void describeTo(Description description) {
description.appendText("with drawable in position " + position);
}
};
}
// Click a TextView at the start/end/top/bottom. Does not check if any CompoundDrawable drawable
// is in that position, it just sends a touch event for those coordinates.
private static void tapCompoundDrawableOnUiThread(
TextView view, @CompoundDrawable int position) {
TestThreadUtils.runOnUiThreadBlocking(() -> {
long downTime = SystemClock.uptimeMillis();
long eventTime = downTime + 50;
float x = view.getWidth() / 2.0f;
float y = view.getHeight() / 2.0f;
if (position == CompoundDrawable.START) {
x = 0.0f;
} else if (position == CompoundDrawable.END) {
x = view.getWidth();
} else if (position == CompoundDrawable.TOP) {
y = 0.0f;
} else if (position == CompoundDrawable.BOTTOM) {
y = view.getHeight();
}
int metaState = 0; // no modifier keys (ex. alt/control), this is just a touch event
view.dispatchTouchEvent(MotionEvent.obtain(
downTime, eventTime, MotionEvent.ACTION_UP, x, y, metaState));
});
}
@Test
@MediumTest
@Feature({"AndroidWebView"})
public void testSearchEmptyByDefault() throws Throwable {
onView(withId(R.id.flag_search_bar)).check(matches(withText("")));
onView(withId(R.id.flag_search_bar)).check(matches(withHintText("Search flags")));
// Magnifier should always be visible, "clear text" icon should be hidden
onView(withId(R.id.flag_search_bar))
.check(matches(compoundDrawableVisible(CompoundDrawable.START)));
onView(withId(R.id.flag_search_bar))
.check(matches(not(compoundDrawableVisible(CompoundDrawable.END))));
}
@Test
@MediumTest
@Feature({"AndroidWebView"})
public void testSearchByName() throws Throwable {
CallbackHelper helper = getFlagUiSearchBarListener();
int searchBarChangeCount = helper.getCallCount();
onView(withId(R.id.flag_search_bar)).perform(replaceText("logging"));
helper.waitForCallback(searchBarChangeCount, 1);
onView(allOf(withId(R.id.flag_name), withText(AwSwitches.WEBVIEW_VERBOSE_LOGGING)))
.check(matches(isDisplayed()));
onView(withId(R.id.flags_list)).check(matches(withCount(1)));
}
@Test
@MediumTest
@Feature({"AndroidWebView"})
public void testSearchByDescription() throws Throwable {
CallbackHelper helper = getFlagUiSearchBarListener();
int searchBarChangeCount = helper.getCallCount();
onView(withId(R.id.flag_search_bar)).perform(replaceText("highlight the contents"));
helper.waitForCallback(searchBarChangeCount, 1);
onView(allOf(withId(R.id.flag_name), withText(AwSwitches.HIGHLIGHT_ALL_WEBVIEWS)))
.check(matches(isDisplayed()));
onView(withId(R.id.flags_list)).check(matches(withCount(1)));
}
@Test
@MediumTest
@Feature({"AndroidWebView"})
public void testCaseInsensitive() throws Throwable {
CallbackHelper helper = getFlagUiSearchBarListener();
int searchBarChangeCount = helper.getCallCount();
onView(withId(R.id.flag_search_bar)).perform(replaceText("LOGGING"));
helper.waitForCallback(searchBarChangeCount, 1);
onView(allOf(withId(R.id.flag_name), withText(AwSwitches.WEBVIEW_VERBOSE_LOGGING)))
.check(matches(isDisplayed()));
onView(withId(R.id.flags_list)).check(matches(withCount(1)));
}
@Test
@MediumTest
@Feature({"AndroidWebView"})
public void testMultipleResults() throws Throwable {
CallbackHelper helper = getFlagUiSearchBarListener();
ListView flagsList = mRule.getActivity().findViewById(R.id.flags_list);
int totalNumFlags = flagsList.getCount();
// This assumes:
// * There will always be > 1 flag which mentions WebView explicitly (ex.
// HIGHLIGHT_ALL_WEBVIEWS and WEBVIEW_VERBOSE_LOGGING)
// * There will always be >= 1 flag which does not mention WebView in its description (ex.
// --show-composited-layer-borders).
int searchBarChangeCount = helper.getCallCount();
onView(withId(R.id.flag_search_bar)).perform(replaceText("webview"));
helper.waitForCallback(searchBarChangeCount, 1);
onView(withId(R.id.flags_list))
.check(matches(withCount(allOf(greaterThan(1), lessThan(totalNumFlags)))));
}
@Test
@MediumTest
@Feature({"AndroidWebView"})
public void testClearingSearchShowsAllFlags() throws Throwable {
CallbackHelper helper = getFlagUiSearchBarListener();
ListView flagsList = mRule.getActivity().findViewById(R.id.flags_list);
int totalNumFlags = flagsList.getCount();
int searchBarChangeCount = helper.getCallCount();
onView(withId(R.id.flag_search_bar)).perform(replaceText("logging"));
helper.waitForCallback(searchBarChangeCount, 1);
onView(withId(R.id.flags_list)).check(matches(withCount(1)));
onView(withId(R.id.flag_search_bar)).perform(replaceText(""));
helper.waitForCallback(searchBarChangeCount, 2);
onView(withId(R.id.flags_list)).check(matches(withCount(totalNumFlags)));
}
@Test
@MediumTest
@Feature({"AndroidWebView"})
public void testTappingClearButtonClearsText() throws Throwable {
CallbackHelper helper = getFlagUiSearchBarListener();
int searchBarChangeCount = helper.getCallCount();
onView(withId(R.id.flag_search_bar)).perform(replaceText("logging"));
helper.waitForCallback(searchBarChangeCount, 1);
// "x" icon should visible if there's some text
onView(withId(R.id.flag_search_bar))
.check(matches(compoundDrawableVisible(CompoundDrawable.END)));
EditText searchBar = mRule.getActivity().findViewById(R.id.flag_search_bar);
tapCompoundDrawableOnUiThread(searchBar, CompoundDrawable.END);
// "x" icon disappears when text is cleared
onView(withId(R.id.flag_search_bar)).check(matches(withText("")));
onView(withId(R.id.flag_search_bar))
.check(matches(not(compoundDrawableVisible(CompoundDrawable.END))));
// Magnifier icon should still be visible
onView(withId(R.id.flag_search_bar))
.check(matches(compoundDrawableVisible(CompoundDrawable.START)));
}
@Test
@MediumTest
@Feature({"AndroidWebView"})
public void testDeletingTextHidesClearTextButton() throws Throwable {
CallbackHelper helper = getFlagUiSearchBarListener();
int searchBarChangeCount = helper.getCallCount();
onView(withId(R.id.flag_search_bar)).perform(replaceText("logging"));
helper.waitForCallback(searchBarChangeCount, 1);
// "x" icon should visible if there's some text
onView(withId(R.id.flag_search_bar))
.check(matches(compoundDrawableVisible(CompoundDrawable.END)));
onView(withId(R.id.flag_search_bar)).perform(replaceText(""));
// "x" icon disappears when text is cleared
onView(withId(R.id.flag_search_bar)).check(matches(withText("")));
onView(withId(R.id.flag_search_bar))
.check(matches(not(compoundDrawableVisible(CompoundDrawable.END))));
// Magnifier icon should still be visible
onView(withId(R.id.flag_search_bar))
.check(matches(compoundDrawableVisible(CompoundDrawable.START)));
}
@Test
@MediumTest
@Feature({"AndroidWebView"})
public void testElsewhereOnSearchBarDoesNotClearText() throws Throwable {
CallbackHelper helper = getFlagUiSearchBarListener();
int searchBarChangeCount = helper.getCallCount();
onView(withId(R.id.flag_search_bar)).perform(replaceText("logging"));
helper.waitForCallback(searchBarChangeCount, 1);
EditText searchBar = mRule.getActivity().findViewById(R.id.flag_search_bar);
tapCompoundDrawableOnUiThread(searchBar, CompoundDrawable.TOP);
// EditText should not be cleared
onView(withId(R.id.flag_search_bar)).check(matches(withText("logging")));
// "x" icon is still visible
onView(withId(R.id.flag_search_bar))
.check(matches(compoundDrawableVisible(CompoundDrawable.END)));
}
/**
* Toggle a flag's spinner with the given state value, and check that text changes to the
* correct value.
*
* @param flagInteraction the {@link DataInteraction} object representing a flag View item via
* {@code onData()}.
* @param state {@code true} for "enabled", {@code false} for "disabled", {@code null} for
* Default.
* @return the same {@code flagInteraction} passed param for the ease of chaining.
*/
private DataInteraction toggleFlag(DataInteraction flagInteraction, Boolean state) {
String stateText = state == null ? "Default" : state ? "Enabled" : "Disabled";
flagInteraction.onChildView(withId(R.id.flag_toggle)).perform(click());
onData(allOf(is(instanceOf(String.class)), is(stateText))).perform(click());
flagInteraction.onChildView(withId(R.id.flag_toggle))
.check(matches(withSpinnerText(containsString(stateText))));
return flagInteraction;
}
@Test
@MediumTest
@Feature({"AndroidWebView"})
public void testTogglingFlagShowsBlueDot() throws Throwable {
DataInteraction flagInteraction =
onData(anything()).inAdapterView(withId(R.id.flags_list)).atPosition(1);
// blue dot should be hidden by default
flagInteraction.onChildView(withId(R.id.flag_name))
.check(matches(not(compoundDrawableVisible(CompoundDrawable.START))));
// Test enabling flags shows a bluedot next to flag name
toggleFlag(flagInteraction, true);
flagInteraction.onChildView(withId(R.id.flag_name))
.check(matches(compoundDrawableVisible(CompoundDrawable.START)));
// Test setting to default hide the blue dot
toggleFlag(flagInteraction, null);
flagInteraction.onChildView(withId(R.id.flag_name))
.check(matches(not(compoundDrawableVisible(CompoundDrawable.START))));
// Test disabling flags shows a bluedot next to flag name
toggleFlag(flagInteraction, false);
flagInteraction.onChildView(withId(R.id.flag_name))
.check(matches(compoundDrawableVisible(CompoundDrawable.START)));
// Test setting to default again hide the blue dot
toggleFlag(flagInteraction, null);
flagInteraction.onChildView(withId(R.id.flag_name))
.check(matches(not(compoundDrawableVisible(CompoundDrawable.START))));
}
@Test
@MediumTest
@Feature({"AndroidWebView"})
public void testToggledFlagsFloatToTop() throws Throwable {
ListView flagsList = mRule.getActivity().findViewById(R.id.flags_list);
int totalNumFlags = flagsList.getCount();
String lastFlagName = ((Flag) flagsList.getAdapter().getItem(totalNumFlags - 1)).getName();
// Toggle the last flag in the list.
toggleFlag(onData(anything())
.inAdapterView(withId(R.id.flags_list))
.atPosition(totalNumFlags - 1),
true);
// Navigate from the flags UI then back to it to trigger list sorting.
onView(withId(R.id.navigation_home)).perform(click());
onView(withId(R.id.navigation_flags_ui)).perform(click());
// Check that the toggled flag is now at the top of the list. This assumes that the flags
// list has > 2 items.
onData(anything())
.inAdapterView(withId(R.id.flags_list))
.atPosition(1)
.onChildView(withId(R.id.flag_name))
.check(matches(withText(lastFlagName)));
// Reset to default.
toggleFlag(onData(anything()).inAdapterView(withId(R.id.flags_list)).atPosition(1), null);
// Navigate from the flags UI then back to it to trigger list sorting.
onView(withId(R.id.navigation_home)).perform(click());
onView(withId(R.id.navigation_flags_ui)).perform(click());
// Check that flags goes back to the end of the list when untoggled.
onData(anything())
.inAdapterView(withId(R.id.flags_list))
.atPosition(totalNumFlags - 1)
.onChildView(withId(R.id.flag_name))
.check(matches(withText(lastFlagName)));
}
@Test
@MediumTest
@Feature({"AndroidWebView"})
public void testResetFlags() throws Throwable {
ListView flagsList = mRule.getActivity().findViewById(R.id.flags_list);
String firstFlagName = ((Flag) flagsList.getAdapter().getItem(1)).getName();
toggleFlag(onData(anything()).inAdapterView(withId(R.id.flags_list)).atPosition(1), true);
Map<String, Boolean> flagOverrides =
DeveloperModeUtils.getFlagOverrides(DeveloperUiTest.TEST_WEBVIEW_PACKAGE_NAME);
assertThat(
"flagOverrides map should contain exactly one entry", flagOverrides.size(), is(1));
assertThat(flagOverrides, hasEntry(firstFlagName, true));
onView(withId(R.id.reset_flags_button)).perform(click());
DataInteraction flagInteraction =
onData(anything()).inAdapterView(withId(R.id.flags_list)).atPosition(1);
flagInteraction.onChildView(withId(R.id.flag_name))
.check(matches(not(compoundDrawableVisible(CompoundDrawable.START))));
flagInteraction.onChildView(withId(R.id.flag_toggle))
.check(matches(withSpinnerText(containsString("Default"))));
Assert.assertTrue(
DeveloperModeUtils.getFlagOverrides(DeveloperUiTest.TEST_WEBVIEW_PACKAGE_NAME)
.isEmpty());
}
}