首页 > 解决方案 > 以编程方式从 Espresso 发出长按返回键

问题描述

我的 Espresso 集成测试需要向被测应用发出长按返回按钮,而不需要对应用源代码进行任何更改。虽然我的测试可以成功发出一个正常的后退按钮(通过androidx.test.espresso.Espresso.pressBack())和长按到显示器上的给定像素位置(通过androidx.test.espresso.action.GeneralClickAction),他们无法成功发出长按后退按钮。该应用程序显示 UI 没有任何变化,但如果在测试操作期间手动长按它就可以正常工作。

我如何让它工作?借用 Espresso,我尝试了以下方法,只更改了类名和 KeyEvent 构造函数。

onView(isRoot()).perform(ViewActions.actionWithAssertions(new PressLongBackAction(true)));

***公共最终类 PressLongBackAction 扩展 LongPressKeyEventActionBase {

private final boolean conditional;

public PressLongBackAction(boolean conditional) {
    this(conditional, new EspressoKey.Builder().withKeyCode(KeyEvent.KEYCODE_BACK).build());
}

public PressLongBackAction(boolean conditional, EspressoKey espressoKey) {
    super(espressoKey);
    this.conditional = conditional;
}

@Override
public void perform(UiController uiController, View view) {

    Activity initialActivity = getCurrentActivity();

    new Exception().printStackTrace(System.out);
    super.perform(uiController, view);
    new Exception().printStackTrace(System.out);

    // Wait for a Stage change of the initial activity.
    waitForStageChangeInitialActivity(uiController, initialActivity);
    // Wait until there are no other pending activities in a foreground stage.
    waitForPendingForegroundActivities(uiController, conditional);
}

}


***类 LongPressKeyEventActionBase 实现 ViewAction { private static final String TAG = "KeyEventTestActionBase";

public static final int BACK_ACTIVITY_TRANSITION_MILLIS_DELAY = 150;
public static final int CLEAR_TRANSITIONING_ACTIVITIES_ATTEMPTS = 4;
public static final int CLEAR_TRANSITIONING_ACTIVITIES_MILLIS_DELAY = 150;

final EspressoKey espressoKey;

LongPressKeyEventActionBase(EspressoKey espressoKey) {
    this.espressoKey = checkNotNull(espressoKey);
}

@Override
public Matcher<View> getConstraints() {
    return isDisplayed();
}

@Override
public String getDescription() {
    return String.format(Locale.ROOT, "send %s key event", this.espressoKey);
}

@Override
public void perform(UiController uiController, View view) {
    try {
        if (!sendKeyEvent(uiController)) {
            Log.e(TAG, "Failed to inject espressoKey event: " + this.espressoKey);
            throw new PerformException.Builder()
                    .withActionDescription(this.getDescription())
                    .withViewDescription(HumanReadables.describe(view))
                    .withCause(
                            new RuntimeException("Failed to inject espressoKey event " + this.espressoKey))
                    .build();
        }
    } catch (InjectEventSecurityException e) {
        Log.e(TAG, "Failed to inject espressoKey event: " + this.espressoKey);
        throw new PerformException.Builder()
                .withActionDescription(this.getDescription())
                .withViewDescription(HumanReadables.describe(view))
                .withCause(e)
                .build();
    }
}

private boolean sendKeyEvent(UiController controller) throws InjectEventSecurityException {

    boolean injected = false;
    long eventTime = SystemClock.uptimeMillis();
    for (int attempts = 0; !injected && attempts < 4; attempts++) {
        final KeyEvent keyEvent = new KeyEvent(
                eventTime,
                eventTime,
                KeyEvent.ACTION_DOWN,
                this.espressoKey.getKeyCode(),
                0,
                this.espressoKey.getMetaState(),
                -1,
                0,
                KeyEvent.FLAG_LONG_PRESS);
        Log.d(TAG, "keyEvent : " + keyEvent.toString());
        injected = controller.injectKeyEvent(keyEvent);
    }

    if (!injected) {
        // it is not a transient failure... :(
        return false;
    }

    injected = false;
    eventTime = SystemClock.uptimeMillis();
    for (int attempts = 0; !injected && attempts < 4; attempts++) {
        final KeyEvent keyEvent = new KeyEvent(
                eventTime,
                eventTime,
                KeyEvent.ACTION_UP,
                this.espressoKey.getKeyCode(),
                0,
                this.espressoKey.getMetaState(),
                -1,
                0,
                KeyEvent.FLAG_LONG_PRESS );
        Log.d(TAG, "keyEvent : " + keyEvent.toString());
        injected = controller.injectKeyEvent(keyEvent);
    }

    return injected;
}

static Activity getCurrentActivity() {
    Collection<Activity> resumedActivities =
            ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED);
    return getOnlyElement(resumedActivities);
}

static void waitForStageChangeInitialActivity(UiController controller, Activity initialActivity) {
    if (isActivityResumed(initialActivity)) {
        // The activity transition hasn't happened yet, wait for it.
        controller.loopMainThreadForAtLeast(BACK_ACTIVITY_TRANSITION_MILLIS_DELAY);
        if (isActivityResumed(initialActivity)) {
            Log.e(
                    TAG,
                    "Back was pressed but there was no Activity stage transition in "
                            + BACK_ACTIVITY_TRANSITION_MILLIS_DELAY
                            + "ms, possibly due to a delay calling super.onBackPressed() from your Activity.");
        }
    }
}

private static boolean isActivityResumed(Activity activity) {
    return ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity)
            == Stage.RESUMED;
}

static void waitForPendingForegroundActivities(UiController controller, boolean conditional) {
    ActivityLifecycleMonitor activityLifecycleMonitor =
            ActivityLifecycleMonitorRegistry.getInstance();
    boolean pendingForegroundActivities = false;
    for (int attempts = 0; attempts < CLEAR_TRANSITIONING_ACTIVITIES_ATTEMPTS; attempts++) {
        controller.loopMainThreadUntilIdle();
        pendingForegroundActivities = hasTransitioningActivities(activityLifecycleMonitor);
        if (pendingForegroundActivities) {
            controller.loopMainThreadForAtLeast(CLEAR_TRANSITIONING_ACTIVITIES_MILLIS_DELAY);
        } else {
            break;
        }
    }

    // Pressing back can kill the app: log a warning.
    if (!hasForegroundActivities(activityLifecycleMonitor)) {
        if (conditional) {
            throw new NoActivityResumedException("Pressed back and killed the app");
        }
        Log.w(TAG, "Pressed back and hopped to a different process or potentially killed the app");
    }

    if (pendingForegroundActivities) {
        Log.e(
                TAG,
                "Back was pressed and left the application in an inconsistent state even after "
                        + (CLEAR_TRANSITIONING_ACTIVITIES_MILLIS_DELAY
                        * CLEAR_TRANSITIONING_ACTIVITIES_ATTEMPTS)
                        + "ms.");
    }
}

}


正常按下后退按钮的 KeyEvent 如下所示:***logcat:

2021-05-12 11:49:57.501 31118-31118/com... D/...: keyEvent : KeyEvent { action=ACTION_DOWN, keyCode=KEYCODE_BACK, scanCode=0, metaState=0, flags=0x80, repeatCount= 0, eventTime=1234519744, downTime=1234519744, deviceId=-1, displayId=0, source=0x0 }

2021-05-12 11:49:57.508 31118-31118/com... D/ViewRootImpl@5a0d36[ActivityMain]: ViewPostImeInputStage processKey 0

2021-05-12 11:49:57.510 31118-31118/com... D/...: keyEvent : KeyEvent { action=ACTION_UP, keyCode=KEYCODE_BACK, scanCode=0, metaState=0, flags=0x80, repeatCount= 0, eventTime=1234519753, downTime=1234519753, deviceId=-1, displayId=0, source=0x0 }

2021-05-12 11:49:57.513 31118-31118/com... D/ViewRootImpl@5a0d36[ActivityMain]: ViewPostImeInputStage processKey 1

2021-05-12 11:50:04.682 31118-31118/com... D/...: keyEvent : KeyEvent { action=ACTION_DOWN, keyCode=KEYCODE_BACK, scanCode=0, metaState=0, flags=0x0, repeatCount= 0, eventTime=1234526925, downTime=1234526925, deviceId=-1, displayId=0, source=0x0 }

2021-05-12 11:50:04.686 31118-31118/com... D/ViewRootImpl@5a0d36[ActivityMain]: ViewPostImeInputStage processKey 0

2021-05-12 11:50:04.688 31118-31118/com... D/...: keyEvent : KeyEvent { action=ACTION_UP, keyCode=KEYCODE_BACK, scanCode=0, metaState=0, flags=0x0, repeatCount= 0, eventTime=1234526931, downTime=1234526931, deviceId=-1, displayId=0, source=0x0 }

2021-05-12 11:50:04.691 31118-31118/com... D/ViewRootImpl@5a0d36[ActivityMain]: ViewPostImeInputStage processKey 1

2021-05-12 11:50:05.101 1314-1418/? V/WindowManager: Relayout Window{5dfbe22d0 u0 com.../com...ui.ActivityMain}: viewVisibility=0 req=2048x1536 WM.LayoutParams{(0,0)(fillxfill) sim=#20 ty=1 fl= #410500 wanim=0x1030465 需要MenuKey=1 naviIconColor=0}

2021-05-12 11:50:05.107 31118-31118/com... D/ViewRootImpl@5a0d36[ActivityMain]:Relayout 返回:oldFrame=[0,0][2048,1536] newFrame=[0,0][ 2048,1536] 结果=0x1 表面={isValid=true 547395848192}surfaceGenerationChanged=false


标签: androidandroid-espresso

解决方案


推荐阅读