android-midscene-automation 1.1.2 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,4 +1,7 @@
1
1
  # 更新记录
2
+ ## v1.1.3
3
+ - [Appium]修复登录等点击操作已经触发 Activity 跳转后,UiAutomator2 仍等待点击确认、最终误报超时并占用命令队列的问题;元素点击先读取实时区域,再通过 ADB 点击中心坐标,并限制界面空闲等待时间。
4
+
2
5
  ## v1.1.2
3
6
  - [Appium]修复登录等点击操作已经触发 Activity 跳转后,持续动画或频繁界面事件仍让 UiAutomator2 长时间等待、最终误报点击超时的问题;限制界面空闲等待时间,避免命令队列被长期占用。
4
7
 
package/README.md CHANGED
@@ -1502,6 +1502,9 @@ Linux: ~/.local/share/android-midscene-automation
1502
1502
 
1503
1503
  # 项目更新记录
1504
1504
 
1505
+ ## v1.1.3
1506
+ - [Appium]修复登录等点击操作已经触发 Activity 跳转后,UiAutomator2 仍等待点击确认、最终误报超时并占用命令队列的问题;元素点击先读取实时区域,再通过 ADB 点击中心坐标,并限制界面空闲等待时间。
1507
+
1505
1508
  ## v1.1.2
1506
1509
  - [Appium]修复登录等点击操作已经触发 Activity 跳转后,持续动画或频繁界面事件仍让 UiAutomator2 长时间等待、最终误报点击超时的问题;限制界面空闲等待时间,避免命令队列被长期占用。
1507
1510
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "android-midscene-automation",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/oooooooko/android-midscene-automation.git"
@@ -536,11 +536,36 @@ async function imageCheckRegion(sessionId: string, step: AppiumRecordedStepRecor
536
536
 
537
537
  async function tapFallback(deviceId: string, step: AppiumRecordedStepRecord) {
538
538
  if (step.fallback?.strategy !== 'bounds' || !Number.isFinite(step.fallback.centerX) || !Number.isFinite(step.fallback.centerY)) {
539
- throw new Error(`${step.label} 未找到元素,且没有可用坐标兜底`);
539
+ throw new Error(`${step.label} 没有可用点击坐标`);
540
540
  }
541
541
  await adbTap(deviceId, Number(step.fallback.centerX), Number(step.fallback.centerY));
542
542
  }
543
543
 
544
+ async function tapLocatedElement(
545
+ sessionId: string,
546
+ deviceId: string,
547
+ elementId: string,
548
+ step?: AppiumRecordedStepRecord,
549
+ ) {
550
+ try {
551
+ const payload = await appiumRequest<AppiumValueResponse<VisualChangeRegion>>(`/session/${sessionId}/element/${elementId}/rect`);
552
+ const rect = payload.value;
553
+ if (rect && [rect.x, rect.y, rect.width, rect.height].every(Number.isFinite) && rect.width > 0 && rect.height > 0) {
554
+ await adbTap(deviceId, rect.x + rect.width / 2, rect.y + rect.height / 2);
555
+ return;
556
+ }
557
+ } catch (error) {
558
+ throwIfReplayStopped(error);
559
+ // 普通点击的外层会重新定位一次,避免列表刷新后使用已失效元素的旧位置。
560
+ if (isStaleElementError(error)) throw error;
561
+ }
562
+ if (step) {
563
+ await tapFallback(deviceId, step);
564
+ return;
565
+ }
566
+ throw new Error('无法读取目标元素的实时点击区域');
567
+ }
568
+
544
569
  async function waitForElement(sessionId: string, step: AppiumRecordedStepRecord) {
545
570
  const startedAt = Date.now();
546
571
  const timeoutMs = step.timeoutMs ?? DEFAULT_NODE_TIMEOUT_MS;
@@ -908,10 +933,7 @@ async function runStep(
908
933
  if (step.type === 'tapIfExists') {
909
934
  const elementId = await findOptionalElement(sessionId, step);
910
935
  if (!elementId) return '未出现,已跳过';
911
- await appiumRequest(`/session/${sessionId}/element/${elementId}/click`, {
912
- method: 'POST',
913
- body: JSON.stringify({}),
914
- });
936
+ await tapLocatedElement(sessionId, deviceId, elementId, step);
915
937
  return '已出现,已点击';
916
938
  }
917
939
 
@@ -962,10 +984,7 @@ async function runStep(
962
984
  for (let attempt = 0; attempt < 2; attempt += 1) {
963
985
  try {
964
986
  const elementId = await findElement(sessionId, step);
965
- await appiumRequest(`/session/${sessionId}/element/${elementId}/click`, {
966
- method: 'POST',
967
- body: JSON.stringify({}),
968
- });
987
+ await tapLocatedElement(sessionId, deviceId, elementId, step);
969
988
  return;
970
989
  } catch (error) {
971
990
  throwIfReplayStopped(error);
@@ -1052,7 +1071,7 @@ async function evaluateCondition(sessionId: string, deviceId: string, step: Appi
1052
1071
  const element = payload.value[0]!;
1053
1072
  const id = element[ELEMENT_KEY] || element.ELEMENT;
1054
1073
  if (!id) throw new Error('文字点击查询缺少元素 ID');
1055
- await appiumRequest(`/session/${sessionId}/element/${id}/click`, { method: 'POST', body: '{}' });
1074
+ await tapLocatedElement(sessionId, deviceId, id);
1056
1075
  return true;
1057
1076
  }
1058
1077
  if (Date.now() >= deadline) {
@@ -0,0 +1,86 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import childProcess from 'node:child_process';
4
+ import { syncBuiltinESMExports } from 'node:module';
5
+ import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
6
+ import { tmpdir } from 'node:os';
7
+ import { join } from 'node:path';
8
+ import type { AppiumRecordedScriptRecord } from './repository';
9
+
10
+ test('element taps use the live element center through ADB without waiting for Appium click acknowledgement', async () => {
11
+ const root = await mkdtemp(join(tmpdir(), 'element-tap-'));
12
+ const env = { ...process.env };
13
+ const original = { execFile: childProcess.execFile, fetch: globalThis.fetch };
14
+ const appiumPaths: string[] = [];
15
+ const adbCalls: string[][] = [];
16
+ let elementRequestCount = 0;
17
+ try {
18
+ const sdk = join(root, 'sdk');
19
+ await mkdir(join(sdk, 'platform-tools'), { recursive: true });
20
+ await writeFile(join(sdk, 'platform-tools', 'adb'), '');
21
+ Object.assign(process.env, {
22
+ ANDROID_MIDSCENE_DATA_ROOT: root,
23
+ ANDROID_HOME: sdk,
24
+ ANDROID_SDK_ROOT: sdk,
25
+ APPIUM_SERVER_URL: 'http://external.invalid',
26
+ });
27
+ childProcess.execFile = ((_command, args: string[], _options, callback) => {
28
+ adbCalls.push(args);
29
+ queueMicrotask(() => callback(null, '', ''));
30
+ return {};
31
+ }) as typeof childProcess.execFile;
32
+ syncBuiltinESMExports();
33
+ globalThis.fetch = async (input, init) => {
34
+ const path = new URL(String(input)).pathname;
35
+ appiumPaths.push(`${init?.method || 'GET'} ${path}`);
36
+ if (path === '/session' && init?.method === 'POST') {
37
+ return Response.json({ value: { sessionId: 'test-session' } });
38
+ }
39
+ if (path === '/session/test-session/element' && init?.method === 'POST') {
40
+ elementRequestCount += 1;
41
+ return Response.json({ value: { 'element-6066-11e4-a52e-4f735466cecf': `button-${elementRequestCount}` } });
42
+ }
43
+ if (path === '/session/test-session/element/button-2/rect') {
44
+ return Response.json({ value: { error: 'stale element reference', message: 'element changed' } }, { status: 404 });
45
+ }
46
+ if (/\/element\/button-[13]\/rect$/.test(path)) {
47
+ return Response.json({ value: { x: 10, y: 20, width: 100, height: 60 } });
48
+ }
49
+ return Response.json({ value: null });
50
+ };
51
+ const { replayAppiumScript } = await import('./appium-runner');
52
+ const script: AppiumRecordedScriptRecord = {
53
+ id: 'tap', name: 'tap', deviceId: 'fake-device', appPackage: '', appActivity: '', createdAt: '', updatedAt: '',
54
+ steps: [{
55
+ id: 'login', type: 'tap', label: '点击登录按钮',
56
+ selector: { strategy: 'id', value: 'example:id/login', unique: true },
57
+ fallback: { strategy: 'bounds', centerX: 999, centerY: 999 },
58
+ }, {
59
+ id: 'refreshing', type: 'tap', label: '点击刷新列表中的按钮',
60
+ selector: { strategy: 'id', value: 'example:id/refreshing', unique: true },
61
+ fallback: { strategy: 'bounds', centerX: 999, centerY: 999 },
62
+ }],
63
+ };
64
+ const result = await replayAppiumScript(script, 'fake-device', undefined, undefined, {
65
+ screenshotReport: false,
66
+ reportSummaryEnabled: false,
67
+ });
68
+ assert.equal(result.success, true, result.output);
69
+ assert.ok(appiumPaths.includes('GET /session/test-session/element/button-1/rect'));
70
+ assert.ok(appiumPaths.includes('GET /session/test-session/element/button-3/rect'));
71
+ assert.equal(elementRequestCount, 3, 're-locate once after a stale element rect');
72
+ assert.equal(appiumPaths.some(path => path.endsWith('/click')), false, 'must not call the blocking Appium click endpoint');
73
+ assert.deepEqual(adbCalls.filter(args => args.includes('tap')).map(args => args.slice(-4)), [
74
+ ['input', 'tap', '60', '50'],
75
+ ['input', 'tap', '60', '50'],
76
+ ]);
77
+ } finally {
78
+ childProcess.execFile = original.execFile;
79
+ globalThis.fetch = original.fetch;
80
+ syncBuiltinESMExports();
81
+ for (const key of ['ANDROID_MIDSCENE_DATA_ROOT', 'ANDROID_HOME', 'ANDROID_SDK_ROOT', 'APPIUM_SERVER_URL']) {
82
+ if (env[key] === undefined) delete process.env[key]; else process.env[key] = env[key];
83
+ }
84
+ await rm(root, { recursive: true, force: true });
85
+ }
86
+ });