@vostride/agent-qa-ios 0.2.3 → 0.2.4
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/dist/index.cjs +24 -116
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -63
- package/dist/index.d.ts +3 -63
- package/dist/index.js +25 -116
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PlatformAdapter, PlatformConfig, LiveStreamCapability,
|
|
1
|
+
import { PlatformAdapter, PlatformConfig, LiveStreamCapability, ActionsFor, ActionResult, ConsoleLogEntry } from '@vostride/agent-qa-runner';
|
|
2
2
|
import { ScreenState } from '@vostride/agent-qa-schema';
|
|
3
3
|
|
|
4
4
|
declare class IOSPlatformAdapter implements PlatformAdapter<'ios'> {
|
|
@@ -21,15 +21,8 @@ declare class IOSPlatformAdapter implements PlatformAdapter<'ios'> {
|
|
|
21
21
|
private snapshotDepthWarned;
|
|
22
22
|
setup(config: PlatformConfig): Promise<void>;
|
|
23
23
|
cleanup(): Promise<void>;
|
|
24
|
-
/**
|
|
25
|
-
* 428 D-11: the labelled 1s snapshot preview, over `captureScreenshot()` — `takeScreenshot()`
|
|
26
|
-
* and nothing else. Never `screenshot()`: that one switches the Appium context, and a tick
|
|
27
|
-
* landing between a step's context switch and its command would run the command in the wrong
|
|
28
|
-
* context. Declared before the driver exists (a capture then answers `undefined`), so the
|
|
29
|
-
* composition's selection never falls through to `screenshot()`.
|
|
30
|
-
*/
|
|
31
24
|
get liveStream(): LiveStreamCapability;
|
|
32
|
-
observe(
|
|
25
|
+
observe(): Promise<ScreenState>;
|
|
33
26
|
execute(action: ActionsFor<'ios'>): Promise<ActionResult>;
|
|
34
27
|
screenshot(): Promise<Buffer | undefined>;
|
|
35
28
|
pollDeviceLogs(): Promise<void>;
|
|
@@ -66,60 +59,12 @@ declare class IOSPlatformAdapter implements PlatformAdapter<'ios'> {
|
|
|
66
59
|
private readIOSPlaceholder;
|
|
67
60
|
/** Read a focused element's `value` attribute; undefined when nothing is readable. */
|
|
68
61
|
private readIOSValue;
|
|
69
|
-
/**
|
|
70
|
-
* EXEC-01 iOS fill compound (D-05..D-08). From the SAME focused-element handle: (append) capture
|
|
71
|
-
* the prior value FIRST via getAttribute('value') then addValue, or (replace) setValue; RE-READ
|
|
72
|
-
* via getAttribute('value') on that same handle; verdict via compareFillReread. Secure fields
|
|
73
|
-
* (nativeType XCUIElementTypeSecureTextField) take the non-empty-only lane — the comparator yields
|
|
74
|
-
* length sentinels so no raw value enters the result (T-359-02); bullet masking is tolerated
|
|
75
|
-
* (A3/Pitfall 7 — never a length compare). On a passing re-read AND submit, a return/newline is
|
|
76
|
-
* delivered AFTER the re-read (D-07). Single typing attempt; a mismatch is success:false + §7.1.
|
|
77
|
-
*
|
|
78
|
-
* Append (437 FA-1, owner 2026-10-08): the XCUITest `value` attribute often lags after addValue,
|
|
79
|
-
* so a re-read equal to the prior cannot show the appended text. That fill is recorded unverified:
|
|
80
|
-
* never exact, and never a mismatch. Every fill records the shared verdict as `fillVerdict`.
|
|
81
|
-
* An empty field reads as its placeholder, so an append prior equal to it counts as empty.
|
|
82
|
-
*
|
|
83
|
-
* Return in a multi-line field adds a new line instead of submitting, so a multi-line submit is
|
|
84
|
-
* never sent and is recorded as such.
|
|
85
|
-
*
|
|
86
|
-
* An unreadable re-read is unverified and does not stop a requested Return, as on Android (D-09,
|
|
87
|
-
* D-10; owner 2026-10-09), but focus is confirmed once first and a Return is held as focus-lost
|
|
88
|
-
* when the field no longer has it (444.1 D-10).
|
|
89
|
-
*/
|
|
90
62
|
private fillIOSCompound;
|
|
91
|
-
/**
|
|
92
|
-
* EXEC-02 platform primitives injected into the shared runner scroll loops (D-02/D-03). The swipe
|
|
93
|
-
* is a page-sized directional gesture (inverted to the finger direction) that reports unknown
|
|
94
|
-
* movement; nativeSearchScroll drives
|
|
95
|
-
* the XCUITest `mobile: scroll` predicateString rung over an injection-escaped target; readInterimTree
|
|
96
|
-
* is a light page-source parse — NO refMap mint, NO screenshot, lastRefs untouched.
|
|
97
|
-
*/
|
|
98
63
|
private buildIOSScrollPrimitives;
|
|
99
|
-
/**
|
|
100
|
-
* EXEC-03 openApp (D-10/D-12, Pitfall 5). Resolve a human name to a bundle id (or validate an
|
|
101
|
-
* explicit bundleId) over the session-cached registry, then launch via activateApp. On real devices
|
|
102
|
-
* `mobile: listApps` enumerates the registry; on simulators it throws, leaving the registry
|
|
103
|
-
* unavailable — the appName lane then mints a structured §7.1 error directing to an explicit
|
|
104
|
-
* bundleId, and the bundleId lane validates without the registry (isAppInstalled / activateApp
|
|
105
|
-
* error conversion, A2). Never a best-guess launch (T-359-04); an invalid bundle never succeeds.
|
|
106
|
-
*/
|
|
107
64
|
private openApp;
|
|
108
|
-
/**
|
|
109
|
-
* Explicit `{bundleId}` launch (D-12, A2). With a registry, validate membership; without one
|
|
110
|
-
* (simulator) validate via `driver.isAppInstalled` when the driver exposes it, otherwise attempt
|
|
111
|
-
* `activateApp` and convert a driver rejection into the structured §7.1 error. An invalid bundle
|
|
112
|
-
* NEVER returns success:true.
|
|
113
|
-
*/
|
|
114
65
|
private openByBundleId;
|
|
115
66
|
/** Return the cached registry, enumerating once on first use. Failed enumeration returns null (uncached). */
|
|
116
67
|
private ensureAppRegistry;
|
|
117
|
-
/**
|
|
118
|
-
* Enumerate installed apps through the driver only (ownership contract — webdriverio only, no
|
|
119
|
-
* child_process/simctl). `mobile: listApps` maps bundle ids → AppRegistryEntry with CFBundleName
|
|
120
|
-
* labels when present (D-10 — no per-app round-trips). It throws on simulators (Pitfall 5) ⇒ null,
|
|
121
|
-
* leaving enumeration unavailable WITHOUT crashing.
|
|
122
|
-
*/
|
|
123
68
|
private enumerateApps;
|
|
124
69
|
/** Coerce a `mobile: listApps` result (bundle-id-keyed map, or an array of entries) into the registry. */
|
|
125
70
|
private mapListApps;
|
|
@@ -132,11 +77,6 @@ interface IOSAdapterConfig extends PlatformConfig {
|
|
|
132
77
|
udid?: string;
|
|
133
78
|
}
|
|
134
79
|
|
|
135
|
-
declare function createIOSSession(config: IOSAdapterConfig
|
|
136
|
-
hostname: string;
|
|
137
|
-
port: number;
|
|
138
|
-
path: string;
|
|
139
|
-
capabilities: Record<string, unknown>;
|
|
140
|
-
}): Promise<WebdriverIO.Browser>;
|
|
80
|
+
declare function createIOSSession(config: IOSAdapterConfig): Promise<WebdriverIO.Browser>;
|
|
141
81
|
|
|
142
82
|
export { type IOSAdapterConfig, IOSPlatformAdapter, createIOSSession };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PlatformAdapter, PlatformConfig, LiveStreamCapability,
|
|
1
|
+
import { PlatformAdapter, PlatformConfig, LiveStreamCapability, ActionsFor, ActionResult, ConsoleLogEntry } from '@vostride/agent-qa-runner';
|
|
2
2
|
import { ScreenState } from '@vostride/agent-qa-schema';
|
|
3
3
|
|
|
4
4
|
declare class IOSPlatformAdapter implements PlatformAdapter<'ios'> {
|
|
@@ -21,15 +21,8 @@ declare class IOSPlatformAdapter implements PlatformAdapter<'ios'> {
|
|
|
21
21
|
private snapshotDepthWarned;
|
|
22
22
|
setup(config: PlatformConfig): Promise<void>;
|
|
23
23
|
cleanup(): Promise<void>;
|
|
24
|
-
/**
|
|
25
|
-
* 428 D-11: the labelled 1s snapshot preview, over `captureScreenshot()` — `takeScreenshot()`
|
|
26
|
-
* and nothing else. Never `screenshot()`: that one switches the Appium context, and a tick
|
|
27
|
-
* landing between a step's context switch and its command would run the command in the wrong
|
|
28
|
-
* context. Declared before the driver exists (a capture then answers `undefined`), so the
|
|
29
|
-
* composition's selection never falls through to `screenshot()`.
|
|
30
|
-
*/
|
|
31
24
|
get liveStream(): LiveStreamCapability;
|
|
32
|
-
observe(
|
|
25
|
+
observe(): Promise<ScreenState>;
|
|
33
26
|
execute(action: ActionsFor<'ios'>): Promise<ActionResult>;
|
|
34
27
|
screenshot(): Promise<Buffer | undefined>;
|
|
35
28
|
pollDeviceLogs(): Promise<void>;
|
|
@@ -66,60 +59,12 @@ declare class IOSPlatformAdapter implements PlatformAdapter<'ios'> {
|
|
|
66
59
|
private readIOSPlaceholder;
|
|
67
60
|
/** Read a focused element's `value` attribute; undefined when nothing is readable. */
|
|
68
61
|
private readIOSValue;
|
|
69
|
-
/**
|
|
70
|
-
* EXEC-01 iOS fill compound (D-05..D-08). From the SAME focused-element handle: (append) capture
|
|
71
|
-
* the prior value FIRST via getAttribute('value') then addValue, or (replace) setValue; RE-READ
|
|
72
|
-
* via getAttribute('value') on that same handle; verdict via compareFillReread. Secure fields
|
|
73
|
-
* (nativeType XCUIElementTypeSecureTextField) take the non-empty-only lane — the comparator yields
|
|
74
|
-
* length sentinels so no raw value enters the result (T-359-02); bullet masking is tolerated
|
|
75
|
-
* (A3/Pitfall 7 — never a length compare). On a passing re-read AND submit, a return/newline is
|
|
76
|
-
* delivered AFTER the re-read (D-07). Single typing attempt; a mismatch is success:false + §7.1.
|
|
77
|
-
*
|
|
78
|
-
* Append (437 FA-1, owner 2026-10-08): the XCUITest `value` attribute often lags after addValue,
|
|
79
|
-
* so a re-read equal to the prior cannot show the appended text. That fill is recorded unverified:
|
|
80
|
-
* never exact, and never a mismatch. Every fill records the shared verdict as `fillVerdict`.
|
|
81
|
-
* An empty field reads as its placeholder, so an append prior equal to it counts as empty.
|
|
82
|
-
*
|
|
83
|
-
* Return in a multi-line field adds a new line instead of submitting, so a multi-line submit is
|
|
84
|
-
* never sent and is recorded as such.
|
|
85
|
-
*
|
|
86
|
-
* An unreadable re-read is unverified and does not stop a requested Return, as on Android (D-09,
|
|
87
|
-
* D-10; owner 2026-10-09), but focus is confirmed once first and a Return is held as focus-lost
|
|
88
|
-
* when the field no longer has it (444.1 D-10).
|
|
89
|
-
*/
|
|
90
62
|
private fillIOSCompound;
|
|
91
|
-
/**
|
|
92
|
-
* EXEC-02 platform primitives injected into the shared runner scroll loops (D-02/D-03). The swipe
|
|
93
|
-
* is a page-sized directional gesture (inverted to the finger direction) that reports unknown
|
|
94
|
-
* movement; nativeSearchScroll drives
|
|
95
|
-
* the XCUITest `mobile: scroll` predicateString rung over an injection-escaped target; readInterimTree
|
|
96
|
-
* is a light page-source parse — NO refMap mint, NO screenshot, lastRefs untouched.
|
|
97
|
-
*/
|
|
98
63
|
private buildIOSScrollPrimitives;
|
|
99
|
-
/**
|
|
100
|
-
* EXEC-03 openApp (D-10/D-12, Pitfall 5). Resolve a human name to a bundle id (or validate an
|
|
101
|
-
* explicit bundleId) over the session-cached registry, then launch via activateApp. On real devices
|
|
102
|
-
* `mobile: listApps` enumerates the registry; on simulators it throws, leaving the registry
|
|
103
|
-
* unavailable — the appName lane then mints a structured §7.1 error directing to an explicit
|
|
104
|
-
* bundleId, and the bundleId lane validates without the registry (isAppInstalled / activateApp
|
|
105
|
-
* error conversion, A2). Never a best-guess launch (T-359-04); an invalid bundle never succeeds.
|
|
106
|
-
*/
|
|
107
64
|
private openApp;
|
|
108
|
-
/**
|
|
109
|
-
* Explicit `{bundleId}` launch (D-12, A2). With a registry, validate membership; without one
|
|
110
|
-
* (simulator) validate via `driver.isAppInstalled` when the driver exposes it, otherwise attempt
|
|
111
|
-
* `activateApp` and convert a driver rejection into the structured §7.1 error. An invalid bundle
|
|
112
|
-
* NEVER returns success:true.
|
|
113
|
-
*/
|
|
114
65
|
private openByBundleId;
|
|
115
66
|
/** Return the cached registry, enumerating once on first use. Failed enumeration returns null (uncached). */
|
|
116
67
|
private ensureAppRegistry;
|
|
117
|
-
/**
|
|
118
|
-
* Enumerate installed apps through the driver only (ownership contract — webdriverio only, no
|
|
119
|
-
* child_process/simctl). `mobile: listApps` maps bundle ids → AppRegistryEntry with CFBundleName
|
|
120
|
-
* labels when present (D-10 — no per-app round-trips). It throws on simulators (Pitfall 5) ⇒ null,
|
|
121
|
-
* leaving enumeration unavailable WITHOUT crashing.
|
|
122
|
-
*/
|
|
123
68
|
private enumerateApps;
|
|
124
69
|
/** Coerce a `mobile: listApps` result (bundle-id-keyed map, or an array of entries) into the registry. */
|
|
125
70
|
private mapListApps;
|
|
@@ -132,11 +77,6 @@ interface IOSAdapterConfig extends PlatformConfig {
|
|
|
132
77
|
udid?: string;
|
|
133
78
|
}
|
|
134
79
|
|
|
135
|
-
declare function createIOSSession(config: IOSAdapterConfig
|
|
136
|
-
hostname: string;
|
|
137
|
-
port: number;
|
|
138
|
-
path: string;
|
|
139
|
-
capabilities: Record<string, unknown>;
|
|
140
|
-
}): Promise<WebdriverIO.Browser>;
|
|
80
|
+
declare function createIOSSession(config: IOSAdapterConfig): Promise<WebdriverIO.Browser>;
|
|
141
81
|
|
|
142
82
|
export { type IOSAdapterConfig, IOSPlatformAdapter, createIOSSession };
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
deriveMobileScroll,
|
|
21
21
|
IOS_EDITABLE_TYPES,
|
|
22
22
|
parseMobileSource,
|
|
23
|
+
printRunText,
|
|
23
24
|
warnIfOutOfBounds
|
|
24
25
|
} from "@vostride/agent-qa-core";
|
|
25
26
|
import { alignToWindow } from "@vostride/agent-qa-runner-local";
|
|
@@ -36,10 +37,10 @@ function positiveInteger(value) {
|
|
|
36
37
|
const parsed = Number(value.trim());
|
|
37
38
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
|
|
38
39
|
}
|
|
39
|
-
function resolveIOSSnapshotDepth(
|
|
40
|
-
return positiveInteger(
|
|
40
|
+
function resolveIOSSnapshotDepth() {
|
|
41
|
+
return positiveInteger(process.env[IOS_SNAPSHOT_DEPTH_ENV]) ?? IOS_SNAPSHOT_MAX_DEPTH;
|
|
41
42
|
}
|
|
42
|
-
async function createIOSSession(config
|
|
43
|
+
async function createIOSSession(config) {
|
|
43
44
|
const match = config.device?.match ?? {};
|
|
44
45
|
const appiumUrl = process.env.AGENT_QA_APPIUM_URL ?? config.appiumUrl ?? "http://localhost:4723";
|
|
45
46
|
const url = new URL(appiumUrl);
|
|
@@ -65,17 +66,6 @@ async function createIOSSession(config, farmSession) {
|
|
|
65
66
|
delete capabilities[key];
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
|
-
if (farmSession) {
|
|
69
|
-
Object.assign(capabilities, farmSession.capabilities);
|
|
70
|
-
return remote({
|
|
71
|
-
protocol: "https",
|
|
72
|
-
hostname: farmSession.hostname,
|
|
73
|
-
port: farmSession.port,
|
|
74
|
-
path: farmSession.path,
|
|
75
|
-
logLevel: config.verbose ? "info" : "silent",
|
|
76
|
-
capabilities
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
69
|
return remote({
|
|
80
70
|
hostname: url.hostname,
|
|
81
71
|
port: parseInt(url.port || "4723", 10),
|
|
@@ -124,15 +114,6 @@ function firstErrorLine(err) {
|
|
|
124
114
|
return (err instanceof Error ? err.message : String(err)).split("\n")[0];
|
|
125
115
|
}
|
|
126
116
|
var IOS_TAP_HOLD_MS = 100;
|
|
127
|
-
async function safePerform(chain) {
|
|
128
|
-
try {
|
|
129
|
-
await chain.perform();
|
|
130
|
-
} catch (err) {
|
|
131
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
132
|
-
if (msg.includes("actions") && msg.includes("DELETE")) return;
|
|
133
|
-
throw err;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
117
|
var IOS_SYSLOG_REGEX = /^.*?\s+(\S+)\[(\d+)\].*?:\s+(.*)/;
|
|
137
118
|
var IOS_APPIUM_LOG_REGEX = /^\S+\s+\S+\s+\[(\w+)\]\s+\[(\S+)\]\s+(.*)/;
|
|
138
119
|
var IOS_LEVEL_MAP = {
|
|
@@ -278,8 +259,7 @@ var IOSPlatformAdapter = class {
|
|
|
278
259
|
consoleDrainIndex = 0;
|
|
279
260
|
captureConsole = true;
|
|
280
261
|
bundleId = null;
|
|
281
|
-
//
|
|
282
|
-
// enumerated OR enumeration unavailable (iOS simulator — mobile: listApps throws). Reset in cleanup().
|
|
262
|
+
// `null` means not yet enumerated, or unavailable because simulators throw on `listApps`.
|
|
283
263
|
appRegistry = null;
|
|
284
264
|
sessionSnapshotDepth = IOS_SNAPSHOT_MAX_DEPTH;
|
|
285
265
|
snapshotDepth = IOS_SNAPSHOT_MAX_DEPTH;
|
|
@@ -289,11 +269,11 @@ var IOSPlatformAdapter = class {
|
|
|
289
269
|
this.config = config;
|
|
290
270
|
this.captureConsole = config.logCapture?.console !== false;
|
|
291
271
|
this.bundleId = this.config.bundleId ?? config.device?.match?.bundleId ?? null;
|
|
292
|
-
this.sessionSnapshotDepth = resolveIOSSnapshotDepth(
|
|
272
|
+
this.sessionSnapshotDepth = resolveIOSSnapshotDepth();
|
|
293
273
|
this.snapshotDepth = this.sessionSnapshotDepth;
|
|
294
274
|
this.readsAtLoweredDepth = 0;
|
|
295
275
|
this.snapshotDepthWarned = false;
|
|
296
|
-
this.driver = await createIOSSession(this.config
|
|
276
|
+
this.driver = await createIOSSession(this.config);
|
|
297
277
|
const bundleId = this.bundleId;
|
|
298
278
|
if (bundleId) {
|
|
299
279
|
try {
|
|
@@ -336,18 +316,12 @@ var IOSPlatformAdapter = class {
|
|
|
336
316
|
this.readsAtLoweredDepth = 0;
|
|
337
317
|
this.snapshotDepthWarned = false;
|
|
338
318
|
}
|
|
339
|
-
|
|
340
|
-
* 428 D-11: the labelled 1s snapshot preview, over `captureScreenshot()` — `takeScreenshot()`
|
|
341
|
-
* and nothing else. Never `screenshot()`: that one switches the Appium context, and a tick
|
|
342
|
-
* landing between a step's context switch and its command would run the command in the wrong
|
|
343
|
-
* context. Declared before the driver exists (a capture then answers `undefined`), so the
|
|
344
|
-
* composition's selection never falls through to `screenshot()`.
|
|
345
|
-
*/
|
|
319
|
+
// `takeScreenshot()` only: `screenshot()` switches the Appium context mid-step.
|
|
346
320
|
get liveStream() {
|
|
347
321
|
this.liveStreamCapability ??= createSnapshotLiveStream({ capture: () => this.captureScreenshot() });
|
|
348
322
|
return this.liveStreamCapability;
|
|
349
323
|
}
|
|
350
|
-
async observe(
|
|
324
|
+
async observe() {
|
|
351
325
|
if (!this.driver) {
|
|
352
326
|
throw new Error("IOSPlatformAdapter not initialized \u2014 call setup() first");
|
|
353
327
|
}
|
|
@@ -511,9 +485,6 @@ var IOSPlatformAdapter = class {
|
|
|
511
485
|
return void 0;
|
|
512
486
|
}
|
|
513
487
|
}
|
|
514
|
-
// ---------------------------------------------------------------------------
|
|
515
|
-
// Compound behaviors (EXEC-01/02/03)
|
|
516
|
-
// ---------------------------------------------------------------------------
|
|
517
488
|
async readIOSPageSource() {
|
|
518
489
|
const driver = this.driver;
|
|
519
490
|
if (this.snapshotDepth < this.sessionSnapshotDepth && this.readsAtLoweredDepth >= IOS_SNAPSHOT_PROBE_INTERVAL) {
|
|
@@ -541,8 +512,9 @@ var IOSPlatformAdapter = class {
|
|
|
541
512
|
if (!this.snapshotDepthWarned) {
|
|
542
513
|
this.snapshotDepthWarned = true;
|
|
543
514
|
const detail = (readErr instanceof Error ? readErr.message : String(readErr)).split("\n")[0].slice(0, 160);
|
|
544
|
-
|
|
545
|
-
`[ios-adapter] page source failed at snapshotMaxDepth ${failedDepth}; later reads stay at ${IOS_SNAPSHOT_FALLBACK_DEPTH} and retry ${this.sessionSnapshotDepth} every ${IOS_SNAPSHOT_PROBE_INTERVAL} reads (${detail})
|
|
515
|
+
printRunText(
|
|
516
|
+
`[ios-adapter] page source failed at snapshotMaxDepth ${failedDepth}; later reads stay at ${IOS_SNAPSHOT_FALLBACK_DEPTH} and retry ${this.sessionSnapshotDepth} every ${IOS_SNAPSHOT_PROBE_INTERVAL} reads (${detail})`,
|
|
517
|
+
(text) => console.warn(text)
|
|
546
518
|
);
|
|
547
519
|
}
|
|
548
520
|
}
|
|
@@ -554,9 +526,7 @@ var IOSPlatformAdapter = class {
|
|
|
554
526
|
return xml;
|
|
555
527
|
}
|
|
556
528
|
async tapAt(point) {
|
|
557
|
-
await
|
|
558
|
-
this.driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: Math.round(point.x), y: Math.round(point.y) }).down({ button: 0 }).pause(IOS_TAP_HOLD_MS).up({ button: 0 })
|
|
559
|
-
);
|
|
529
|
+
await this.driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: Math.round(point.x), y: Math.round(point.y) }).down({ button: 0 }).pause(IOS_TAP_HOLD_MS).up({ button: 0 }).perform();
|
|
560
530
|
}
|
|
561
531
|
async readRect(elementId) {
|
|
562
532
|
try {
|
|
@@ -688,27 +658,7 @@ var IOSPlatformAdapter = class {
|
|
|
688
658
|
return void 0;
|
|
689
659
|
}
|
|
690
660
|
}
|
|
691
|
-
|
|
692
|
-
* EXEC-01 iOS fill compound (D-05..D-08). From the SAME focused-element handle: (append) capture
|
|
693
|
-
* the prior value FIRST via getAttribute('value') then addValue, or (replace) setValue; RE-READ
|
|
694
|
-
* via getAttribute('value') on that same handle; verdict via compareFillReread. Secure fields
|
|
695
|
-
* (nativeType XCUIElementTypeSecureTextField) take the non-empty-only lane — the comparator yields
|
|
696
|
-
* length sentinels so no raw value enters the result (T-359-02); bullet masking is tolerated
|
|
697
|
-
* (A3/Pitfall 7 — never a length compare). On a passing re-read AND submit, a return/newline is
|
|
698
|
-
* delivered AFTER the re-read (D-07). Single typing attempt; a mismatch is success:false + §7.1.
|
|
699
|
-
*
|
|
700
|
-
* Append (437 FA-1, owner 2026-10-08): the XCUITest `value` attribute often lags after addValue,
|
|
701
|
-
* so a re-read equal to the prior cannot show the appended text. That fill is recorded unverified:
|
|
702
|
-
* never exact, and never a mismatch. Every fill records the shared verdict as `fillVerdict`.
|
|
703
|
-
* An empty field reads as its placeholder, so an append prior equal to it counts as empty.
|
|
704
|
-
*
|
|
705
|
-
* Return in a multi-line field adds a new line instead of submitting, so a multi-line submit is
|
|
706
|
-
* never sent and is recorded as such.
|
|
707
|
-
*
|
|
708
|
-
* An unreadable re-read is unverified and does not stop a requested Return, as on Android (D-09,
|
|
709
|
-
* D-10; owner 2026-10-09), but focus is confirmed once first and a Return is held as focus-lost
|
|
710
|
-
* when the field no longer has it (444.1 D-10).
|
|
711
|
-
*/
|
|
661
|
+
// XCUITest `value` lags after `addValue`, so an append read back as the prior is unverified.
|
|
712
662
|
async fillIOSCompound(el, action, box, secure, multiline, target) {
|
|
713
663
|
const driver = this.driver;
|
|
714
664
|
const isAppend = action.clearMode === "append";
|
|
@@ -746,13 +696,7 @@ var IOSPlatformAdapter = class {
|
|
|
746
696
|
};
|
|
747
697
|
return verdict.ok ? { success: true, ...box, ...notice, metadata } : { success: false, error: verdict.selfCorrectingError, ...box, ...notice, metadata };
|
|
748
698
|
}
|
|
749
|
-
|
|
750
|
-
* EXEC-02 platform primitives injected into the shared runner scroll loops (D-02/D-03). The swipe
|
|
751
|
-
* is a page-sized directional gesture (inverted to the finger direction) that reports unknown
|
|
752
|
-
* movement; nativeSearchScroll drives
|
|
753
|
-
* the XCUITest `mobile: scroll` predicateString rung over an injection-escaped target; readInterimTree
|
|
754
|
-
* is a light page-source parse — NO refMap mint, NO screenshot, lastRefs untouched.
|
|
755
|
-
*/
|
|
699
|
+
// iOS swipes report unknown movement; `readInterimTree` mints no refs and takes no screenshot.
|
|
756
700
|
async buildIOSScrollPrimitives(scrollDirection, maxSwipes, factor = 1) {
|
|
757
701
|
const driver = this.driver;
|
|
758
702
|
const viewport = await driver.getWindowSize();
|
|
@@ -768,9 +712,7 @@ var IOSPlatformAdapter = class {
|
|
|
768
712
|
sy = Math.round(center.y);
|
|
769
713
|
}
|
|
770
714
|
const pts = computeSwipe(sx, sy, fingerDirectionFor(direction), distance);
|
|
771
|
-
await
|
|
772
|
-
driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: pts.startX, y: pts.startY }).down({ button: 0 }).move({ x: pts.endX, y: pts.endY, duration: 300 }).up({ button: 0 })
|
|
773
|
-
);
|
|
715
|
+
await driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: pts.startX, y: pts.startY }).down({ button: 0 }).move({ x: pts.endX, y: pts.endY, duration: 300 }).up({ button: 0 }).perform();
|
|
774
716
|
return "unknown";
|
|
775
717
|
},
|
|
776
718
|
nativeSearchScroll: async (target) => {
|
|
@@ -789,14 +731,7 @@ var IOSPlatformAdapter = class {
|
|
|
789
731
|
}
|
|
790
732
|
};
|
|
791
733
|
}
|
|
792
|
-
|
|
793
|
-
* EXEC-03 openApp (D-10/D-12, Pitfall 5). Resolve a human name to a bundle id (or validate an
|
|
794
|
-
* explicit bundleId) over the session-cached registry, then launch via activateApp. On real devices
|
|
795
|
-
* `mobile: listApps` enumerates the registry; on simulators it throws, leaving the registry
|
|
796
|
-
* unavailable — the appName lane then mints a structured §7.1 error directing to an explicit
|
|
797
|
-
* bundleId, and the bundleId lane validates without the registry (isAppInstalled / activateApp
|
|
798
|
-
* error conversion, A2). Never a best-guess launch (T-359-04); an invalid bundle never succeeds.
|
|
799
|
-
*/
|
|
734
|
+
// Simulators throw on `listApps`, so `appName` needs a real device; `bundleId` works without it.
|
|
800
735
|
async openApp(action) {
|
|
801
736
|
const driver = this.driver;
|
|
802
737
|
const registry = await this.ensureAppRegistry();
|
|
@@ -835,12 +770,7 @@ var IOSPlatformAdapter = class {
|
|
|
835
770
|
}
|
|
836
771
|
}
|
|
837
772
|
}
|
|
838
|
-
|
|
839
|
-
* Explicit `{bundleId}` launch (D-12, A2). With a registry, validate membership; without one
|
|
840
|
-
* (simulator) validate via `driver.isAppInstalled` when the driver exposes it, otherwise attempt
|
|
841
|
-
* `activateApp` and convert a driver rejection into the structured §7.1 error. An invalid bundle
|
|
842
|
-
* NEVER returns success:true.
|
|
843
|
-
*/
|
|
773
|
+
// Without a registry, validate via `isAppInstalled` or convert an `activateApp` rejection.
|
|
844
774
|
async openByBundleId(bundleId, registry) {
|
|
845
775
|
const driver = this.driver;
|
|
846
776
|
if (registry !== null) {
|
|
@@ -887,12 +817,7 @@ var IOSPlatformAdapter = class {
|
|
|
887
817
|
if (enumerated !== null) this.appRegistry = enumerated;
|
|
888
818
|
return enumerated;
|
|
889
819
|
}
|
|
890
|
-
|
|
891
|
-
* Enumerate installed apps through the driver only (ownership contract — webdriverio only, no
|
|
892
|
-
* child_process/simctl). `mobile: listApps` maps bundle ids → AppRegistryEntry with CFBundleName
|
|
893
|
-
* labels when present (D-10 — no per-app round-trips). It throws on simulators (Pitfall 5) ⇒ null,
|
|
894
|
-
* leaving enumeration unavailable WITHOUT crashing.
|
|
895
|
-
*/
|
|
820
|
+
// Driver only, no simctl; `listApps` throws on simulators, which leaves enumeration unavailable.
|
|
896
821
|
async enumerateApps() {
|
|
897
822
|
const driver = this.driver;
|
|
898
823
|
try {
|
|
@@ -1069,9 +994,7 @@ var IOSPlatformAdapter = class {
|
|
|
1069
994
|
warnIfOutOfBounds({ x: pts.startX, y: pts.startY }, { width, height }, "swipe");
|
|
1070
995
|
warnIfOutOfBounds({ x: pts.endX, y: pts.endY }, { width, height }, "swipe");
|
|
1071
996
|
const swipeDuration = action.durationMs;
|
|
1072
|
-
await
|
|
1073
|
-
driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: pts.startX, y: pts.startY }).down({ button: 0 }).move({ x: pts.endX, y: pts.endY, duration: swipeDuration }).up({ button: 0 })
|
|
1074
|
-
);
|
|
997
|
+
await driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: pts.startX, y: pts.startY }).down({ button: 0 }).move({ x: pts.endX, y: pts.endY, duration: swipeDuration }).up({ button: 0 }).perform();
|
|
1075
998
|
return {
|
|
1076
999
|
success: true,
|
|
1077
1000
|
startCoordinates: { x: pts.startX, y: pts.startY },
|
|
@@ -1122,9 +1045,7 @@ var IOSPlatformAdapter = class {
|
|
|
1122
1045
|
}
|
|
1123
1046
|
case "longpress": {
|
|
1124
1047
|
const { center } = resolver.resolve(action.ref);
|
|
1125
|
-
await
|
|
1126
|
-
driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: Math.round(center.x), y: Math.round(center.y) }).down({ button: 0 }).pause(action.duration ?? 1e3).up({ button: 0 })
|
|
1127
|
-
);
|
|
1048
|
+
await driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: Math.round(center.x), y: Math.round(center.y) }).down({ button: 0 }).pause(action.duration ?? 1e3).up({ button: 0 }).perform();
|
|
1128
1049
|
return { success: true };
|
|
1129
1050
|
}
|
|
1130
1051
|
case "waitFor": {
|
|
@@ -1312,25 +1233,19 @@ var IOSPlatformAdapter = class {
|
|
|
1312
1233
|
success: false,
|
|
1313
1234
|
error: 'assertValue {source:"network"} (HTTP log inspection) is not available on the iOS adapter. Use a web target for network assertions, or assert on visible UI text.'
|
|
1314
1235
|
};
|
|
1315
|
-
case "expr-result":
|
|
1316
|
-
return { success: true, data: void 0 };
|
|
1317
1236
|
}
|
|
1318
1237
|
}
|
|
1319
1238
|
case "drag": {
|
|
1320
1239
|
const from = resolver.resolve(action.fromRef);
|
|
1321
1240
|
const to = resolver.resolve(action.toRef);
|
|
1322
|
-
await
|
|
1323
|
-
driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: Math.round(from.center.x), y: Math.round(from.center.y) }).down({ button: 0 }).pause(300).move({ x: Math.round(to.center.x), y: Math.round(to.center.y), duration: 500 }).up({ button: 0 })
|
|
1324
|
-
);
|
|
1241
|
+
await driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: Math.round(from.center.x), y: Math.round(from.center.y) }).down({ button: 0 }).pause(300).move({ x: Math.round(to.center.x), y: Math.round(to.center.y), duration: 500 }).up({ button: 0 }).perform();
|
|
1325
1242
|
return { success: true };
|
|
1326
1243
|
}
|
|
1327
1244
|
case "doubleTap": {
|
|
1328
1245
|
const { center, bounds } = resolver.resolve(action.ref);
|
|
1329
1246
|
const x = Math.round(center.x);
|
|
1330
1247
|
const y = Math.round(center.y);
|
|
1331
|
-
await
|
|
1332
|
-
driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x, y }).down({ button: 0 }).up({ button: 0 }).pause(50).down({ button: 0 }).up({ button: 0 })
|
|
1333
|
-
);
|
|
1248
|
+
await driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x, y }).down({ button: 0 }).up({ button: 0 }).pause(50).down({ button: 0 }).up({ button: 0 }).perform();
|
|
1334
1249
|
return { success: true, coordinates: center, boundingBox: bounds };
|
|
1335
1250
|
}
|
|
1336
1251
|
case "openApp":
|
|
@@ -1423,16 +1338,10 @@ var IOSPlatformAdapter = class {
|
|
|
1423
1338
|
end = { x: midX, y: Math.round(height * 0.4) };
|
|
1424
1339
|
break;
|
|
1425
1340
|
}
|
|
1426
|
-
await
|
|
1427
|
-
driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: start.x, y: start.y }).down({ button: 0 }).move({ x: end.x, y: end.y, duration: 300 }).up({ button: 0 })
|
|
1428
|
-
);
|
|
1341
|
+
await driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: start.x, y: start.y }).down({ button: 0 }).move({ x: end.x, y: end.y, duration: 300 }).up({ button: 0 }).perform();
|
|
1429
1342
|
return { success: true, startCoordinates: start, endCoordinates: end };
|
|
1430
1343
|
}
|
|
1431
|
-
// Runner-
|
|
1432
|
-
// setVariable are intercepted at the runner dispatch seam and never reach the adapter. A
|
|
1433
|
-
// grouped §7.1 defensive case surfaces a dispatch-routing error if one arrives here;
|
|
1434
|
-
// assertNever remains the backstop for a genuinely unknown schema verb. (uniqueValue is
|
|
1435
|
-
// web-only and is not part of ActionsFor<'ios'>.)
|
|
1344
|
+
// Runner-channel verbs never reach the adapter; one arriving here is a routing error.
|
|
1436
1345
|
case "reportValue":
|
|
1437
1346
|
case "assertAI":
|
|
1438
1347
|
case "extractAI":
|