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
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
|
@@ -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
|
|
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
|
|
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
|
|
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
|
+
});
|