android - 以编程方式从 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
解决方案
推荐阅读
- java - 如何为不同的用户提供不同的 Autowired 模型
- php - PHP测试多种形式
- ios - iOS 弱链接符号
- java - CmisUnauthorizedException - 连接到共享点
- c - 如何处理将新对象排队到已满的循环队列?
- c - 预期为“int”,但参数的类型为“char *”
- reactjs - React JS:有条件的样式正在工作,但需要刷新才能正确应用
- azure - Application Insights 和 kubernetes:如何不记录成功的 /liveness 和 /hc 探测以跟踪日志
- java - @Valid Annotations 在测试 Spring Boot 服务时无法从 Junit 5 和 mockito 工作
- sql - 性能调优复杂 SQL 连接