@delacour/warden-e2e 0.11.0 → 0.12.0
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/{chunk-ggcjzrvh.mjs → chunk-qz94hsb6.mjs} +365 -216
- package/index.mjs +5 -2
- package/package.json +2 -2
- package/preload.mjs +570 -419
- package/types/e2e/src/devtools/native-log-tail.d.ts +13 -0
- package/types/e2e/src/devtools/native-log.d.ts +14 -1
- package/types/e2e/src/runner/failure.d.ts +2 -0
- package/types/e2e/src/session.d.ts +1 -1
package/preload.mjs
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
isLocator,
|
|
40
40
|
Locator,
|
|
41
41
|
scrollUntilVisible,
|
|
42
|
+
realNativeLogDeps,
|
|
42
43
|
bunExec,
|
|
43
44
|
execError,
|
|
44
45
|
driverError,
|
|
@@ -104,12 +105,11 @@ import {
|
|
|
104
105
|
createDevicePerf,
|
|
105
106
|
spawnWarden,
|
|
106
107
|
screenshotPath,
|
|
107
|
-
realNativeLogDeps,
|
|
108
108
|
BUN_TIMEOUT_MARGIN_MS2,
|
|
109
109
|
onlyWasSeen,
|
|
110
110
|
WORKSPACE_VERSION,
|
|
111
111
|
WARDEN_E2E_VERSION2
|
|
112
|
-
} from "./chunk-
|
|
112
|
+
} from "./chunk-qz94hsb6.mjs";
|
|
113
113
|
|
|
114
114
|
// ../../packages/e2e/src/preload.ts
|
|
115
115
|
import { afterAll, setDefaultTimeout } from "bun:test";
|
|
@@ -306,35 +306,46 @@ function parseAdbDeviceList(stdout) {
|
|
|
306
306
|
const entries = [];
|
|
307
307
|
for (const raw of stdout.split(`
|
|
308
308
|
`)) {
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
continue;
|
|
313
|
-
const serial = row[1];
|
|
314
|
-
const rest = row[2];
|
|
315
|
-
const noPermissions = rest.startsWith("no permissions");
|
|
316
|
-
const state = noPermissions ? "no permissions" : rest.split(/\s+/)[0] ?? "";
|
|
317
|
-
const emulator = /^emulator-(\d+)$/.exec(serial);
|
|
318
|
-
if (emulator?.[1]) {
|
|
319
|
-
entries.push({ kind: "emulator", serial, port: Number(emulator[1]), state });
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
const attributes = noPermissions ? rest.slice(rest.lastIndexOf("]") + 1) : rest.slice(state.length);
|
|
323
|
-
const attr = (key) => new RegExp(`(?:^|\\s)${key}:(\\S+)`).exec(attributes)?.[1];
|
|
324
|
-
const networked = /^[^:\s]+:\d+$|_adb-tls-/.test(serial);
|
|
325
|
-
const model = attr("model");
|
|
326
|
-
const product = attr("product");
|
|
327
|
-
entries.push({
|
|
328
|
-
kind: "other",
|
|
329
|
-
serial,
|
|
330
|
-
state,
|
|
331
|
-
transport: networked ? "wifi" : "usb",
|
|
332
|
-
...model !== undefined ? { model } : {},
|
|
333
|
-
...product !== undefined ? { product } : {}
|
|
334
|
-
});
|
|
309
|
+
const entry = parseAdbRow(raw.trim());
|
|
310
|
+
if (entry)
|
|
311
|
+
entries.push(entry);
|
|
335
312
|
}
|
|
336
313
|
return entries;
|
|
337
314
|
}
|
|
315
|
+
function parseAdbRow(line) {
|
|
316
|
+
const row = /^(\S+)\s+(.+)$/.exec(line);
|
|
317
|
+
if (!row?.[1] || !row[2] || line.startsWith("List of devices") || line.startsWith("*"))
|
|
318
|
+
return;
|
|
319
|
+
const serial = row[1];
|
|
320
|
+
const rest = row[2];
|
|
321
|
+
const noPermissions = rest.startsWith("no permissions");
|
|
322
|
+
const state = noPermissions ? "no permissions" : rest.split(/\s+/)[0] ?? "";
|
|
323
|
+
const emulator = /^emulator-(\d+)$/.exec(serial);
|
|
324
|
+
if (emulator?.[1])
|
|
325
|
+
return { kind: "emulator", serial, port: Number(emulator[1]), state };
|
|
326
|
+
const attributes = noPermissions ? rest.slice(rest.lastIndexOf("]") + 1) : rest.slice(state.length);
|
|
327
|
+
const attr = (key) => new RegExp(`(?:^|\\s)${key}:(\\S+)`).exec(attributes)?.[1];
|
|
328
|
+
const networked = /^[^:\s]+:\d+$|_adb-tls-/.test(serial);
|
|
329
|
+
const model = attr("model");
|
|
330
|
+
const product = attr("product");
|
|
331
|
+
return {
|
|
332
|
+
kind: "other",
|
|
333
|
+
serial,
|
|
334
|
+
state,
|
|
335
|
+
transport: networked ? "wifi" : "usb",
|
|
336
|
+
...model !== undefined ? { model } : {},
|
|
337
|
+
...product !== undefined ? { product } : {}
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function physicalOf(entry, model, hwSerial) {
|
|
341
|
+
return {
|
|
342
|
+
platform: "android",
|
|
343
|
+
transport: entry.transport,
|
|
344
|
+
adbState: entry.state,
|
|
345
|
+
...hwSerial !== undefined ? { hwSerial } : {},
|
|
346
|
+
...model !== undefined ? { model } : {}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
338
349
|
function parseAdbDevices(stdout) {
|
|
339
350
|
const emulators = [];
|
|
340
351
|
for (const entry of parseAdbDeviceList(stdout)) {
|
|
@@ -663,14 +674,7 @@ function createAndroidProvider(deps, options = {}) {
|
|
|
663
674
|
return device;
|
|
664
675
|
if (model !== undefined)
|
|
665
676
|
device.profile = profileSlug(model);
|
|
666
|
-
|
|
667
|
-
device.physical = {
|
|
668
|
-
platform: "android",
|
|
669
|
-
transport: entry.transport,
|
|
670
|
-
adbState: entry.state,
|
|
671
|
-
...hwSerial !== undefined ? { hwSerial } : {},
|
|
672
|
-
...model !== undefined ? { model } : {}
|
|
673
|
-
};
|
|
677
|
+
device.physical = physicalOf(entry, model, info?.hwSerial);
|
|
674
678
|
return device;
|
|
675
679
|
}
|
|
676
680
|
async function avdNameOf(serial) {
|
|
@@ -824,6 +828,37 @@ function createAndroidProvider(deps, options = {}) {
|
|
|
824
828
|
deps.log?.(`${serial}: launch from snapshot ${snapshot} failed (${launched.error}) \u2014 retrying with a cold boot`);
|
|
825
829
|
return attempt(undefined);
|
|
826
830
|
}
|
|
831
|
+
async function refuseBootingPhone(id) {
|
|
832
|
+
const listed = await listAdbDevices(deps);
|
|
833
|
+
const state = listed.success ? listed.data.find((e) => e.serial === id)?.state : undefined;
|
|
834
|
+
if (state === "unauthorized")
|
|
835
|
+
return err(`${id} is unauthorized: accept the USB debugging prompt on the phone, then retry`);
|
|
836
|
+
return err(`${id} is a physical device: warden never boots one (state: ${state ?? "not connected"})`);
|
|
837
|
+
}
|
|
838
|
+
async function pollRegistered(id, polls) {
|
|
839
|
+
for (let i = 0;; i++) {
|
|
840
|
+
const emulators = await listEmulators(deps);
|
|
841
|
+
if (!emulators.success)
|
|
842
|
+
return emulators;
|
|
843
|
+
if (emulators.data.some((e) => e.serial === id))
|
|
844
|
+
return ok(true);
|
|
845
|
+
if (i >= polls)
|
|
846
|
+
return ok(false);
|
|
847
|
+
await sleep(REGISTER_POLL_MS);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
async function pollGone(id, killError) {
|
|
851
|
+
const deadline = deps.now() + RELAUNCH_GONE_TIMEOUT_MS;
|
|
852
|
+
for (;; ) {
|
|
853
|
+
const listed = await listEmulators(deps);
|
|
854
|
+
if (listed.success && !listed.data.some((e) => e.serial === id))
|
|
855
|
+
return ok(undefined);
|
|
856
|
+
if (deps.now() >= deadline) {
|
|
857
|
+
return err(`${id} did not shut down within ${RELAUNCH_GONE_TIMEOUT_MS / 1000}s${killError === undefined ? "" : `: ${killError}`}`);
|
|
858
|
+
}
|
|
859
|
+
await sleep(REGISTER_POLL_MS);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
827
862
|
return {
|
|
828
863
|
platform: "android",
|
|
829
864
|
async inventory() {
|
|
@@ -878,25 +913,15 @@ function createAndroidProvider(deps, options = {}) {
|
|
|
878
913
|
return ok(device);
|
|
879
914
|
},
|
|
880
915
|
async boot(id) {
|
|
881
|
-
if (!isEmulatorSerial(id))
|
|
882
|
-
|
|
883
|
-
const state = listed.success ? listed.data.find((e) => e.serial === id)?.state : undefined;
|
|
884
|
-
if (state === "unauthorized")
|
|
885
|
-
return err(`${id} is unauthorized: accept the USB debugging prompt on the phone, then retry`);
|
|
886
|
-
return err(`${id} is a physical device: warden never boots one (state: ${state ?? "not connected"})`);
|
|
887
|
-
}
|
|
916
|
+
if (!isEmulatorSerial(id))
|
|
917
|
+
return refuseBootingPhone(id);
|
|
888
918
|
const launched = deps.store.listDevices("android").some((d) => d.id === id);
|
|
889
919
|
const polls = launched ? Math.ceil(REGISTER_TIMEOUT_MS / REGISTER_POLL_MS) : 0;
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
return ok(undefined);
|
|
896
|
-
if (i >= polls)
|
|
897
|
-
break;
|
|
898
|
-
await sleep(REGISTER_POLL_MS);
|
|
899
|
-
}
|
|
920
|
+
const registered = await pollRegistered(id, polls);
|
|
921
|
+
if (!registered.success)
|
|
922
|
+
return registered;
|
|
923
|
+
if (registered.data)
|
|
924
|
+
return ok(undefined);
|
|
900
925
|
return launched ? err(`${id} did not register with adb within ${REGISTER_TIMEOUT_MS / 1000}s of its launch`) : err(`${id} is not running \u2014 android devices are created, not booted`);
|
|
901
926
|
},
|
|
902
927
|
async waitReady(id, timeoutMs) {
|
|
@@ -936,16 +961,9 @@ function createAndroidProvider(deps, options = {}) {
|
|
|
936
961
|
if (!resolved.success)
|
|
937
962
|
return resolved;
|
|
938
963
|
const killed = await run(deps, adb(id, "emu", "kill"), { timeoutMs: EMU_CONSOLE_TIMEOUT_MS });
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
if (listed.success && !listed.data.some((e) => e.serial === id))
|
|
943
|
-
break;
|
|
944
|
-
if (deps.now() >= deadline) {
|
|
945
|
-
return err(`${id} did not shut down within ${RELAUNCH_GONE_TIMEOUT_MS / 1000}s${killed.success ? "" : `: ${killed.error}`}`);
|
|
946
|
-
}
|
|
947
|
-
await sleep(REGISTER_POLL_MS);
|
|
948
|
-
}
|
|
964
|
+
const gone = await pollGone(id, killed.success ? undefined : killed.error);
|
|
965
|
+
if (!gone.success)
|
|
966
|
+
return gone;
|
|
949
967
|
await sleep(RELAUNCH_PORT_SETTLE_MS);
|
|
950
968
|
const launched = await launchOrColdBoot(resolved.data.avdName, port, id, undefined, true);
|
|
951
969
|
if (!launched.success)
|
|
@@ -2107,6 +2125,26 @@ async function preparePhone(session, clock, facts, log) {
|
|
|
2107
2125
|
return { success: true, data: undefined };
|
|
2108
2126
|
}
|
|
2109
2127
|
var SETTING_VALUE = /^(null|\d)$/;
|
|
2128
|
+
function restoreCommands(rotation, uimode) {
|
|
2129
|
+
const commands = [];
|
|
2130
|
+
if (rotation) {
|
|
2131
|
+
for (const key of ["accelerometer_rotation", "user_rotation"]) {
|
|
2132
|
+
commands.push(rotation[key] === "null" ? ["settings", "delete", "system", key] : ["settings", "put", "system", key, rotation[key]]);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
if (uimode)
|
|
2136
|
+
commands.push(["cmd", "uimode", "night", uimode]);
|
|
2137
|
+
return commands;
|
|
2138
|
+
}
|
|
2139
|
+
async function runAll(session, commands) {
|
|
2140
|
+
let failed = false;
|
|
2141
|
+
for (const argv of commands) {
|
|
2142
|
+
const res = await session.run(shellCommand(argv));
|
|
2143
|
+
if (!res.success || res.data.exitCode !== 0)
|
|
2144
|
+
failed = true;
|
|
2145
|
+
}
|
|
2146
|
+
return failed;
|
|
2147
|
+
}
|
|
2110
2148
|
async function createSettingsGuard(deps) {
|
|
2111
2149
|
const { session, serial, sdk, state, log } = deps;
|
|
2112
2150
|
let rotation = deps.pending.rotation;
|
|
@@ -2142,22 +2180,10 @@ async function createSettingsGuard(deps) {
|
|
|
2142
2180
|
return { success: true, data: undefined };
|
|
2143
2181
|
},
|
|
2144
2182
|
async restore() {
|
|
2145
|
-
const commands =
|
|
2146
|
-
if (rotation) {
|
|
2147
|
-
for (const key of ["accelerometer_rotation", "user_rotation"]) {
|
|
2148
|
-
commands.push(rotation[key] === "null" ? ["settings", "delete", "system", key] : ["settings", "put", "system", key, rotation[key]]);
|
|
2149
|
-
}
|
|
2150
|
-
}
|
|
2151
|
-
if (uimode)
|
|
2152
|
-
commands.push(["cmd", "uimode", "night", uimode]);
|
|
2183
|
+
const commands = restoreCommands(rotation, uimode);
|
|
2153
2184
|
if (commands.length === 0)
|
|
2154
2185
|
return;
|
|
2155
|
-
|
|
2156
|
-
for (const argv of commands) {
|
|
2157
|
-
const res = await session.run(shellCommand(argv));
|
|
2158
|
-
if (!res.success || res.data.exitCode !== 0)
|
|
2159
|
-
failed = true;
|
|
2160
|
-
}
|
|
2186
|
+
const failed = await runAll(session, commands);
|
|
2161
2187
|
if (failed) {
|
|
2162
2188
|
log?.("could not restore the rotation / night mode; the next driver on this device will");
|
|
2163
2189
|
return;
|
|
@@ -2487,63 +2513,50 @@ function createAndroidCamera(deps) {
|
|
|
2487
2513
|
const staged = [];
|
|
2488
2514
|
const mode = {};
|
|
2489
2515
|
for (const facing of CAMERA_FACINGS) {
|
|
2490
|
-
const
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
if (feed === undefined || feed.kind === "video") {
|
|
2494
|
-
staged.push({ tmp: tmpPng, final: png });
|
|
2495
|
-
writeFileSync3(tmpPng, PLACEHOLDER_PNG);
|
|
2496
|
-
}
|
|
2497
|
-
if (feed === undefined)
|
|
2498
|
-
continue;
|
|
2499
|
-
const sniffed = sniffCameraMedia(feed.path);
|
|
2500
|
-
if (!sniffed.success)
|
|
2501
|
-
return driverErr("E_BAD_REQUEST", sniffed.error.message);
|
|
2502
|
-
if (sniffed.data.kind !== feed.kind) {
|
|
2503
|
-
return driverErr("E_BAD_REQUEST", `${feed.path} is ${sniffed.data.kind} media, not a ${feed.kind} feed`);
|
|
2504
|
-
}
|
|
2505
|
-
if (feed.kind === "video") {
|
|
2506
|
-
const name = `${facing}${extname(feed.path).toLowerCase() || ".mp4"}`;
|
|
2507
|
-
const tmp = join5(dir, `.${name}.tmp-${process.pid}`);
|
|
2508
|
-
copyFileSync(feed.path, tmp);
|
|
2509
|
-
staged.push({ tmp, final: join5(dir, name) });
|
|
2510
|
-
mode[facing] = name;
|
|
2511
|
-
continue;
|
|
2512
|
-
}
|
|
2513
|
-
staged.push({ tmp: tmpPng, final: png });
|
|
2514
|
-
if (sniffed.data.ext === "png") {
|
|
2515
|
-
copyFileSync(feed.path, tmpPng);
|
|
2516
|
-
continue;
|
|
2517
|
-
}
|
|
2518
|
-
const converted = await reencode(feed.path, tmpPng);
|
|
2519
|
-
if (!converted.success)
|
|
2520
|
-
return converted;
|
|
2516
|
+
const failed = await prepareFacing(facing, feeds[facing], staged, mode);
|
|
2517
|
+
if (failed)
|
|
2518
|
+
return failed;
|
|
2521
2519
|
}
|
|
2522
2520
|
return { success: true, data: { staged, mode } };
|
|
2523
2521
|
}
|
|
2522
|
+
async function prepareFacing(facing, feed, staged, mode) {
|
|
2523
|
+
const png = join5(dir, `${facing}.png`);
|
|
2524
|
+
const tmpPng = join5(dir, `.${facing}.png.tmp-${process.pid}`);
|
|
2525
|
+
if (feed === undefined || feed.kind === "video") {
|
|
2526
|
+
staged.push({ tmp: tmpPng, final: png });
|
|
2527
|
+
writeFileSync3(tmpPng, PLACEHOLDER_PNG);
|
|
2528
|
+
}
|
|
2529
|
+
if (feed === undefined)
|
|
2530
|
+
return;
|
|
2531
|
+
const sniffed = sniffCameraMedia(feed.path);
|
|
2532
|
+
if (!sniffed.success)
|
|
2533
|
+
return driverErr("E_BAD_REQUEST", sniffed.error.message);
|
|
2534
|
+
if (sniffed.data.kind !== feed.kind) {
|
|
2535
|
+
return driverErr("E_BAD_REQUEST", `${feed.path} is ${sniffed.data.kind} media, not a ${feed.kind} feed`);
|
|
2536
|
+
}
|
|
2537
|
+
if (feed.kind === "video")
|
|
2538
|
+
return stageVideo(facing, feed.path, staged, mode);
|
|
2539
|
+
staged.push({ tmp: tmpPng, final: png });
|
|
2540
|
+
if (sniffed.data.ext === "png") {
|
|
2541
|
+
copyFileSync(feed.path, tmpPng);
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
const converted = await reencode(feed.path, tmpPng);
|
|
2545
|
+
return converted.success ? undefined : converted;
|
|
2546
|
+
}
|
|
2547
|
+
function stageVideo(facing, path, staged, mode) {
|
|
2548
|
+
const name = `${facing}${extname(path).toLowerCase() || ".mp4"}`;
|
|
2549
|
+
const tmp = join5(dir, `.${name}.tmp-${process.pid}`);
|
|
2550
|
+
copyFileSync(path, tmp);
|
|
2551
|
+
staged.push({ tmp, final: join5(dir, name) });
|
|
2552
|
+
mode[facing] = name;
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2524
2555
|
function discard(staged) {
|
|
2525
2556
|
for (const { tmp } of staged)
|
|
2526
2557
|
rmSync3(tmp, { force: true });
|
|
2527
2558
|
}
|
|
2528
|
-
|
|
2529
|
-
const profile = deps.profile();
|
|
2530
|
-
if (profile === undefined || !splitCameraProfile(profile).camera) {
|
|
2531
|
-
return driverErr("E_CAMERA_DEVICE", `${deps.serial} is not a camera emulator: its camera source is fixed when it launches, so claim it with --camera (profile \`camera: true\`)`);
|
|
2532
|
-
}
|
|
2533
|
-
const feeds = op ?? {};
|
|
2534
|
-
const before = readCameraMode(dir);
|
|
2535
|
-
let prepared;
|
|
2536
|
-
try {
|
|
2537
|
-
mkdirSync4(dir, { recursive: true });
|
|
2538
|
-
prepared = await prepare(feeds);
|
|
2539
|
-
} catch (error) {
|
|
2540
|
-
return driverErr("E_IO", `camera feed: ${message(error)}`);
|
|
2541
|
-
}
|
|
2542
|
-
if (!prepared.success) {
|
|
2543
|
-
rmTemps(dir);
|
|
2544
|
-
return prepared;
|
|
2545
|
-
}
|
|
2546
|
-
const { staged, mode } = prepared.data;
|
|
2559
|
+
function commit(before, staged, mode) {
|
|
2547
2560
|
try {
|
|
2548
2561
|
for (const { tmp, final } of staged)
|
|
2549
2562
|
renameSync3(tmp, final);
|
|
@@ -2553,19 +2566,48 @@ function createAndroidCamera(deps) {
|
|
|
2553
2566
|
rmSync3(join5(dir, old), { force: true });
|
|
2554
2567
|
}
|
|
2555
2568
|
writeCameraMode(dir, mode);
|
|
2569
|
+
return;
|
|
2556
2570
|
} catch (error) {
|
|
2557
2571
|
discard(staged);
|
|
2558
2572
|
return driverErr("E_IO", `camera feed: ${message(error)}`);
|
|
2559
2573
|
}
|
|
2574
|
+
}
|
|
2575
|
+
async function prepareGuarded(feeds) {
|
|
2576
|
+
try {
|
|
2577
|
+
mkdirSync4(dir, { recursive: true });
|
|
2578
|
+
return await prepare(feeds);
|
|
2579
|
+
} catch (error) {
|
|
2580
|
+
return driverErr("E_IO", `camera feed: ${message(error)}`);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
async function relaunchForMode(before, mode) {
|
|
2584
|
+
const restarted = await deps.relaunch();
|
|
2585
|
+
if (restarted.success)
|
|
2586
|
+
return;
|
|
2587
|
+
return driverErr("E_NOT_BOOTED", `restarting ${deps.serial} to switch its camera between ${usesVideo(before) ? "video" : "image"} and ${usesVideo(mode) ? "video" : "image"} failed: ${restarted.error}`);
|
|
2588
|
+
}
|
|
2589
|
+
return async (op) => {
|
|
2590
|
+
const profile = deps.profile();
|
|
2591
|
+
if (profile === undefined || !splitCameraProfile(profile).camera) {
|
|
2592
|
+
return driverErr("E_CAMERA_DEVICE", `${deps.serial} is not a camera emulator: its camera source is fixed when it launches, so claim it with --camera (profile \`camera: true\`)`);
|
|
2593
|
+
}
|
|
2594
|
+
const feeds = op ?? {};
|
|
2595
|
+
const before = readCameraMode(dir);
|
|
2596
|
+
const prepared = await prepareGuarded(feeds);
|
|
2597
|
+
if (!prepared.success) {
|
|
2598
|
+
rmTemps(dir);
|
|
2599
|
+
return prepared;
|
|
2600
|
+
}
|
|
2601
|
+
const { staged, mode } = prepared.data;
|
|
2602
|
+
const committed = commit(before, staged, mode);
|
|
2603
|
+
if (committed)
|
|
2604
|
+
return committed;
|
|
2560
2605
|
deps.record?.(feeds);
|
|
2561
2606
|
const state = { feeds };
|
|
2562
2607
|
if (sameCameraMode(before, mode))
|
|
2563
2608
|
return { success: true, data: { ...state, appliesOnCameraOpen: true, rebooted: false } };
|
|
2564
|
-
const restarted = await
|
|
2565
|
-
|
|
2566
|
-
return driverErr("E_NOT_BOOTED", `restarting ${deps.serial} to switch its camera between ${usesVideo(before) ? "video" : "image"} and ${usesVideo(mode) ? "video" : "image"} failed: ${restarted.error}`);
|
|
2567
|
-
}
|
|
2568
|
-
return { success: true, data: { ...state, rebooted: true } };
|
|
2609
|
+
const restarted = await relaunchForMode(before, mode);
|
|
2610
|
+
return restarted ?? { success: true, data: { ...state, rebooted: true } };
|
|
2569
2611
|
};
|
|
2570
2612
|
}
|
|
2571
2613
|
function rmTemps(dir) {
|
|
@@ -3911,67 +3953,84 @@ var SWITCHES = [
|
|
|
3911
3953
|
["highContrastText", "secure", "high_text_contrast_enabled"],
|
|
3912
3954
|
["colorInversion", "secure", "accessibility_display_inversion_enabled"]
|
|
3913
3955
|
];
|
|
3956
|
+
var stepFontScale = async (c, op, run) => {
|
|
3957
|
+
if (op !== null && op.textSize === undefined)
|
|
3958
|
+
return;
|
|
3959
|
+
const font = fontScaleOf(op?.textSize);
|
|
3960
|
+
if ("success" in font)
|
|
3961
|
+
return font;
|
|
3962
|
+
run.notes = font.notes;
|
|
3963
|
+
const done = await c.putSetting("system", "font_scale", String(font.scale));
|
|
3964
|
+
if (!done.success)
|
|
3965
|
+
return done;
|
|
3966
|
+
run.applied.push("textSize");
|
|
3967
|
+
return;
|
|
3968
|
+
};
|
|
3969
|
+
var stepBoldText = async (c, op, run) => {
|
|
3970
|
+
if (op !== null && op.boldText === undefined)
|
|
3971
|
+
return;
|
|
3972
|
+
const done = await c.putSetting("secure", "font_weight_adjustment", String(op?.boldText === true ? BOLD_TEXT_ADJUSTMENT : 0));
|
|
3973
|
+
if (!done.success)
|
|
3974
|
+
return done;
|
|
3975
|
+
run.applied.push("boldText");
|
|
3976
|
+
return;
|
|
3977
|
+
};
|
|
3978
|
+
var stepSwitches = async (c, op, run) => {
|
|
3979
|
+
for (const [field, table, key] of SWITCHES) {
|
|
3980
|
+
if (op !== null && op[field] === undefined)
|
|
3981
|
+
continue;
|
|
3982
|
+
const done = await c.putSetting(table, key, op?.[field] === true ? "1" : "0");
|
|
3983
|
+
if (!done.success)
|
|
3984
|
+
return done;
|
|
3985
|
+
if (op !== null)
|
|
3986
|
+
run.applied.push(field);
|
|
3987
|
+
}
|
|
3988
|
+
return;
|
|
3989
|
+
};
|
|
3990
|
+
var stepReduceMotion = async (c, op, run) => {
|
|
3991
|
+
if (op !== null && op.reduceMotion === undefined)
|
|
3992
|
+
return;
|
|
3993
|
+
const done = await c.putSetting("global", "transition_animation_scale", op?.reduceMotion === true ? "0" : transitionOff(c));
|
|
3994
|
+
if (!done.success)
|
|
3995
|
+
return done;
|
|
3996
|
+
if (op !== null)
|
|
3997
|
+
run.applied.push("reduceMotion");
|
|
3998
|
+
return;
|
|
3999
|
+
};
|
|
4000
|
+
var stepScreenReader = async (c, op, run) => {
|
|
4001
|
+
if (op !== null && op.screenReader === undefined)
|
|
4002
|
+
return;
|
|
4003
|
+
const done = await (op?.screenReader === true ? c.talkback.enable() : c.talkback.disable());
|
|
4004
|
+
if (!done.success)
|
|
4005
|
+
return done;
|
|
4006
|
+
if (op !== null) {
|
|
4007
|
+
run.screenReader = done.data;
|
|
4008
|
+
run.applied.push("screenReader");
|
|
4009
|
+
run.screenReaderPrevious = c.talkback.previous();
|
|
4010
|
+
}
|
|
4011
|
+
return;
|
|
4012
|
+
};
|
|
4013
|
+
var A11Y_STEPS = [stepFontScale, stepBoldText, stepSwitches, stepReduceMotion, stepScreenReader];
|
|
3914
4014
|
function a11yOp(c) {
|
|
3915
4015
|
return async (op) => {
|
|
3916
|
-
const
|
|
3917
|
-
const
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
if ("success" in font)
|
|
3922
|
-
return font;
|
|
3923
|
-
notes = font.notes;
|
|
3924
|
-
const done = await c.putSetting("system", "font_scale", String(font.scale));
|
|
3925
|
-
if (!done.success)
|
|
3926
|
-
return done;
|
|
3927
|
-
applied.push("textSize");
|
|
3928
|
-
}
|
|
3929
|
-
if (op === null || op.boldText !== undefined) {
|
|
3930
|
-
const done = await c.putSetting("secure", "font_weight_adjustment", String(op?.boldText === true ? BOLD_TEXT_ADJUSTMENT : 0));
|
|
3931
|
-
if (!done.success)
|
|
3932
|
-
return done;
|
|
3933
|
-
applied.push("boldText");
|
|
3934
|
-
}
|
|
3935
|
-
for (const [field, table, key] of SWITCHES) {
|
|
3936
|
-
if (op === null || op[field] !== undefined) {
|
|
3937
|
-
const done = await c.putSetting(table, key, op?.[field] === true ? "1" : "0");
|
|
3938
|
-
if (!done.success)
|
|
3939
|
-
return done;
|
|
3940
|
-
if (op !== null)
|
|
3941
|
-
applied.push(field);
|
|
3942
|
-
}
|
|
3943
|
-
}
|
|
3944
|
-
if (op === null || op.reduceMotion !== undefined) {
|
|
3945
|
-
const done = await c.putSetting("global", "transition_animation_scale", op?.reduceMotion === true ? "0" : transitionOff(c));
|
|
3946
|
-
if (!done.success)
|
|
3947
|
-
return done;
|
|
3948
|
-
if (op !== null)
|
|
3949
|
-
applied.push("reduceMotion");
|
|
3950
|
-
}
|
|
3951
|
-
let screenReader;
|
|
3952
|
-
let screenReaderPrevious;
|
|
3953
|
-
if (op === null || op.screenReader !== undefined) {
|
|
3954
|
-
const done = await (op?.screenReader === true ? c.talkback.enable() : c.talkback.disable());
|
|
3955
|
-
if (!done.success)
|
|
3956
|
-
return done;
|
|
3957
|
-
if (op !== null) {
|
|
3958
|
-
screenReader = done.data;
|
|
3959
|
-
applied.push("screenReader");
|
|
3960
|
-
screenReaderPrevious = c.talkback.previous();
|
|
3961
|
-
}
|
|
4016
|
+
const run = { applied: [], notes: [] };
|
|
4017
|
+
for (const step of A11Y_STEPS) {
|
|
4018
|
+
const failed = await step(c, op, run);
|
|
4019
|
+
if (failed)
|
|
4020
|
+
return failed;
|
|
3962
4021
|
}
|
|
3963
|
-
skipped
|
|
3964
|
-
if (applied.length === 0 && skipped.length > 0) {
|
|
4022
|
+
const skipped = a11ySkips(op);
|
|
4023
|
+
if (run.applied.length === 0 && skipped.length > 0) {
|
|
3965
4024
|
return unsupported(skipped.map((s) => ({ name: s.setting, reason: s.reason })));
|
|
3966
4025
|
}
|
|
3967
4026
|
return {
|
|
3968
4027
|
success: true,
|
|
3969
4028
|
data: {
|
|
3970
|
-
applied,
|
|
4029
|
+
applied: run.applied,
|
|
3971
4030
|
skipped,
|
|
3972
|
-
notes,
|
|
3973
|
-
...screenReader === undefined ? {} : { screenReader },
|
|
3974
|
-
...screenReaderPrevious === undefined ? {} : { screenReaderPrevious }
|
|
4031
|
+
notes: run.notes,
|
|
4032
|
+
...run.screenReader === undefined ? {} : { screenReader: run.screenReader },
|
|
4033
|
+
...run.screenReaderPrevious === undefined ? {} : { screenReaderPrevious: run.screenReaderPrevious }
|
|
3975
4034
|
}
|
|
3976
4035
|
};
|
|
3977
4036
|
};
|
|
@@ -3982,6 +4041,14 @@ function batteryRequestProblem(op) {
|
|
|
3982
4041
|
return bad;
|
|
3983
4042
|
return batteryArgvs(op).length === 0 ? driverErr("E_BAD_REQUEST", "battery needs a level, charging, or both") : undefined;
|
|
3984
4043
|
}
|
|
4044
|
+
async function applyBattery(c, op) {
|
|
4045
|
+
for (const cmd of batteryArgvs(op)) {
|
|
4046
|
+
const done = await c.must(cmd);
|
|
4047
|
+
if (!done.success)
|
|
4048
|
+
return done;
|
|
4049
|
+
}
|
|
4050
|
+
return;
|
|
4051
|
+
}
|
|
3985
4052
|
function batteryOp(c) {
|
|
3986
4053
|
return async (op) => {
|
|
3987
4054
|
if (!c.serial.startsWith("emulator-")) {
|
|
@@ -3994,11 +4061,9 @@ function batteryOp(c) {
|
|
|
3994
4061
|
const bad = batteryRequestProblem(op);
|
|
3995
4062
|
if (bad)
|
|
3996
4063
|
return bad;
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
return done;
|
|
4001
|
-
}
|
|
4064
|
+
const failed = await applyBattery(c, op);
|
|
4065
|
+
if (failed)
|
|
4066
|
+
return failed;
|
|
4002
4067
|
return {
|
|
4003
4068
|
success: true,
|
|
4004
4069
|
data: {
|
|
@@ -5156,21 +5221,25 @@ function createUiadOps(deps) {
|
|
|
5156
5221
|
await backend.failed(error);
|
|
5157
5222
|
return neverSent(error) && canRetry ? "retry" : "surface";
|
|
5158
5223
|
}
|
|
5224
|
+
async function attemptCall(op, use, idempotent, surfaceRefusals, canRetry) {
|
|
5225
|
+
const server = await backend.server();
|
|
5226
|
+
if (server === undefined)
|
|
5227
|
+
return { retry: false, outcome: undefined };
|
|
5228
|
+
const res = await use(server);
|
|
5229
|
+
if (res.success)
|
|
5230
|
+
return { retry: false, outcome: res };
|
|
5231
|
+
if (surfaceRefusals && REFUSALS.has(res.error.code))
|
|
5232
|
+
return { retry: false, outcome: res };
|
|
5233
|
+
const verdict = await judge(op, res.error, canRetry);
|
|
5234
|
+
if (verdict === "retry")
|
|
5235
|
+
return { retry: true };
|
|
5236
|
+
return { retry: false, outcome: verdict === "surface" && !idempotent ? res : undefined };
|
|
5237
|
+
}
|
|
5159
5238
|
async function call(op, use, idempotent, surfaceRefusals = false) {
|
|
5160
5239
|
for (let attempt = 0;attempt < 2; attempt++) {
|
|
5161
|
-
const
|
|
5162
|
-
if (
|
|
5163
|
-
return;
|
|
5164
|
-
const res = await use(server);
|
|
5165
|
-
if (res.success)
|
|
5166
|
-
return res;
|
|
5167
|
-
if (surfaceRefusals && REFUSALS.has(res.error.code))
|
|
5168
|
-
return res;
|
|
5169
|
-
const verdict = await judge(op, res.error, attempt === 0);
|
|
5170
|
-
if (verdict === "surface" && !idempotent)
|
|
5171
|
-
return res;
|
|
5172
|
-
if (verdict !== "retry")
|
|
5173
|
-
return;
|
|
5240
|
+
const tried = await attemptCall(op, use, idempotent, surfaceRefusals, attempt === 0);
|
|
5241
|
+
if (!tried.retry)
|
|
5242
|
+
return tried.outcome;
|
|
5174
5243
|
}
|
|
5175
5244
|
return;
|
|
5176
5245
|
}
|
|
@@ -5336,6 +5405,27 @@ function elementAt(flat, point) {
|
|
|
5336
5405
|
const deepest = (list) => list.reduce((best, e) => best === undefined || e.depth >= best.depth ? e : best, undefined);
|
|
5337
5406
|
return deepest(inside.filter((e) => e.hittable)) ?? deepest(inside) ?? null;
|
|
5338
5407
|
}
|
|
5408
|
+
async function bootAndProbe(options, session, clock) {
|
|
5409
|
+
const { serial, log } = options;
|
|
5410
|
+
const booted = await waitForBoot(session, serial, clock, {
|
|
5411
|
+
...options.bootTimeoutMs !== undefined ? { timeoutMs: options.bootTimeoutMs } : {},
|
|
5412
|
+
...log ? { log } : {}
|
|
5413
|
+
});
|
|
5414
|
+
if (!booted.success)
|
|
5415
|
+
return booted;
|
|
5416
|
+
const sdk = booted.data.sdk;
|
|
5417
|
+
const phone = booted.data.physical;
|
|
5418
|
+
if (phone) {
|
|
5419
|
+
const awake = await preparePhone(session, clock, phone, log);
|
|
5420
|
+
if (!awake.success)
|
|
5421
|
+
return awake;
|
|
5422
|
+
}
|
|
5423
|
+
const probed = options.screen ?? booted.data.screen;
|
|
5424
|
+
const screen = probed ? { success: true, data: probed } : await readScreen(session, serial);
|
|
5425
|
+
if (!screen.success)
|
|
5426
|
+
return screen;
|
|
5427
|
+
return { success: true, data: { sdk, phone, screen: screen.data } };
|
|
5428
|
+
}
|
|
5339
5429
|
async function openAndroidDriver(options) {
|
|
5340
5430
|
const { serial, adb, log } = options;
|
|
5341
5431
|
const clock = options.clock ?? realClock;
|
|
@@ -5351,23 +5441,10 @@ async function openAndroidDriver(options) {
|
|
|
5351
5441
|
return { success: false, error };
|
|
5352
5442
|
};
|
|
5353
5443
|
const savedState = options.state?.read(serial);
|
|
5354
|
-
const
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
}
|
|
5358
|
-
if (!booted.success)
|
|
5359
|
-
return failed(booted.error);
|
|
5360
|
-
const sdk = booted.data.sdk;
|
|
5361
|
-
const phone = booted.data.physical;
|
|
5362
|
-
if (phone) {
|
|
5363
|
-
const awake = await preparePhone(session, clock, phone, log);
|
|
5364
|
-
if (!awake.success)
|
|
5365
|
-
return failed(awake.error);
|
|
5366
|
-
}
|
|
5367
|
-
const probed = options.screen ?? booted.data.screen;
|
|
5368
|
-
const screen = probed ? { success: true, data: probed } : await readScreen(session, serial);
|
|
5369
|
-
if (!screen.success)
|
|
5370
|
-
return failed(screen.error);
|
|
5444
|
+
const probed = await bootAndProbe(options, session, clock);
|
|
5445
|
+
if (!probed.success)
|
|
5446
|
+
return failed(probed.error);
|
|
5447
|
+
const { sdk, phone, screen } = probed.data;
|
|
5371
5448
|
const saved = forSdk(await savedState ?? {}, sdk);
|
|
5372
5449
|
const animations = await disableAnimations({
|
|
5373
5450
|
session,
|
|
@@ -5387,7 +5464,7 @@ async function openAndroidDriver(options) {
|
|
|
5387
5464
|
}) : undefined;
|
|
5388
5465
|
return {
|
|
5389
5466
|
success: true,
|
|
5390
|
-
data: buildDriver(options, session, screen
|
|
5467
|
+
data: buildDriver(options, session, screen, clock, execBinary, {
|
|
5391
5468
|
sdk,
|
|
5392
5469
|
savedRoute: saved.dumpRoute,
|
|
5393
5470
|
savedUiadUnsupported: saved.uiadUnsupported,
|
|
@@ -5425,12 +5502,72 @@ function openUiad(input) {
|
|
|
5425
5502
|
atf: () => ensureAtfJar({ session, adb, serial, ...log ? { log } : {} })
|
|
5426
5503
|
});
|
|
5427
5504
|
}
|
|
5428
|
-
function
|
|
5505
|
+
async function dumpWithRetry(dumper, clock, log) {
|
|
5506
|
+
let dumped = await dumper.dump();
|
|
5507
|
+
for (let attempt = 1;attempt < SNAPSHOT_ATTEMPTS2 && !dumped.success && dumped.error.code === "E_AX_UNAVAILABLE"; attempt++) {
|
|
5508
|
+
log?.(`uiautomator unavailable, retrying (${attempt}/${SNAPSHOT_ATTEMPTS2 - 1})`);
|
|
5509
|
+
await clock.sleep(SNAPSHOT_RETRY_MS2);
|
|
5510
|
+
dumped = await dumper.dump();
|
|
5511
|
+
}
|
|
5512
|
+
return dumped;
|
|
5513
|
+
}
|
|
5514
|
+
function parseDump(xml, baseScreen, clock) {
|
|
5515
|
+
const rotation = hierarchyRotation(xml);
|
|
5516
|
+
const screen = rotatedScreen(baseScreen, rotation);
|
|
5517
|
+
let parsed;
|
|
5518
|
+
try {
|
|
5519
|
+
parsed = parseUiautomatorXml(xml, screen);
|
|
5520
|
+
} catch (error) {
|
|
5521
|
+
if (error instanceof UiautomatorParseError)
|
|
5522
|
+
return driverErr("E_AX_UNAVAILABLE", error.message);
|
|
5523
|
+
throw error;
|
|
5524
|
+
}
|
|
5525
|
+
return {
|
|
5526
|
+
success: true,
|
|
5527
|
+
data: {
|
|
5528
|
+
platform: "android",
|
|
5529
|
+
backend: "uiautomator",
|
|
5530
|
+
takenAt: clock.now(),
|
|
5531
|
+
screen,
|
|
5532
|
+
...parsed,
|
|
5533
|
+
truncated: false
|
|
5534
|
+
}
|
|
5535
|
+
};
|
|
5536
|
+
}
|
|
5537
|
+
async function touchSensor(exec, adb, serial, fingerId) {
|
|
5538
|
+
const touched = await exec(emuFingerTouchArgv(adb, serial, fingerId), { timeoutMs: 1e4 });
|
|
5539
|
+
if (touched.exitCode === 0 && !/^KO\b/m.test(touched.stdout))
|
|
5540
|
+
return { success: true, data: undefined };
|
|
5541
|
+
return driverErr("E_IO", `adb emu finger touch failed: ${`${touched.stdout}${touched.stderr}`.trim().slice(0, 200)}`);
|
|
5542
|
+
}
|
|
5543
|
+
function biometricsOp(session, exec, adb, serial) {
|
|
5544
|
+
return async (op) => {
|
|
5545
|
+
if (!isEmulatorSerial2(serial)) {
|
|
5546
|
+
return driverErr("E_UNSUPPORTED", `biometrics need an emulator, not the physical device ${serial}`);
|
|
5547
|
+
}
|
|
5548
|
+
if (op === "enroll" || op === "unenroll") {
|
|
5549
|
+
return driverErr("E_UNSUPPORTED", `Android fingerprint enrollment comes from the golden snapshot (profile \`biometrics: true\`); \`${op}\` is not available at run time`);
|
|
5550
|
+
}
|
|
5551
|
+
if (op !== "status") {
|
|
5552
|
+
const touched = await touchSensor(exec, adb, serial, op === "match" ? 1 : UNENROLLED_FINGER_ID);
|
|
5553
|
+
if (!touched.success)
|
|
5554
|
+
return touched;
|
|
5555
|
+
}
|
|
5556
|
+
const dump = await runChecked(session, shellCommand(dumpsysFingerprintArgv()), "E_UNSUPPORTED");
|
|
5557
|
+
if (!dump.success)
|
|
5558
|
+
return dump;
|
|
5559
|
+
const state = { enrolled: parseFingerprintEnrolled(dump.data), modality: "fingerprint" };
|
|
5560
|
+
return { success: true, data: state };
|
|
5561
|
+
};
|
|
5562
|
+
}
|
|
5563
|
+
function capabilitiesOf(serverOn, shellPointers) {
|
|
5564
|
+
return serverOn ? { ...CAPABILITIES, pinch: true, maxPointers: 2, unicodeText: true, nativeSettle: true } : { ...CAPABILITIES, maxPointers: shellPointers };
|
|
5565
|
+
}
|
|
5566
|
+
function openDumper(options, session, facts, saving) {
|
|
5429
5567
|
const { serial, adb, exec, log } = options;
|
|
5430
5568
|
const { sdk } = facts;
|
|
5431
5569
|
const initialRoute = options.dumpRoute ?? facts.savedRoute;
|
|
5432
|
-
|
|
5433
|
-
const dumper = createDumper({
|
|
5570
|
+
return createDumper({
|
|
5434
5571
|
session,
|
|
5435
5572
|
exec,
|
|
5436
5573
|
adb,
|
|
@@ -5444,40 +5581,19 @@ function buildDriver(options, session, baseScreen, clock, execBinary, facts) {
|
|
|
5444
5581
|
saving.push(options.state.update(serial, patchFor(sdk, { dumpRoute: route })));
|
|
5445
5582
|
}
|
|
5446
5583
|
});
|
|
5584
|
+
}
|
|
5585
|
+
function buildDriver(options, session, baseScreen, clock, execBinary, facts) {
|
|
5586
|
+
const { serial, adb, exec, log } = options;
|
|
5587
|
+
const { sdk } = facts;
|
|
5588
|
+
const saving = [];
|
|
5589
|
+
const dumper = openDumper(options, session, facts, saving);
|
|
5447
5590
|
const uiad = openUiad({ options, session, baseScreen, clock, facts, saving });
|
|
5448
|
-
async function dumpWithRetry() {
|
|
5449
|
-
let dumped = await dumper.dump();
|
|
5450
|
-
for (let attempt = 1;attempt < SNAPSHOT_ATTEMPTS2 && !dumped.success && dumped.error.code === "E_AX_UNAVAILABLE"; attempt++) {
|
|
5451
|
-
log?.(`uiautomator unavailable, retrying (${attempt}/${SNAPSHOT_ATTEMPTS2 - 1})`);
|
|
5452
|
-
await clock.sleep(SNAPSHOT_RETRY_MS2);
|
|
5453
|
-
dumped = await dumper.dump();
|
|
5454
|
-
}
|
|
5455
|
-
return dumped;
|
|
5456
|
-
}
|
|
5457
5591
|
async function dumpSnapshot() {
|
|
5458
5592
|
noteSpanDetail("backend", "dump");
|
|
5459
|
-
const dumped = await dumpWithRetry();
|
|
5593
|
+
const dumped = await dumpWithRetry(dumper, clock, log);
|
|
5460
5594
|
if (!dumped.success)
|
|
5461
5595
|
return dumped;
|
|
5462
|
-
|
|
5463
|
-
const rotation = hierarchyRotation(dumped.data.xml);
|
|
5464
|
-
const screen = rotatedScreen(baseScreen, rotation);
|
|
5465
|
-
try {
|
|
5466
|
-
parsed = parseUiautomatorXml(dumped.data.xml, screen);
|
|
5467
|
-
} catch (error) {
|
|
5468
|
-
if (error instanceof UiautomatorParseError)
|
|
5469
|
-
return driverErr("E_AX_UNAVAILABLE", error.message);
|
|
5470
|
-
throw error;
|
|
5471
|
-
}
|
|
5472
|
-
const taken = {
|
|
5473
|
-
platform: "android",
|
|
5474
|
-
backend: "uiautomator",
|
|
5475
|
-
takenAt: clock.now(),
|
|
5476
|
-
screen,
|
|
5477
|
-
...parsed,
|
|
5478
|
-
truncated: false
|
|
5479
|
-
};
|
|
5480
|
-
return { success: true, data: taken };
|
|
5596
|
+
return parseDump(dumped.data.xml, baseScreen, clock);
|
|
5481
5597
|
}
|
|
5482
5598
|
async function takeSnapshot() {
|
|
5483
5599
|
return await uiad?.snapshot() ?? dumpSnapshot();
|
|
@@ -5613,18 +5729,20 @@ function buildDriver(options, session, baseScreen, clock, execBinary, facts) {
|
|
|
5613
5729
|
}
|
|
5614
5730
|
return { success: true, data: { path: opts.path } };
|
|
5615
5731
|
};
|
|
5616
|
-
const
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
return res;
|
|
5625
|
-
}
|
|
5626
|
-
return { success: true, data: set };
|
|
5732
|
+
const setOrientation = async (set) => {
|
|
5733
|
+
const saved = await facts.settings?.beforeRotate();
|
|
5734
|
+
if (saved && !saved.success)
|
|
5735
|
+
return saved;
|
|
5736
|
+
for (const argv of rotationArgvs(orientationToRotation(set))) {
|
|
5737
|
+
const res = await input(shellCommand(argv));
|
|
5738
|
+
if (!res.success)
|
|
5739
|
+
return res;
|
|
5627
5740
|
}
|
|
5741
|
+
return { success: true, data: set };
|
|
5742
|
+
};
|
|
5743
|
+
const orientation = async (set) => {
|
|
5744
|
+
if (set)
|
|
5745
|
+
return setOrientation(set);
|
|
5628
5746
|
const res = await session.run(shellCommand(userRotationArgv()));
|
|
5629
5747
|
if (!res.success)
|
|
5630
5748
|
return res;
|
|
@@ -5774,30 +5892,7 @@ function buildDriver(options, session, baseScreen, clock, execBinary, facts) {
|
|
|
5774
5892
|
});
|
|
5775
5893
|
return unit;
|
|
5776
5894
|
};
|
|
5777
|
-
const
|
|
5778
|
-
const touched = await options.exec(emuFingerTouchArgv(adb, serial, fingerId), { timeoutMs: 1e4 });
|
|
5779
|
-
if (touched.exitCode === 0 && !/^KO\b/m.test(touched.stdout))
|
|
5780
|
-
return { success: true, data: undefined };
|
|
5781
|
-
return driverErr("E_IO", `adb emu finger touch failed: ${`${touched.stdout}${touched.stderr}`.trim().slice(0, 200)}`);
|
|
5782
|
-
};
|
|
5783
|
-
const biometrics = async (op) => {
|
|
5784
|
-
if (!isEmulatorSerial2(serial)) {
|
|
5785
|
-
return driverErr("E_UNSUPPORTED", `biometrics need an emulator, not the physical device ${serial}`);
|
|
5786
|
-
}
|
|
5787
|
-
if (op === "enroll" || op === "unenroll") {
|
|
5788
|
-
return driverErr("E_UNSUPPORTED", `Android fingerprint enrollment comes from the golden snapshot (profile \`biometrics: true\`); \`${op}\` is not available at run time`);
|
|
5789
|
-
}
|
|
5790
|
-
if (op !== "status") {
|
|
5791
|
-
const touched = await touchSensor(op === "match" ? 1 : UNENROLLED_FINGER_ID);
|
|
5792
|
-
if (!touched.success)
|
|
5793
|
-
return touched;
|
|
5794
|
-
}
|
|
5795
|
-
const dump = await runChecked(session, shellCommand(dumpsysFingerprintArgv()), "E_UNSUPPORTED");
|
|
5796
|
-
if (!dump.success)
|
|
5797
|
-
return dump;
|
|
5798
|
-
const state = { enrolled: parseFingerprintEnrolled(dump.data), modality: "fingerprint" };
|
|
5799
|
-
return { success: true, data: state };
|
|
5800
|
-
};
|
|
5895
|
+
const biometrics = biometricsOp(session, options.exec, adb, serial);
|
|
5801
5896
|
const clipboard = async () => driverErr("E_UNSUPPORTED", "Android has no public shell clipboard access; type ASCII with typeText");
|
|
5802
5897
|
const health = async () => {
|
|
5803
5898
|
const [release, sdk, model] = await Promise.all(["ro.build.version.release", "ro.build.version.sdk", "ro.product.model"].map((k) => session.run(shellCommand(getpropArgv(k)))));
|
|
@@ -5816,7 +5911,7 @@ function buildDriver(options, session, baseScreen, clock, execBinary, facts) {
|
|
|
5816
5911
|
platform: "android",
|
|
5817
5912
|
id: serial,
|
|
5818
5913
|
get capabilities() {
|
|
5819
|
-
return uiad?.enabled()
|
|
5914
|
+
return capabilitiesOf(uiad?.enabled() === true, maxPointers());
|
|
5820
5915
|
},
|
|
5821
5916
|
health,
|
|
5822
5917
|
snapshot,
|
|
@@ -5832,8 +5927,10 @@ function buildDriver(options, session, baseScreen, clock, execBinary, facts) {
|
|
|
5832
5927
|
key,
|
|
5833
5928
|
pressKeyRepeat,
|
|
5834
5929
|
...uiad !== undefined || sdk !== undefined && sdk >= SELECT_ALL_MIN_SDK ? { selectAll } : {},
|
|
5835
|
-
...uiad !== undefined ? { setText, a11yEvents, ...screenReader } : {},
|
|
5836
5930
|
...uiad !== undefined ? {
|
|
5931
|
+
setText,
|
|
5932
|
+
a11yEvents,
|
|
5933
|
+
...screenReader,
|
|
5837
5934
|
auditNatively: (opts) => atfAudit({ run: (o) => uiad.audit(o) }, opts?.contrast === undefined ? {} : { contrast: opts.contrast })
|
|
5838
5935
|
} : {},
|
|
5839
5936
|
pressButton,
|
|
@@ -6382,18 +6479,27 @@ function isRecord4(value) {
|
|
|
6382
6479
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6383
6480
|
}
|
|
6384
6481
|
var optionalString = (value) => typeof value === "string" && value !== "" ? value : undefined;
|
|
6482
|
+
function readFrame(frame) {
|
|
6483
|
+
if (!Array.isArray(frame) || frame.length !== 4 || !frame.every((n) => typeof n === "number"))
|
|
6484
|
+
return;
|
|
6485
|
+
return frame;
|
|
6486
|
+
}
|
|
6487
|
+
function actionsValid(actions) {
|
|
6488
|
+
return actions === undefined || Array.isArray(actions) && actions.every((a) => typeof a === "string");
|
|
6489
|
+
}
|
|
6490
|
+
function traitsValid(traits) {
|
|
6491
|
+
return typeof traits === "string" && /^0x[0-9a-f]+$/i.test(traits);
|
|
6492
|
+
}
|
|
6385
6493
|
function readNode2(value) {
|
|
6386
6494
|
if (!isRecord4(value) || typeof value.ae !== "boolean" || typeof value.cls !== "string")
|
|
6387
6495
|
return;
|
|
6388
|
-
const {
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
if (typeof traits !== "string" || !/^0x[0-9a-f]+$/i.test(traits))
|
|
6496
|
+
const { traits, actions } = value;
|
|
6497
|
+
const box = readFrame(value.frame);
|
|
6498
|
+
if (!box)
|
|
6392
6499
|
return;
|
|
6393
|
-
if (
|
|
6500
|
+
if (!traitsValid(traits) || !actionsValid(actions))
|
|
6394
6501
|
return;
|
|
6395
|
-
|
|
6396
|
-
const [x, y, width, height] = frame;
|
|
6502
|
+
const [x, y, width, height] = box;
|
|
6397
6503
|
const label = optionalString(value.label);
|
|
6398
6504
|
const hint = optionalString(value.hint);
|
|
6399
6505
|
const text = optionalString(value.value);
|
|
@@ -7389,6 +7495,79 @@ function withStagedSettings(requested, settings) {
|
|
|
7389
7495
|
args: [...requested?.args ?? [], ...args]
|
|
7390
7496
|
};
|
|
7391
7497
|
}
|
|
7498
|
+
var NOTHING_APPLIED = { ok: true, applied: [] };
|
|
7499
|
+
async function a11yTextSizeStep(run, udid, op) {
|
|
7500
|
+
const size = op === null ? undefined : op.textSize;
|
|
7501
|
+
if (op !== null && size === undefined)
|
|
7502
|
+
return NOTHING_APPLIED;
|
|
7503
|
+
const resolved = op === null ? undefined : resolveTextSize(size);
|
|
7504
|
+
if (resolved && !resolved.success)
|
|
7505
|
+
return { ok: false, failure: driverErr("E_BAD_REQUEST", resolved.error) };
|
|
7506
|
+
const done = await run(contentSizeArgv(udid, resolved ? resolved.data.category : DEFAULT_CONTENT_SIZE));
|
|
7507
|
+
return done.success ? { ok: true, applied: ["textSize"] } : { ok: false, failure: done };
|
|
7508
|
+
}
|
|
7509
|
+
async function a11yContrastStep(run, udid, op) {
|
|
7510
|
+
if (op !== null && op.contrast === undefined)
|
|
7511
|
+
return NOTHING_APPLIED;
|
|
7512
|
+
const done = await run(contrastArgv(udid, op?.contrast === "increased"));
|
|
7513
|
+
return done.success ? { ok: true, applied: ["contrast"] } : { ok: false, failure: done };
|
|
7514
|
+
}
|
|
7515
|
+
async function a11yDefaultsStep(run, udid, op) {
|
|
7516
|
+
if (op === null) {
|
|
7517
|
+
for (const argv of iosDefaultsDeleteArgvs(udid, IOS_DEFAULTS_FIELDS)) {
|
|
7518
|
+
const done = await run(argv, false, iosDefaultsMissing);
|
|
7519
|
+
if (!done.success)
|
|
7520
|
+
return { ok: false, failure: done };
|
|
7521
|
+
}
|
|
7522
|
+
return NOTHING_APPLIED;
|
|
7523
|
+
}
|
|
7524
|
+
const entries = IOS_DEFAULTS_FIELDS.flatMap((field) => {
|
|
7525
|
+
const on = op[field];
|
|
7526
|
+
return on === undefined ? [] : [[field, on]];
|
|
7527
|
+
});
|
|
7528
|
+
for (const argv of iosDefaultsWriteArgvs(udid, entries)) {
|
|
7529
|
+
const done = await run(argv);
|
|
7530
|
+
if (!done.success)
|
|
7531
|
+
return { ok: false, failure: done };
|
|
7532
|
+
}
|
|
7533
|
+
return { ok: true, applied: entries.map(([field]) => field) };
|
|
7534
|
+
}
|
|
7535
|
+
function a11ySkipped(op) {
|
|
7536
|
+
const skipped = [];
|
|
7537
|
+
for (const setting of ["highContrastText", "colorInversion"]) {
|
|
7538
|
+
if (op?.[setting] !== undefined)
|
|
7539
|
+
skipped.push({ setting, reason: "Android only" });
|
|
7540
|
+
}
|
|
7541
|
+
return skipped;
|
|
7542
|
+
}
|
|
7543
|
+
function a11yScreenReader(op) {
|
|
7544
|
+
if (op?.screenReader === undefined)
|
|
7545
|
+
return;
|
|
7546
|
+
return op.screenReader ? { mode: "simulated", engine: "voiceover" } : { mode: "off" };
|
|
7547
|
+
}
|
|
7548
|
+
async function batteryClear(run, udid) {
|
|
7549
|
+
const done = await run(statusBarClearArgv(udid));
|
|
7550
|
+
return done.success ? { success: true, data: { cosmetic: true } } : done;
|
|
7551
|
+
}
|
|
7552
|
+
async function batteryOverride(run, udid, op) {
|
|
7553
|
+
const bad = badRequest(batteryProblem(op.level));
|
|
7554
|
+
if (bad)
|
|
7555
|
+
return bad;
|
|
7556
|
+
if (op.level === undefined && op.charging === undefined) {
|
|
7557
|
+
return driverErr("E_BAD_REQUEST", "battery needs a level, charging, or both");
|
|
7558
|
+
}
|
|
7559
|
+
const done = await run(statusBarBatteryArgv(udid, op));
|
|
7560
|
+
if (!done.success)
|
|
7561
|
+
return done;
|
|
7562
|
+
return {
|
|
7563
|
+
success: true,
|
|
7564
|
+
data: {
|
|
7565
|
+
...op.level === undefined ? {} : { level: op.level },
|
|
7566
|
+
...op.charging === undefined ? {} : { charging: op.charging },
|
|
7567
|
+
cosmetic: true
|
|
7568
|
+
}
|
|
7569
|
+
};
|
|
7570
|
+
}
|
|
7392
7571
|
function createIosSettings(deps) {
|
|
7393
7572
|
const { udid, exec } = deps;
|
|
7394
7573
|
let memoryStaged = {};
|
|
@@ -7467,76 +7646,25 @@ function createIosSettings(deps) {
|
|
|
7467
7646
|
},
|
|
7468
7647
|
async a11y(op) {
|
|
7469
7648
|
const applied = [];
|
|
7470
|
-
const
|
|
7471
|
-
const
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
const done = await simctl(contentSizeArgv(udid, resolved ? resolved.data.category : DEFAULT_CONTENT_SIZE));
|
|
7477
|
-
if (!done.success)
|
|
7478
|
-
return done;
|
|
7479
|
-
applied.push("textSize");
|
|
7480
|
-
}
|
|
7481
|
-
if (op === null || op.contrast !== undefined) {
|
|
7482
|
-
const done = await simctl(contrastArgv(udid, op?.contrast === "increased"));
|
|
7483
|
-
if (!done.success)
|
|
7484
|
-
return done;
|
|
7485
|
-
applied.push("contrast");
|
|
7486
|
-
}
|
|
7487
|
-
if (op === null) {
|
|
7488
|
-
for (const argv of iosDefaultsDeleteArgvs(udid, IOS_DEFAULTS_FIELDS)) {
|
|
7489
|
-
const done = await simctl(argv, false, iosDefaultsMissing);
|
|
7490
|
-
if (!done.success)
|
|
7491
|
-
return done;
|
|
7492
|
-
}
|
|
7493
|
-
} else {
|
|
7494
|
-
const entries = IOS_DEFAULTS_FIELDS.flatMap((field) => {
|
|
7495
|
-
const on = op[field];
|
|
7496
|
-
return on === undefined ? [] : [[field, on]];
|
|
7497
|
-
});
|
|
7498
|
-
for (const argv of iosDefaultsWriteArgvs(udid, entries)) {
|
|
7499
|
-
const done = await simctl(argv);
|
|
7500
|
-
if (!done.success)
|
|
7501
|
-
return done;
|
|
7502
|
-
}
|
|
7503
|
-
applied.push(...entries.map(([field]) => field));
|
|
7504
|
-
}
|
|
7505
|
-
for (const setting of ["highContrastText", "colorInversion"]) {
|
|
7506
|
-
if (op?.[setting] !== undefined)
|
|
7507
|
-
skipped.push({ setting, reason: "Android only" });
|
|
7649
|
+
const steps = [a11yTextSizeStep, a11yContrastStep, a11yDefaultsStep];
|
|
7650
|
+
for (const step of steps) {
|
|
7651
|
+
const outcome = await step(simctl, udid, op);
|
|
7652
|
+
if (!outcome.ok)
|
|
7653
|
+
return outcome.failure;
|
|
7654
|
+
applied.push(...outcome.applied);
|
|
7508
7655
|
}
|
|
7656
|
+
const skipped = a11ySkipped(op);
|
|
7509
7657
|
if (applied.length === 0 && skipped.length > 0) {
|
|
7510
7658
|
return driverErr("E_UNSUPPORTED", skipped.map((s) => `${s.setting}: ${s.reason}`).join("; "));
|
|
7511
7659
|
}
|
|
7512
|
-
const screenReader = op
|
|
7660
|
+
const screenReader = a11yScreenReader(op);
|
|
7513
7661
|
return {
|
|
7514
7662
|
success: true,
|
|
7515
7663
|
data: { applied, skipped, notes: [], ...screenReader ? { screenReader } : {} }
|
|
7516
7664
|
};
|
|
7517
7665
|
},
|
|
7518
7666
|
async battery(op) {
|
|
7519
|
-
|
|
7520
|
-
const done = await simctl(statusBarClearArgv(udid));
|
|
7521
|
-
return done.success ? { success: true, data: { cosmetic: true } } : done;
|
|
7522
|
-
}
|
|
7523
|
-
const bad = badRequest(batteryProblem(op.level));
|
|
7524
|
-
if (bad)
|
|
7525
|
-
return bad;
|
|
7526
|
-
if (op.level === undefined && op.charging === undefined) {
|
|
7527
|
-
return driverErr("E_BAD_REQUEST", "battery needs a level, charging, or both");
|
|
7528
|
-
}
|
|
7529
|
-
const done = await simctl(statusBarBatteryArgv(udid, op));
|
|
7530
|
-
if (!done.success)
|
|
7531
|
-
return done;
|
|
7532
|
-
return {
|
|
7533
|
-
success: true,
|
|
7534
|
-
data: {
|
|
7535
|
-
...op.level === undefined ? {} : { level: op.level },
|
|
7536
|
-
...op.charging === undefined ? {} : { charging: op.charging },
|
|
7537
|
-
cosmetic: true
|
|
7538
|
-
}
|
|
7539
|
-
};
|
|
7667
|
+
return op === null ? batteryClear(simctl, udid) : batteryOverride(simctl, udid, op);
|
|
7540
7668
|
},
|
|
7541
7669
|
async memoryWarning() {
|
|
7542
7670
|
return driverErr("E_UNSUPPORTED", "no way to deliver a memory warning to an iOS app was found: `notifyutil -p com.apple.system.lowmemory` is accepted but a UIKit app never receives UIApplicationDidReceiveMemoryWarning (verified); use Simulator > Debug > Simulate Memory Warning by hand");
|
|
@@ -8609,6 +8737,19 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
8609
8737
|
throw error;
|
|
8610
8738
|
}
|
|
8611
8739
|
}
|
|
8740
|
+
async function settleAfterLaunch(bundleId) {
|
|
8741
|
+
lastLaunched = bundleId;
|
|
8742
|
+
launchedBundle = bundleId;
|
|
8743
|
+
if (await rebind(bundleId))
|
|
8744
|
+
launchedBundle = undefined;
|
|
8745
|
+
const loaded = await net?.confirmLoaded();
|
|
8746
|
+
if (loaded && !loaded.success)
|
|
8747
|
+
return loaded.error;
|
|
8748
|
+
const a11yLoaded = await a11y?.confirmLoaded();
|
|
8749
|
+
if (a11yLoaded && !a11yLoaded.success)
|
|
8750
|
+
return a11yLoaded.error;
|
|
8751
|
+
return;
|
|
8752
|
+
}
|
|
8612
8753
|
async function rebind(bundleId) {
|
|
8613
8754
|
if (rebindUnsupported || settleUnsupported)
|
|
8614
8755
|
return false;
|
|
@@ -8814,19 +8955,10 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
8814
8955
|
const o = withEnv.data;
|
|
8815
8956
|
const viaHelper = await appOp("launch", params({ bundleId, args: o?.args && [...o.args], env: o?.env, terminateRunning: o?.relaunch !== false }), LAUNCH_TIMEOUT_MS);
|
|
8816
8957
|
const launched = await appOutcome(viaHelper, () => simctl.launch(bundleId, o), (data) => noteSpanDetail("pid", data.pid));
|
|
8817
|
-
if (launched.success)
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
launchedBundle = undefined;
|
|
8822
|
-
const loaded = await net?.confirmLoaded();
|
|
8823
|
-
if (loaded && !loaded.success)
|
|
8824
|
-
return loaded;
|
|
8825
|
-
const a11yLoaded = await a11y?.confirmLoaded();
|
|
8826
|
-
if (a11yLoaded && !a11yLoaded.success)
|
|
8827
|
-
return a11yLoaded;
|
|
8828
|
-
}
|
|
8829
|
-
return launched;
|
|
8958
|
+
if (!launched.success)
|
|
8959
|
+
return launched;
|
|
8960
|
+
const unconfirmed = await settleAfterLaunch(bundleId);
|
|
8961
|
+
return unconfirmed ? { success: false, error: unconfirmed } : launched;
|
|
8830
8962
|
},
|
|
8831
8963
|
terminate: async (bundleId) => {
|
|
8832
8964
|
const viaHelper = await appOp("terminate", { bundleId }, APP_OP_TIMEOUT_MS);
|
|
@@ -8892,6 +9024,33 @@ async function connect(o, log) {
|
|
|
8892
9024
|
...timeoutOption(o)
|
|
8893
9025
|
});
|
|
8894
9026
|
}
|
|
9027
|
+
function nativeAuditFor(o, exec, log, ctx) {
|
|
9028
|
+
return createIosNativeAudit({
|
|
9029
|
+
udid: o.udid,
|
|
9030
|
+
exec,
|
|
9031
|
+
bundleId: () => o.bundleId ?? ctx.lastBundle(),
|
|
9032
|
+
...o.wardenHome ? { wardenHome: o.wardenHome } : {},
|
|
9033
|
+
...o.version ? { version: o.version } : {},
|
|
9034
|
+
...o.runnerSources ? { runnerSources: o.runnerSources } : {},
|
|
9035
|
+
...o.nativeAudit ?? {},
|
|
9036
|
+
...o.debug ? { log } : {}
|
|
9037
|
+
});
|
|
9038
|
+
}
|
|
9039
|
+
function settingsFor(o, exec, ctx) {
|
|
9040
|
+
return createIosSettings({
|
|
9041
|
+
udid: o.udid,
|
|
9042
|
+
exec,
|
|
9043
|
+
lastBundle: ctx.lastBundle,
|
|
9044
|
+
...o.bundleId ? { defaultBundle: o.bundleId } : {},
|
|
9045
|
+
staged: o.stagedSettings ?? stagedSettingsFor(o.wardenHome ?? wardenHome(), o.udid)
|
|
9046
|
+
});
|
|
9047
|
+
}
|
|
9048
|
+
function helperPreflight(o) {
|
|
9049
|
+
if (o.client || (o.socketPath ?? o.env?.simdSocket))
|
|
9050
|
+
return;
|
|
9051
|
+
const helper = resolveHelper(o);
|
|
9052
|
+
return helper.success ? undefined : helper;
|
|
9053
|
+
}
|
|
8895
9054
|
async function openIosDriver(o) {
|
|
8896
9055
|
const log = o.log ?? (() => {
|
|
8897
9056
|
return;
|
|
@@ -8899,11 +9058,9 @@ async function openIosDriver(o) {
|
|
|
8899
9058
|
const now = o.now ?? Date.now;
|
|
8900
9059
|
const exec = o.debug ? timedExec(o.exec, log, now) : o.exec;
|
|
8901
9060
|
const simctl = createSimctl({ udid: o.udid, exec, ...o.devicesRoot ? { devicesRoot: o.devicesRoot } : {} });
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
return helper;
|
|
8906
|
-
}
|
|
9061
|
+
const preflight = helperPreflight(o);
|
|
9062
|
+
if (preflight)
|
|
9063
|
+
return preflight;
|
|
8907
9064
|
const connecting = o.client ? Promise.resolve({ success: true, data: o.client }) : connect(o, log);
|
|
8908
9065
|
const [client, automation] = await Promise.all([connecting, simctl.enableAutomation()]);
|
|
8909
9066
|
if (!automation.success)
|
|
@@ -8927,23 +9084,8 @@ async function openIosDriver(o) {
|
|
|
8927
9084
|
log,
|
|
8928
9085
|
...network ? { network } : {},
|
|
8929
9086
|
a11y: () => iosA11yFor(o, now, sleep),
|
|
8930
|
-
nativeAudit: (ctx) =>
|
|
8931
|
-
|
|
8932
|
-
exec,
|
|
8933
|
-
bundleId: () => o.bundleId ?? ctx.lastBundle(),
|
|
8934
|
-
...o.wardenHome ? { wardenHome: o.wardenHome } : {},
|
|
8935
|
-
...o.version ? { version: o.version } : {},
|
|
8936
|
-
...o.runnerSources ? { runnerSources: o.runnerSources } : {},
|
|
8937
|
-
...o.nativeAudit ?? {},
|
|
8938
|
-
...o.debug ? { log } : {}
|
|
8939
|
-
}),
|
|
8940
|
-
settings: (ctx) => createIosSettings({
|
|
8941
|
-
udid: o.udid,
|
|
8942
|
-
exec,
|
|
8943
|
-
lastBundle: ctx.lastBundle,
|
|
8944
|
-
...o.bundleId ? { defaultBundle: o.bundleId } : {},
|
|
8945
|
-
staged: o.stagedSettings ?? stagedSettingsFor(o.wardenHome ?? wardenHome(), o.udid)
|
|
8946
|
-
}),
|
|
9087
|
+
nativeAudit: (ctx) => nativeAuditFor(o, exec, log, ctx),
|
|
9088
|
+
settings: (ctx) => settingsFor(o, exec, ctx),
|
|
8947
9089
|
...o.clockRecorder ? { clockRecorder: o.clockRecorder } : {}
|
|
8948
9090
|
})
|
|
8949
9091
|
};
|
|
@@ -9530,15 +9672,20 @@ var stderrLog = (line) => {
|
|
|
9530
9672
|
process.stderr.write(`${line}
|
|
9531
9673
|
`);
|
|
9532
9674
|
};
|
|
9675
|
+
function leaseSeams(env, deps, given) {
|
|
9676
|
+
const networkProxy = given.networkProxy ?? leaseProxyFor(env, deps);
|
|
9677
|
+
const clockRecorder = given.clockRecorder ?? leaseClockRecorderFor(env, deps);
|
|
9678
|
+
return {
|
|
9679
|
+
...networkProxy === undefined ? {} : { networkProxy },
|
|
9680
|
+
...clockRecorder === undefined ? {} : { clockRecorder }
|
|
9681
|
+
};
|
|
9682
|
+
}
|
|
9533
9683
|
function androidOptions(env, deps, debug) {
|
|
9534
9684
|
const android = deps.android ?? {};
|
|
9535
9685
|
const log = deps.log ?? (debug ? stderrLog : undefined);
|
|
9536
|
-
const networkProxy = android.networkProxy ?? leaseProxyFor(env, deps);
|
|
9537
|
-
const clockRecorder = android.clockRecorder ?? leaseClockRecorderFor(env, deps);
|
|
9538
9686
|
return {
|
|
9539
9687
|
...android,
|
|
9540
|
-
...
|
|
9541
|
-
...clockRecorder === undefined ? {} : { clockRecorder },
|
|
9688
|
+
...leaseSeams(env, deps, android),
|
|
9542
9689
|
serial: env.udid,
|
|
9543
9690
|
adb: android.adb ?? androidTools(process.env).adb,
|
|
9544
9691
|
exec: deps.exec ?? bunExec,
|
|
@@ -9570,12 +9717,9 @@ async function openDriver(env, deps = {}) {
|
|
|
9570
9717
|
case "ios": {
|
|
9571
9718
|
if (isPhysicalIosId(env.udid))
|
|
9572
9719
|
return openIosDeviceDriver(deviceOptions(env, deps, debug));
|
|
9573
|
-
const networkProxy = deps.networkProxy ?? leaseProxyFor(env, deps);
|
|
9574
|
-
const clockRecorder = deps.clockRecorder ?? leaseClockRecorderFor(env, deps);
|
|
9575
9720
|
return openIosDriver({
|
|
9576
9721
|
...deps,
|
|
9577
|
-
...
|
|
9578
|
-
...clockRecorder === undefined ? {} : { clockRecorder },
|
|
9722
|
+
...leaseSeams(env, deps, deps),
|
|
9579
9723
|
udid: env.udid,
|
|
9580
9724
|
bundleId: env.bundleId,
|
|
9581
9725
|
env,
|
|
@@ -10124,6 +10268,12 @@ function createDevice({
|
|
|
10124
10268
|
signal,
|
|
10125
10269
|
step: (label, fn) => session.step === undefined ? fn() : session.step("locator", label, fn)
|
|
10126
10270
|
});
|
|
10271
|
+
async function closeAutofillSaveSheet(action) {
|
|
10272
|
+
if (typeof action !== "string") {
|
|
10273
|
+
throw new E2eError2("ACTION_FAILED", `the autofill "Save password?" sheet is closed with Back only, never "${action.button}": use accept or dismiss`);
|
|
10274
|
+
}
|
|
10275
|
+
await unwrap(() => driver.pressButton("back"), "dismiss autofill save sheet", signal());
|
|
10276
|
+
}
|
|
10127
10277
|
const device = {
|
|
10128
10278
|
a11yAudit: a11y.a11yAudit,
|
|
10129
10279
|
screenReader: a11y.screenReader,
|
|
@@ -10151,10 +10301,7 @@ function createDevice({
|
|
|
10151
10301
|
if (isOpenInSheet(info) && typeof action === "string" && linkInFlight(session))
|
|
10152
10302
|
return false;
|
|
10153
10303
|
if (driver.platform === "android" && isAutofillSaveSheet(info.buttons)) {
|
|
10154
|
-
|
|
10155
|
-
throw new E2eError2("ACTION_FAILED", `the autofill "Save password?" sheet is closed with Back only, never "${action.button}": use accept or dismiss`);
|
|
10156
|
-
}
|
|
10157
|
-
await unwrap(() => driver.pressButton("back"), "dismiss autofill save sheet", signal());
|
|
10304
|
+
await closeAutofillSaveSheet(action);
|
|
10158
10305
|
return true;
|
|
10159
10306
|
}
|
|
10160
10307
|
const target = alertTarget(info, action, driver.platform);
|
|
@@ -10482,6 +10629,23 @@ function abortError(signal) {
|
|
|
10482
10629
|
function isFinitePoint(point) {
|
|
10483
10630
|
return Number.isFinite(point.x) && Number.isFinite(point.y);
|
|
10484
10631
|
}
|
|
10632
|
+
async function swipeDirection(rt, options) {
|
|
10633
|
+
const observed = await rt.observe();
|
|
10634
|
+
if (canActOnElements(observed.driver)) {
|
|
10635
|
+
if (rt.drive("swipe", await observed.driver.scrollElement(options.direction)))
|
|
10636
|
+
return;
|
|
10637
|
+
}
|
|
10638
|
+
const vector = swipeVector(options.direction, screenRect(observed.snapshot.screen), options.momentum);
|
|
10639
|
+
rt.drive("swipe", await observed.driver.swipe(vector.from, vector.to, vector.options));
|
|
10640
|
+
}
|
|
10641
|
+
async function swipeBetween(rt, host, options) {
|
|
10642
|
+
if (!isFinitePoint(options.from) || !isFinitePoint(options.to)) {
|
|
10643
|
+
throw new E2eError2("INVALID_ARGUMENT", "swipe needs finite coordinates for from and to");
|
|
10644
|
+
}
|
|
10645
|
+
const driver = await host.driver();
|
|
10646
|
+
const opts = options.durationMs === undefined ? undefined : { durationMs: options.durationMs };
|
|
10647
|
+
rt.drive("swipe", await driver.swipe(options.from, options.to, opts));
|
|
10648
|
+
}
|
|
10485
10649
|
function createScreen(host) {
|
|
10486
10650
|
const rt = new Runtime(host);
|
|
10487
10651
|
const lazy = (query) => new Locator({ kind: "query", query }, rt);
|
|
@@ -10493,22 +10657,9 @@ function createScreen(host) {
|
|
|
10493
10657
|
throw new E2eError2("INVALID_ARGUMENT", "swipe takes either { direction, momentum? } or { from, to, durationMs? }, not a mix");
|
|
10494
10658
|
}
|
|
10495
10659
|
return rt.step("swipe", async () => {
|
|
10496
|
-
if ("direction" in options)
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
if (rt.drive("swipe", await observed.driver.scrollElement(options.direction)))
|
|
10500
|
-
return;
|
|
10501
|
-
}
|
|
10502
|
-
const vector = swipeVector(options.direction, screenRect(observed.snapshot.screen), options.momentum);
|
|
10503
|
-
rt.drive("swipe", await observed.driver.swipe(vector.from, vector.to, vector.options));
|
|
10504
|
-
return;
|
|
10505
|
-
}
|
|
10506
|
-
if (!isFinitePoint(options.from) || !isFinitePoint(options.to)) {
|
|
10507
|
-
throw new E2eError2("INVALID_ARGUMENT", "swipe needs finite coordinates for from and to");
|
|
10508
|
-
}
|
|
10509
|
-
const driver = await host.driver();
|
|
10510
|
-
const opts = options.durationMs === undefined ? undefined : { durationMs: options.durationMs };
|
|
10511
|
-
rt.drive("swipe", await driver.swipe(options.from, options.to, opts));
|
|
10660
|
+
if ("direction" in options)
|
|
10661
|
+
return swipeDirection(rt, options);
|
|
10662
|
+
return swipeBetween(rt, host, options);
|
|
10512
10663
|
});
|
|
10513
10664
|
};
|
|
10514
10665
|
return {
|