Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check Window flags when selecting root view in LocalUiController. #6741 #6745

Merged
merged 1 commit into from Nov 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -2,9 +2,6 @@

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import org.robolectric.integration.axt.R;

/** Fixture activity for {@link EspressoTest} */
Expand All @@ -18,13 +15,6 @@ public void onCreate(Bundle savedInstanceState) {

setContentView(R.layout.espresso_activity);

Button button = findViewById(R.id.button);
button.setOnClickListener(
new OnClickListener() {
@Override
public void onClick(View view) {
buttonClicked = true;
}
});
findViewById(R.id.button).setOnClickListener(view -> buttonClicked = true);
}
}
@@ -0,0 +1,216 @@
package org.robolectric.integrationtests.axt;

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.action.ViewActions.typeText;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static com.google.common.truth.Truth.assertThat;

import android.view.View;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.PopupWindow;
import android.widget.TextView;
import androidx.test.core.app.ActivityScenario;
import androidx.test.espresso.Root;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.LooperMode;
import org.robolectric.integration.axt.R;

/** Test Espresso on Robolectric interoperability for toolbar menus. */
@RunWith(AndroidJUnit4.class)
@LooperMode(LooperMode.Mode.PAUSED)
public class EspressoWithWindowLayersTest {
private static final String TEXT = "Hello World";

/** The touchable popup window gets the event and the button is *not* clicked. */
@Test
public void click_interactivePopupWindow_isNotClicked() {
try (ActivityScenario<EspressoActivity> scenario =
ActivityScenario.launch(EspressoActivity.class)) {
showInteractivePopupAsButtonDropdown(scenario);

onView(withId(R.id.button)).inRoot(new IsBaseApplication()).perform(click());

scenario.onActivity(activity -> assertThat(activity.buttonClicked).isFalse());
}
}

/** The touchable popup window gets the event and the button is *not* clicked. */
@Test
public void click_occludingInteractivePopupWindow_isNotClicked() {
try (ActivityScenario<EspressoActivity> scenario =
ActivityScenario.launch(EspressoActivity.class)) {
showOccludingInteractivePopupAsButtonDropdown(scenario);

onView(withId(R.id.button)).inRoot(new IsBaseApplication()).perform(click());

scenario.onActivity(activity -> assertThat(activity.buttonClicked).isFalse());
}
}

/** The non-touchable popup window does *not* get the event and the button is clicked. */
@Test
public void click_nonInteractivePopupWindow_isClicked() {
try (ActivityScenario<EspressoActivity> scenario =
ActivityScenario.launch(EspressoActivity.class)) {
showNonInteractivePopupAsButtonDropdown(scenario);

onView(withId(R.id.button)).perform(click());

scenario.onActivity(activity -> assertThat(activity.buttonClicked).isTrue());
}
}

/** The focusable popup window gets the event and the text is *not* typed. */
@Test
public void typeText_interactivePopupWindow_textIsNotTyped() {
try (ActivityScenario<EspressoActivity> scenario =
ActivityScenario.launch(EspressoActivity.class)) {
showOccludingInteractivePopupAsButtonDropdown(scenario);

onView(withId(R.id.edit_text)).inRoot(new IsBaseApplication()).perform(typeText(TEXT));

scenario.onActivity(
activity -> {
TextView tv = activity.findViewById(R.id.edit_text);
assertThat(tv.getText().toString()).isNotEqualTo(TEXT);
});
}
}

/** The non-focusable popup window does *not* get the event and the text is typed. */
@Test
public void typeText_nonInteractivePopupWindow_textIsTyped() {
try (ActivityScenario<EspressoActivity> scenario =
ActivityScenario.launch(EspressoActivity.class)) {
showNonInteractivePopupAsButtonDropdown(scenario);

onView(withId(R.id.edit_text)).inRoot(new IsBaseApplication()).perform(typeText(TEXT));

scenario.onActivity(
activity -> {
TextView tv = activity.findViewById(R.id.edit_text);
assertThat(tv.getText().toString()).isEqualTo(TEXT);
});
}
}

/** Replacing text does not depend on events, so the focusable window does not interfere. */
@Test
public void replaceText_interactivePopupWindow_textIsReplaced() {
try (ActivityScenario<EspressoActivity> scenario =
ActivityScenario.launch(EspressoActivity.class)) {
showOccludingInteractivePopupAsButtonDropdown(scenario);

onView(withId(R.id.edit_text)).inRoot(new IsBaseApplication()).perform(replaceText(TEXT));

scenario.onActivity(
activity -> {
TextView tv = activity.findViewById(R.id.edit_text);
assertThat(tv.getText().toString()).isEqualTo(TEXT);
});
}
}

/** Replacing text does not depend on events, so the non-focusable window does not interfere. */
@Test
public void replaceText_nonInteractivePopupWindow_textIsReplaced() {
try (ActivityScenario<EspressoActivity> scenario =
ActivityScenario.launch(EspressoActivity.class)) {
showNonInteractivePopupAsButtonDropdown(scenario);

onView(withId(R.id.edit_text)).perform(replaceText(TEXT));

scenario.onActivity(
activity -> {
TextView tv = activity.findViewById(R.id.edit_text);
assertThat(tv.getText().toString()).isEqualTo(TEXT);
});
}
}

/**
* Shows an occluding touchable and focusable popup window as a drop-down on the button.
*
* <p>The drop-down is shown *over* the button by adjusting the x and y offsets. The position of
* the popup is *not* yet accounted for in the window selection heuristic. If it were, we should
* see different behavior when attempting to click the button underneath.
*/
private static void showOccludingInteractivePopupAsButtonDropdown(
ActivityScenario<EspressoActivity> scenario) {
scenario.onActivity(
activity -> {
View anchor = activity.findViewById(R.id.button);
new PopupWindow(
/* contentView= */ new FrameLayout(activity),
/* width= */ anchor.getWidth() * 2,
/* height= */ anchor.getHeight() * 2,
/* focusable= */ true)
.showAsDropDown(anchor, -anchor.getWidth(), -anchor.getHeight());
});
}

/** Shows a non-occluding touchable and focusable popup window as a drop-down on the button. */
private static void showInteractivePopupAsButtonDropdown(
ActivityScenario<EspressoActivity> scenario) {
scenario.onActivity(
activity -> {
View anchor = activity.findViewById(R.id.button);
new PopupWindow(
/* contentView= */ new FrameLayout(activity),
/* width= */ 10,
/* height= */ 10,
/* focusable= */ true)
.showAsDropDown(anchor);
});
}

/**
* Shows an occluding non-touchable and non-focusable popup window as a drop-down on the button.
*
* <p>The drop-down is shown *over* the button by adjusting the x and y offsets. The position of
* the popup is *not* yet accounted for in the window selection heuristic. If it were, we should
* see different behavior when attempting to click the button underneath.
*/
private static void showNonInteractivePopupAsButtonDropdown(
ActivityScenario<EspressoActivity> scenario) {
scenario.onActivity(
activity -> {
View anchor = activity.findViewById(R.id.button);
PopupWindow popup =
new PopupWindow(
/* contentView= */ new FrameLayout(activity),
/* width= */ anchor.getWidth() * 2,
/* height= */ anchor.getHeight() * 2,
/* focusable= */ false);
popup.setTouchable(false);
popup.showAsDropDown(anchor, -anchor.getWidth(), -anchor.getHeight());
});
}

/**
* Espresso Root matcher for only windows with the base application window type.
*
* <p>This matcher is required as Espresso will dutifully default to finding views in the focused
* window. However, the events are *not* guaranteed to be dispatched in this window. Robolectric
* uses a different heuristic, so forcing this window to be used is good for testing.
*/
static final class IsBaseApplication extends TypeSafeMatcher<Root> {
@Override
public void describeTo(Description description) {
description.appendText("is the base application window");
}

@Override
public boolean matchesSafely(Root root) {
return root.getWindowLayoutParams().get().type
== WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
}
}
}
Expand Up @@ -38,7 +38,8 @@ public boolean injectMotionEvent(MotionEvent event) throws InjectEventSecurityEx
checkState(Looper.myLooper() == Looper.getMainLooper(), "Expecting to be on main thread!");
loopMainThreadUntilIdle();

getViewRoot().dispatchTouchEvent(event);
// FLAG_NOT_TOUCHABLE: "this window can never receive touch events"
getTopMostViewRootExcluding(LayoutParams.FLAG_NOT_TOUCHABLE).dispatchTouchEvent(event);

loopMainThreadUntilIdle();

Expand All @@ -49,9 +50,10 @@ public boolean injectMotionEvent(MotionEvent event) throws InjectEventSecurityEx
public boolean injectKeyEvent(KeyEvent event) throws InjectEventSecurityException {
checkNotNull(event);
checkState(Looper.myLooper() == Looper.getMainLooper(), "Expecting to be on main thread!");

loopMainThreadUntilIdle();
getViewRoot().dispatchKeyEvent(event);

// FLAG_NOT_FOCUSABLE: "this window won't ever get key input focus"
getTopMostViewRootExcluding(LayoutParams.FLAG_NOT_FOCUSABLE).dispatchKeyEvent(event);
utzcoz marked this conversation as resolved.
Show resolved Hide resolved

loopMainThreadUntilIdle();
return true;
Expand Down Expand Up @@ -146,7 +148,7 @@ public void loopMainThreadForAtLeast(long millisDelay) {
shadowMainLooper().idleFor(Duration.ofMillis(millisDelay));
}

private View getViewRoot() {
private View getTopMostViewRootExcluding(int prohibitedFlags) {
List<ViewRootImpl> viewRoots = getViewRoots();
if (viewRoots.isEmpty()) {
throw new IllegalStateException("no view roots!");
Expand All @@ -159,9 +161,13 @@ private View getViewRoot() {
int topMostRootIndex = 0;
for (int i = 0; i < params.size(); i++) {
LayoutParams param = params.get(i);
if (param.type > params.get(topMostRootIndex).type) {
topMostRootIndex = i;
if ((param.flags & prohibitedFlags) != 0) {
continue;
}
if (param.type <= params.get(topMostRootIndex).type) {
continue;
}
topMostRootIndex = i;
}
return viewRoots.get(topMostRootIndex).getView();
}
Expand Down