@delacour/warden-e2e 0.3.1 → 0.4.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-8em9kgsk.mjs → chunk-3874jt2j.mjs} +125 -30
- package/index.mjs +1 -1
- package/package.json +2 -2
- package/preload.mjs +59 -10
- package/types/core/src/exec.d.ts +2 -0
- package/types/core/src/providers/provider.types.d.ts +7 -0
- package/types/e2e/src/devtools/cdp.d.ts +7 -2
- package/types/e2e/src/devtools/devtools.types.d.ts +13 -0
- package/types/e2e/src/devtools/session.d.ts +8 -1
- package/types/e2e/src/driver/ios/ios-driver.d.ts +7 -6
- package/types/e2e/src/driver/ios/simd-protocol.d.ts +23 -0
- package/types/e2e/src/driver/ios/type-budget.d.ts +8 -4
- package/types/e2e/src/driver/observation-cache.d.ts +3 -1
- package/types/e2e/src/driver/typing.d.ts +10 -0
- package/types/e2e/src/preload.d.ts +5 -0
- package/types/e2e/src/screen/field.d.ts +11 -1
- package/types/e2e/src/screen/locator.d.ts +24 -5
|
@@ -64,10 +64,17 @@ var READS = new Set([
|
|
|
64
64
|
"previous",
|
|
65
65
|
"peek"
|
|
66
66
|
]);
|
|
67
|
+
var WAITS = new Set(["settle"]);
|
|
67
68
|
var READS_WITHOUT_ARGUMENT = new Set(["orientation", "appearance", "clipboard"]);
|
|
68
69
|
function isCall(value) {
|
|
69
70
|
return typeof value === "function";
|
|
70
71
|
}
|
|
72
|
+
function reportsQuiet(result) {
|
|
73
|
+
if (typeof result !== "object" || result === null || !("success" in result) || result.success !== true)
|
|
74
|
+
return false;
|
|
75
|
+
const data = "data" in result ? result.data : undefined;
|
|
76
|
+
return typeof data === "object" && data !== null && "quiet" in data && data.quiet === true;
|
|
77
|
+
}
|
|
71
78
|
function isThenable(value) {
|
|
72
79
|
return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
|
|
73
80
|
}
|
|
@@ -137,21 +144,31 @@ function cacheObservations(driver, options = {}) {
|
|
|
137
144
|
return { success: true, data: reader.alertIn(taken.data) };
|
|
138
145
|
};
|
|
139
146
|
};
|
|
140
|
-
const
|
|
147
|
+
const edged = (fn, target, edge) => {
|
|
141
148
|
return (...args) => {
|
|
142
|
-
|
|
149
|
+
edge();
|
|
143
150
|
let result;
|
|
144
151
|
try {
|
|
145
152
|
result = Reflect.apply(fn, target, args);
|
|
146
153
|
} catch (error) {
|
|
147
|
-
|
|
154
|
+
edge();
|
|
148
155
|
throw error;
|
|
149
156
|
}
|
|
150
157
|
if (!isThenable(result)) {
|
|
151
|
-
|
|
158
|
+
edge();
|
|
152
159
|
return result;
|
|
153
160
|
}
|
|
154
|
-
return Promise.resolve(result).finally(
|
|
161
|
+
return Promise.resolve(result).finally(edge);
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
const asInput = (fn, target) => edged(fn, target, inputEdge);
|
|
165
|
+
const asWait = (fn, target) => {
|
|
166
|
+
const waited = edged(fn, target, moveOn);
|
|
167
|
+
return async (...args) => {
|
|
168
|
+
const result = await waited(...args);
|
|
169
|
+
if (reportsQuiet(result))
|
|
170
|
+
lastInputAt = undefined;
|
|
171
|
+
return result;
|
|
155
172
|
};
|
|
156
173
|
};
|
|
157
174
|
const wrapMember = (name, value, target) => {
|
|
@@ -159,6 +176,8 @@ function cacheObservations(driver, options = {}) {
|
|
|
159
176
|
return wrap(value, {});
|
|
160
177
|
if (!isCall(value) || READS.has(name))
|
|
161
178
|
return value;
|
|
179
|
+
if (WAITS.has(name))
|
|
180
|
+
return asWait(value, target);
|
|
162
181
|
const input = asInput(value, target);
|
|
163
182
|
if (!READS_WITHOUT_ARGUMENT.has(name))
|
|
164
183
|
return input;
|
|
@@ -271,6 +290,9 @@ function canSelectAll(driver) {
|
|
|
271
290
|
function canSetText(driver) {
|
|
272
291
|
return typeof driver.setText === "function";
|
|
273
292
|
}
|
|
293
|
+
function canPasteText(driver) {
|
|
294
|
+
return typeof driver.pasteText === "function";
|
|
295
|
+
}
|
|
274
296
|
var segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
275
297
|
function graphemesOf(text) {
|
|
276
298
|
return Array.from(segmenter.segment(text), (part) => part.segment);
|
|
@@ -921,7 +943,7 @@ async function focusAndClear(driver, sleep, point, entry, method = "auto") {
|
|
|
921
943
|
return prepared;
|
|
922
944
|
return prepared.data.pending === "selection" ? pressKeys(driver, sleep, "Backspace", 1) : ok(undefined);
|
|
923
945
|
}
|
|
924
|
-
async function focusClearType(driver, sleep, point, entry, text, method = "auto", env = process.env) {
|
|
946
|
+
async function focusClearType(driver, sleep, point, entry, text, method = "auto", env = process.env, via = "paste") {
|
|
925
947
|
const wantsSetText = method === "setText" || method === "auto" && setTextOptedIn(env);
|
|
926
948
|
if (wantsSetText && entry !== undefined && fieldContent(entry).kind === "text" && canSetText(driver)) {
|
|
927
949
|
const focused = await driver.tap(point);
|
|
@@ -930,15 +952,20 @@ async function focusClearType(driver, sleep, point, entry, text, method = "auto"
|
|
|
930
952
|
const replaced = await driver.setText(text);
|
|
931
953
|
if (replaced.success)
|
|
932
954
|
return replaced;
|
|
933
|
-
return typeOver(driver, sleep, await prepareFocused(driver, sleep, entry, method), text);
|
|
955
|
+
return typeOver(driver, sleep, await prepareFocused(driver, sleep, entry, method), text, "type");
|
|
934
956
|
}
|
|
935
|
-
return typeOver(driver, sleep, await focusAndSelect(driver, sleep, point, entry, method), text);
|
|
957
|
+
return typeOver(driver, sleep, await focusAndSelect(driver, sleep, point, entry, method), text, via);
|
|
936
958
|
}
|
|
937
|
-
async function typeOver(driver, sleep, prepared, text) {
|
|
959
|
+
async function typeOver(driver, sleep, prepared, text, via) {
|
|
938
960
|
if (!prepared.success)
|
|
939
961
|
return prepared;
|
|
940
962
|
if (text === "")
|
|
941
963
|
return prepared.data.pending === "selection" ? pressKeys(driver, sleep, "Backspace", 1) : ok(undefined);
|
|
964
|
+
if (via === "paste" && canPasteText(driver)) {
|
|
965
|
+
const pasted = await driver.pasteText(text);
|
|
966
|
+
if (pasted.success)
|
|
967
|
+
return pasted;
|
|
968
|
+
}
|
|
942
969
|
return typeText(driver, sleep, text);
|
|
943
970
|
}
|
|
944
971
|
|
|
@@ -1750,10 +1777,20 @@ class Locator {
|
|
|
1750
1777
|
const reason = blockedReason(entry, spec, observed);
|
|
1751
1778
|
if (reason !== undefined)
|
|
1752
1779
|
return { done: false, state: { kind: "blocked", reason, snapshot: observed.snapshot } };
|
|
1753
|
-
if (!anyFrame && !this.heldStill(entry, observation.earlier))
|
|
1754
|
-
|
|
1780
|
+
if (!anyFrame && !this.heldStill(entry, observation.earlier)) {
|
|
1781
|
+
const compared = observation.earlier?.previous !== undefined;
|
|
1782
|
+
if (compared || !await this.stillByHitTest(entry, observed))
|
|
1783
|
+
return "moving";
|
|
1784
|
+
}
|
|
1755
1785
|
return { done: true, value: { entry, observed } };
|
|
1756
1786
|
}
|
|
1787
|
+
async stillByHitTest(entry, observed) {
|
|
1788
|
+
const { driver } = observed;
|
|
1789
|
+
if (driver.capabilities.cheapHitTest !== true || this.rt.host.answerSystemAlert !== undefined)
|
|
1790
|
+
return false;
|
|
1791
|
+
const hit = await driver.elementAtPoint(tapPointOf(entry));
|
|
1792
|
+
return hit.success && hit.data !== null && sameElement(hit.data, entry.element);
|
|
1793
|
+
}
|
|
1757
1794
|
heldStill(entry, earlier) {
|
|
1758
1795
|
if (earlier === undefined)
|
|
1759
1796
|
return true;
|
|
@@ -1851,14 +1888,15 @@ class Locator {
|
|
|
1851
1888
|
});
|
|
1852
1889
|
}
|
|
1853
1890
|
async fill(text, options = {}) {
|
|
1854
|
-
assertOptions("fill", options, ["timeout"]);
|
|
1891
|
+
assertOptions("fill", options, ["via", "timeout"]);
|
|
1892
|
+
const via = fillViaOf(this, options.via);
|
|
1855
1893
|
return this.act("fill", async () => {
|
|
1856
1894
|
const budget = this.rt.budget(options.timeout);
|
|
1857
1895
|
let target = await this.actionable({ enabled: true, tapPointOnScreen: false }, budget.remaining());
|
|
1858
1896
|
let firstHeld;
|
|
1859
1897
|
for (let attempt = 1;; attempt += 1) {
|
|
1860
1898
|
const point = focusPoint(target.entry.element.frame);
|
|
1861
|
-
this.rt.drive("fill", await focusClearType(target.observed.driver, this.pause, point, target.entry, text, clearMethodFor(attempt)));
|
|
1899
|
+
this.rt.drive("fill", await focusClearType(target.observed.driver, this.pause, point, target.entry, text, clearMethodFor(attempt), process.env, viaFor(attempt, via)));
|
|
1862
1900
|
if (target.entry.secure)
|
|
1863
1901
|
return;
|
|
1864
1902
|
const outcome = await this.confirm(target, text, budget);
|
|
@@ -2067,6 +2105,14 @@ function retryPlan(options) {
|
|
|
2067
2105
|
function describeUntil(until) {
|
|
2068
2106
|
return typeof until === "function" ? "check" : String(until);
|
|
2069
2107
|
}
|
|
2108
|
+
function fillViaOf(locator, via) {
|
|
2109
|
+
if (via === undefined || via === "paste" || via === "type")
|
|
2110
|
+
return via ?? "paste";
|
|
2111
|
+
throw new E2eError2("INVALID_ARGUMENT", `${locator}: fill via must be "paste" or "type", got ${JSON.stringify(via)}`);
|
|
2112
|
+
}
|
|
2113
|
+
function viaFor(attempt, via) {
|
|
2114
|
+
return attempt === 1 ? via : "type";
|
|
2115
|
+
}
|
|
2070
2116
|
function clearMethodFor(attempt) {
|
|
2071
2117
|
return attempt === 1 ? "auto" : "counted";
|
|
2072
2118
|
}
|
|
@@ -2313,7 +2359,7 @@ async function openCdp(wsUrl, options = {}) {
|
|
|
2313
2359
|
if (method)
|
|
2314
2360
|
dispatch(method, params);
|
|
2315
2361
|
});
|
|
2316
|
-
ws.addEventListener("close", () => {
|
|
2362
|
+
ws.addEventListener("close", (event) => {
|
|
2317
2363
|
if (closed)
|
|
2318
2364
|
return;
|
|
2319
2365
|
closed = true;
|
|
@@ -2326,7 +2372,7 @@ async function openCdp(wsUrl, options = {}) {
|
|
|
2326
2372
|
binding.settle(devtoolsErr("cdp_closed", "CDP connection closed"));
|
|
2327
2373
|
if (!intentional)
|
|
2328
2374
|
for (const handler of [...closeHandlers])
|
|
2329
|
-
handler();
|
|
2375
|
+
handler({ code: event.code, reason: event.reason });
|
|
2330
2376
|
});
|
|
2331
2377
|
const subscribe = (set, handler) => {
|
|
2332
2378
|
set.add(handler);
|
|
@@ -2427,7 +2473,8 @@ var stackTraceSchema = z2.looseObject({
|
|
|
2427
2473
|
callFrames: z2.array(z2.looseObject({
|
|
2428
2474
|
url: z2.string().optional(),
|
|
2429
2475
|
lineNumber: z2.number().optional(),
|
|
2430
|
-
columnNumber: z2.number().optional()
|
|
2476
|
+
columnNumber: z2.number().optional(),
|
|
2477
|
+
functionName: z2.string().optional()
|
|
2431
2478
|
}))
|
|
2432
2479
|
});
|
|
2433
2480
|
var consoleCalledSchema = z2.looseObject({
|
|
@@ -2438,7 +2485,7 @@ var consoleCalledSchema = z2.looseObject({
|
|
|
2438
2485
|
});
|
|
2439
2486
|
var exceptionThrownSchema = z2.looseObject({
|
|
2440
2487
|
timestamp: z2.number().optional(),
|
|
2441
|
-
exceptionDetails: exceptionDetailsSchema
|
|
2488
|
+
exceptionDetails: exceptionDetailsSchema.extend({ stackTrace: stackTraceSchema.optional() })
|
|
2442
2489
|
});
|
|
2443
2490
|
var LEVELS = {
|
|
2444
2491
|
log: "log",
|
|
@@ -2529,6 +2576,20 @@ function sourceOf(url, line, col) {
|
|
|
2529
2576
|
return;
|
|
2530
2577
|
return { url, line: (line ?? 0) + 1, col: (col ?? 0) + 1 };
|
|
2531
2578
|
}
|
|
2579
|
+
function framesOf(trace) {
|
|
2580
|
+
const frames = [];
|
|
2581
|
+
for (const frame of trace?.callFrames ?? []) {
|
|
2582
|
+
if (!frame.url)
|
|
2583
|
+
continue;
|
|
2584
|
+
frames.push({
|
|
2585
|
+
file: frame.url,
|
|
2586
|
+
line: (frame.lineNumber ?? 0) + 1,
|
|
2587
|
+
col: (frame.columnNumber ?? 0) + 1,
|
|
2588
|
+
...frame.functionName ? { fn: frame.functionName } : {}
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2591
|
+
return frames.length > 0 ? frames : undefined;
|
|
2592
|
+
}
|
|
2532
2593
|
var lineKey = (ts, text) => `${ts}\x00${text}`;
|
|
2533
2594
|
function createConsoleCapture(options = {}) {
|
|
2534
2595
|
const ring = options.ring ?? DEFAULT_RING;
|
|
@@ -2576,13 +2637,15 @@ function createConsoleCapture(options = {}) {
|
|
|
2576
2637
|
const formatted = formatConsoleArgs(args);
|
|
2577
2638
|
const frame = stackTrace?.callFrames[0];
|
|
2578
2639
|
const source = sourceOf(frame?.url, frame?.lineNumber, frame?.columnNumber);
|
|
2640
|
+
const stack = framesOf(stackTrace);
|
|
2579
2641
|
return {
|
|
2580
2642
|
ts: timestamp ?? now(),
|
|
2581
2643
|
level: LEVELS[type] ?? "log",
|
|
2582
2644
|
kind: "console",
|
|
2583
2645
|
text: formatted.text,
|
|
2584
2646
|
args: formatted.args,
|
|
2585
|
-
...source ? { source } : {}
|
|
2647
|
+
...source ? { source } : {},
|
|
2648
|
+
...stack ? { stack } : {}
|
|
2586
2649
|
};
|
|
2587
2650
|
};
|
|
2588
2651
|
const fromException = (params) => {
|
|
@@ -2591,12 +2654,14 @@ function createConsoleCapture(options = {}) {
|
|
|
2591
2654
|
return;
|
|
2592
2655
|
const { exceptionDetails: details, timestamp } = parsed.data;
|
|
2593
2656
|
const source = sourceOf(details.url, details.lineNumber, details.columnNumber);
|
|
2657
|
+
const stack = framesOf(details.stackTrace);
|
|
2594
2658
|
return {
|
|
2595
2659
|
ts: timestamp ?? now(),
|
|
2596
2660
|
level: "error",
|
|
2597
2661
|
kind: "exception",
|
|
2598
2662
|
text: describeException(details),
|
|
2599
|
-
...source ? { source } : {}
|
|
2663
|
+
...source ? { source } : {},
|
|
2664
|
+
...stack ? { stack } : {}
|
|
2600
2665
|
};
|
|
2601
2666
|
};
|
|
2602
2667
|
return {
|
|
@@ -3033,7 +3098,8 @@ async function connectDevtools(o) {
|
|
|
3033
3098
|
const cdpHandlers = new Map;
|
|
3034
3099
|
const eventHandlers = {
|
|
3035
3100
|
reconnected: new Set,
|
|
3036
|
-
lost: new Set
|
|
3101
|
+
lost: new Set,
|
|
3102
|
+
yielded: new Set
|
|
3037
3103
|
};
|
|
3038
3104
|
let unsubscribeCdp = [];
|
|
3039
3105
|
let connection = 0;
|
|
@@ -3083,9 +3149,17 @@ async function connectDevtools(o) {
|
|
|
3083
3149
|
for (const handler of handlers)
|
|
3084
3150
|
unsubscribeCdp.push(attached.cdp.on(method, handler));
|
|
3085
3151
|
}
|
|
3086
|
-
attached.cdp.onClose(() => {
|
|
3087
|
-
if (state.kind
|
|
3088
|
-
|
|
3152
|
+
attached.cdp.onClose((info) => {
|
|
3153
|
+
if (state.kind !== "up" || state.attached.cdp !== attached.cdp)
|
|
3154
|
+
return;
|
|
3155
|
+
if (o.yieldOnClose?.(info)) {
|
|
3156
|
+
state = { kind: "closed" };
|
|
3157
|
+
log(`devtools: yielded (${info.code} ${info.reason})`);
|
|
3158
|
+
for (const handler of [...eventHandlers.yielded])
|
|
3159
|
+
handler(info);
|
|
3160
|
+
return;
|
|
3161
|
+
}
|
|
3162
|
+
reconnect("socket closed");
|
|
3089
3163
|
});
|
|
3090
3164
|
attached.cdp.on("Runtime.executionContextsCleared", () => {
|
|
3091
3165
|
if (state.kind === "up" && state.attached.cdp === attached.cdp)
|
|
@@ -3937,6 +4011,25 @@ function spawnFailure(cmd, cwd, error) {
|
|
|
3937
4011
|
return `cannot run \`${cmd.join(" ")}\`: no such directory ${cwd}`;
|
|
3938
4012
|
return `cannot run \`${cmd.join(" ")}\`: ${error instanceof Error ? error.message : String(error)}`;
|
|
3939
4013
|
}
|
|
4014
|
+
async function readLines(stream, onLine) {
|
|
4015
|
+
const decoder = new TextDecoder;
|
|
4016
|
+
let text = "";
|
|
4017
|
+
let pending = "";
|
|
4018
|
+
for await (const chunk of stream) {
|
|
4019
|
+
const piece = decoder.decode(chunk, { stream: true });
|
|
4020
|
+
text += piece;
|
|
4021
|
+
const lines = (pending + piece).split(/\r?\n/);
|
|
4022
|
+
pending = lines.pop() ?? "";
|
|
4023
|
+
for (const line of lines)
|
|
4024
|
+
onLine(line);
|
|
4025
|
+
}
|
|
4026
|
+
const rest = decoder.decode();
|
|
4027
|
+
text += rest;
|
|
4028
|
+
pending += rest;
|
|
4029
|
+
if (pending !== "")
|
|
4030
|
+
onLine(pending);
|
|
4031
|
+
return text;
|
|
4032
|
+
}
|
|
3940
4033
|
var bunExec = async (cmd, opts = {}) => {
|
|
3941
4034
|
try {
|
|
3942
4035
|
const proc = Bun.spawn([...cmd], {
|
|
@@ -3948,11 +4041,9 @@ var bunExec = async (cmd, opts = {}) => {
|
|
|
3948
4041
|
timeout: opts.timeoutMs,
|
|
3949
4042
|
...opts.signal !== undefined ? { signal: opts.signal } : {}
|
|
3950
4043
|
});
|
|
3951
|
-
const
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
proc.exited
|
|
3955
|
-
]);
|
|
4044
|
+
const { onLine } = opts;
|
|
4045
|
+
const read = (stream) => onLine ? readLines(stream, onLine) : new Response(stream).text();
|
|
4046
|
+
const [stdout, stderr, exitCode] = await Promise.all([read(proc.stdout), read(proc.stderr), proc.exited]);
|
|
3956
4047
|
return { exitCode, stdout, stderr };
|
|
3957
4048
|
} catch (error) {
|
|
3958
4049
|
return { exitCode: 127, stdout: "", stderr: spawnFailure(cmd, opts.cwd, error) };
|
|
@@ -4186,6 +4277,8 @@ var frontmostDataSchema = z6.looseObject({
|
|
|
4186
4277
|
pid: z6.number().optional(),
|
|
4187
4278
|
frontmostPid: z6.number().optional()
|
|
4188
4279
|
});
|
|
4280
|
+
var launchDataSchema = z6.looseObject({ pid: z6.number() });
|
|
4281
|
+
var terminateDataSchema = z6.looseObject({ terminated: z6.boolean() });
|
|
4189
4282
|
var orientationDataSchema = z6.object({
|
|
4190
4283
|
orientation: z6.enum([
|
|
4191
4284
|
"portrait",
|
|
@@ -4207,7 +4300,9 @@ var opSchemas = {
|
|
|
4207
4300
|
rebind: rebindDataSchema,
|
|
4208
4301
|
alertButtons: alertButtonsDataSchema,
|
|
4209
4302
|
frontmost: frontmostDataSchema,
|
|
4210
|
-
orientation: orientationDataSchema
|
|
4303
|
+
orientation: orientationDataSchema,
|
|
4304
|
+
launch: launchDataSchema,
|
|
4305
|
+
terminate: terminateDataSchema
|
|
4211
4306
|
};
|
|
4212
4307
|
function hasOpSchema(op) {
|
|
4213
4308
|
return Object.hasOwn(opSchemas, op);
|
|
@@ -5855,6 +5950,6 @@ function onlyWasSeen() {
|
|
|
5855
5950
|
|
|
5856
5951
|
// ../../packages/e2e/src/version.ts
|
|
5857
5952
|
var WORKSPACE_VERSION = "0.0.0-workspace";
|
|
5858
|
-
var WARDEN_E2E_VERSION2 = "0.
|
|
5953
|
+
var WARDEN_E2E_VERSION2 = "0.4.0";
|
|
5859
5954
|
|
|
5860
5955
|
export { E2eError2, noteSpanDetail, addSpanDetail, canObserve, canDetectAlerts, settleDetailOf, DEFAULT_SETTLE_QUIET_MS, DEFAULT_SETTLE_TIMEOUT_MS, settleByHash, snapshotHash, ok, err, swipeVector, screenRect, fieldValue, PINCH_MS, ROTATE_MS, PATH_MS, centreOf, pinchRadius, rotateRadius, requireGesture, assertScale, assertDegrees, isInteractiveRole, resolveRole, normalizeText, matchesText, indexSnapshot, testIdQuery, textQuery, roleQuery, selectorExpression, renderSnapshot, renderHeader, resolve, locateByTestIdViaReact, isLocator, Locator, pickOne, textOf, scrollUntilVisible, bunExec, execError, driverError, driverErr, logEventSchema, fatalEventSchema, responseSchema, mapWireError, encodeRequest, bunSpawn, stdioTransport, spawnSimd, connectSimd, isHidLost, ENV_NAMES, DEFAULT_ASSERTION_TIMEOUT_MS, readEnv, runStep, setDefaultHidRecovery, setDefaultNativeLog, setDefaultDriverFactory, hasDefaultDriverFactory, setDefaultFixtureFactory, hasDefaultFixtureFactory, attemptSession, getSession, parseBudgets, createDevicePerf, perfStartFor, spawnWarden, hash6, attemptDir, screenshotPath, specIdOf, realNativeLogDeps, captureNativeLog, callerFile, BUN_TIMEOUT_MARGIN_MS2, ownHook, currentScope, createScope, runInScope, hooksFor, resolveUse, registrationDefaults, markOnly, onlyWasSeen, WORKSPACE_VERSION, WARDEN_E2E_VERSION2 };
|
package/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delacour/warden-e2e",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "TypeScript spec API for `warden e2e`: test, expect, screen, device, app",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"LICENSE"
|
|
38
38
|
],
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@delacour/warden": "0.
|
|
40
|
+
"@delacour/warden": "0.4.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"zod": "^4.4.3"
|
package/preload.mjs
CHANGED
|
@@ -68,7 +68,7 @@ import {
|
|
|
68
68
|
onlyWasSeen,
|
|
69
69
|
WORKSPACE_VERSION,
|
|
70
70
|
WARDEN_E2E_VERSION2
|
|
71
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-3874jt2j.mjs";
|
|
72
72
|
|
|
73
73
|
// ../../packages/e2e/src/preload.ts
|
|
74
74
|
import { afterAll, setDefaultTimeout } from "bun:test";
|
|
@@ -3238,8 +3238,8 @@ function createSimctl(deps) {
|
|
|
3238
3238
|
|
|
3239
3239
|
// ../../packages/e2e/src/driver/ios/type-budget.ts
|
|
3240
3240
|
var TYPE_BASE_MS = 1e4;
|
|
3241
|
-
var TYPE_PER_CHAR_MS =
|
|
3242
|
-
var TYPE_PER_REPEAT_MS =
|
|
3241
|
+
var TYPE_PER_CHAR_MS = 100;
|
|
3242
|
+
var TYPE_PER_REPEAT_MS = 200;
|
|
3243
3243
|
function typeTextBudgetMs(text) {
|
|
3244
3244
|
const characters = Array.from(text);
|
|
3245
3245
|
let repeats = 0;
|
|
@@ -3257,6 +3257,8 @@ var SETTLE_MARGIN_MS = 2000;
|
|
|
3257
3257
|
var REBIND_TIMEOUT_MS = 3000;
|
|
3258
3258
|
var GESTURE_MARGIN_MS = 5000;
|
|
3259
3259
|
var FRONTMOST_TIMEOUT_MS = 3000;
|
|
3260
|
+
var LAUNCH_TIMEOUT_MS = 30000;
|
|
3261
|
+
var APP_OP_TIMEOUT_MS = 15000;
|
|
3260
3262
|
var KEY_USAGE = {
|
|
3261
3263
|
Enter: 40,
|
|
3262
3264
|
Escape: 41,
|
|
@@ -3348,6 +3350,9 @@ function restOf(error, text) {
|
|
|
3348
3350
|
function decodeBase64(png) {
|
|
3349
3351
|
return new Uint8Array(Buffer.from(png, "base64"));
|
|
3350
3352
|
}
|
|
3353
|
+
function predatesOp(error) {
|
|
3354
|
+
return error.code === "E_UNSUPPORTED" || error.code === "E_BAD_REQUEST" && /unknown (input )?op/.test(error.message);
|
|
3355
|
+
}
|
|
3351
3356
|
function createIosDriver(swap, simctl, opts) {
|
|
3352
3357
|
const { udid, now, sleep } = opts;
|
|
3353
3358
|
const { client } = swap;
|
|
@@ -3359,6 +3364,7 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
3359
3364
|
let settleUnsupported = !nativeSettle;
|
|
3360
3365
|
let rebindUnsupported = false;
|
|
3361
3366
|
let gestureUnsupported = false;
|
|
3367
|
+
let appOpsOutdated = false;
|
|
3362
3368
|
const capabilities = {
|
|
3363
3369
|
pinch: true,
|
|
3364
3370
|
maxPointers: 2,
|
|
@@ -3426,6 +3432,30 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
3426
3432
|
}
|
|
3427
3433
|
return result;
|
|
3428
3434
|
};
|
|
3435
|
+
async function appOp(op, p, timeoutMs) {
|
|
3436
|
+
if (appOpsOutdated) {
|
|
3437
|
+
noteSpanDetail("via", "simctl");
|
|
3438
|
+
return;
|
|
3439
|
+
}
|
|
3440
|
+
const result = await request(op, p, { timeoutMs });
|
|
3441
|
+
if (!result.success && predatesOp(result.error)) {
|
|
3442
|
+
appOpsOutdated = true;
|
|
3443
|
+
noteSpanDetail("via", "simctl");
|
|
3444
|
+
noteSpanDetail("reason", "helper_outdated");
|
|
3445
|
+
opts.log?.(`[ios] this warden-simd predates the \`${op}\` op (${result.error.code}: ${result.error.message}); app lifecycle goes through simctl until the helper is restarted (warden ui close --udid ${udid}) or rebuilt (bun run --cwd packages/e2e native:build)`);
|
|
3446
|
+
return;
|
|
3447
|
+
}
|
|
3448
|
+
noteSpanDetail("via", "simd");
|
|
3449
|
+
return result;
|
|
3450
|
+
}
|
|
3451
|
+
async function appOutcome(viaHelper, fallback, note) {
|
|
3452
|
+
if (viaHelper === undefined)
|
|
3453
|
+
return fallback();
|
|
3454
|
+
if (!viaHelper.success)
|
|
3455
|
+
return viaHelper;
|
|
3456
|
+
note(viaHelper.data);
|
|
3457
|
+
return okVoid;
|
|
3458
|
+
}
|
|
3429
3459
|
const fetchSnapshot = async (o) => {
|
|
3430
3460
|
const result = await request("snapshot", params({ interactiveOnly: o?.interactiveOnly, backend: o?.backend }));
|
|
3431
3461
|
if (!result.success)
|
|
@@ -3463,11 +3493,17 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
3463
3493
|
const rest = restOf(typed.error, text);
|
|
3464
3494
|
if (rest === undefined)
|
|
3465
3495
|
return typed;
|
|
3466
|
-
const copied = await simctl.pbcopy(rest);
|
|
3467
|
-
if (!copied.success)
|
|
3468
|
-
return copied;
|
|
3469
3496
|
opts.log?.(`[ios] typeText: pasting ${rest.length} unmappable character(s)`);
|
|
3470
|
-
return
|
|
3497
|
+
return pasteText(rest);
|
|
3498
|
+
}
|
|
3499
|
+
async function pasteText(text) {
|
|
3500
|
+
if (text === "")
|
|
3501
|
+
return ok(undefined);
|
|
3502
|
+
const copied = await simctl.pbcopy(text);
|
|
3503
|
+
const result = copied.success ? await sendVoid("paste") : copied;
|
|
3504
|
+
if (!result.success)
|
|
3505
|
+
opts.log?.(`[ios] pasteText failed (${result.error.code}): ${result.error.message}`);
|
|
3506
|
+
return result;
|
|
3471
3507
|
}
|
|
3472
3508
|
async function hashSettle(o) {
|
|
3473
3509
|
try {
|
|
@@ -3631,6 +3667,7 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
3631
3667
|
typeText,
|
|
3632
3668
|
key: (key, modifiers) => sendVoid("key", params({ usage: KEY_USAGE[key], modifiers: modifiers && [...modifiers] })),
|
|
3633
3669
|
selectAll: () => sendVoid("selectAll"),
|
|
3670
|
+
pasteText,
|
|
3634
3671
|
pressButton,
|
|
3635
3672
|
screenshot,
|
|
3636
3673
|
orientation,
|
|
@@ -3647,7 +3684,8 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
3647
3684
|
},
|
|
3648
3685
|
app: {
|
|
3649
3686
|
launch: async (bundleId, o) => {
|
|
3650
|
-
const
|
|
3687
|
+
const viaHelper = await appOp("launch", params({ bundleId, args: o?.args && [...o.args], env: o?.env, terminateRunning: o?.relaunch !== false }), LAUNCH_TIMEOUT_MS);
|
|
3688
|
+
const launched = await appOutcome(viaHelper, () => simctl.launch(bundleId, o), (data) => noteSpanDetail("pid", data.pid));
|
|
3651
3689
|
if (launched.success) {
|
|
3652
3690
|
launchedBundle = bundleId;
|
|
3653
3691
|
if (await rebind(bundleId))
|
|
@@ -3656,12 +3694,18 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
3656
3694
|
return launched;
|
|
3657
3695
|
},
|
|
3658
3696
|
terminate: async (bundleId) => {
|
|
3659
|
-
const
|
|
3697
|
+
const viaHelper = await appOp("terminate", { bundleId }, APP_OP_TIMEOUT_MS);
|
|
3698
|
+
const terminated = await appOutcome(viaHelper, () => simctl.terminate(bundleId), (data) => noteSpanDetail("terminated", data.terminated));
|
|
3660
3699
|
if (terminated.success && launchedBundle === bundleId)
|
|
3661
3700
|
launchedBundle = undefined;
|
|
3662
3701
|
return terminated;
|
|
3663
3702
|
},
|
|
3664
|
-
openUrl: (url) =>
|
|
3703
|
+
openUrl: async (url) => {
|
|
3704
|
+
const viaHelper = await appOp("openUrl", { url }, APP_OP_TIMEOUT_MS);
|
|
3705
|
+
return appOutcome(viaHelper, () => simctl.openUrl(url), () => {
|
|
3706
|
+
return;
|
|
3707
|
+
});
|
|
3708
|
+
},
|
|
3665
3709
|
install: (path) => simctl.install(path),
|
|
3666
3710
|
uninstall: (bundleId) => simctl.uninstall(bundleId),
|
|
3667
3711
|
isInstalled: (bundleId) => simctl.isInstalled(bundleId),
|
|
@@ -4773,6 +4817,10 @@ ${read.error}
|
|
|
4773
4817
|
}
|
|
4774
4818
|
});
|
|
4775
4819
|
}
|
|
4820
|
+
function scrubRunnerEnv(env) {
|
|
4821
|
+
delete env.BUN_BE_BUN;
|
|
4822
|
+
}
|
|
4823
|
+
scrubRunnerEnv(process.env);
|
|
4776
4824
|
if (!hasDefaultDriverFactory()) {
|
|
4777
4825
|
setDefaultDriverFactory(defaultDriverFactory);
|
|
4778
4826
|
setDefaultNativeLog(realNativeLogDeps(bunExec, androidTools(process.env).adb));
|
|
@@ -4796,5 +4844,6 @@ export {
|
|
|
4796
4844
|
BUN_TIMEOUT_MARGIN_MS2 as BUN_TIMEOUT_MARGIN_MS,
|
|
4797
4845
|
installPreload,
|
|
4798
4846
|
isCi,
|
|
4847
|
+
scrubRunnerEnv,
|
|
4799
4848
|
versionMismatch
|
|
4800
4849
|
};
|
package/types/core/src/exec.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type ExecOptions = {
|
|
|
9
9
|
timeoutMs?: number;
|
|
10
10
|
/** aborting kills the command (SIGTERM), like a timeout */
|
|
11
11
|
signal?: AbortSignal;
|
|
12
|
+
/** each line of stdout and stderr as it arrives (no trailing newline); the result still carries the whole output */
|
|
13
|
+
onLine?: (line: string) => void;
|
|
12
14
|
};
|
|
13
15
|
/** Injectable process runner — providers take this so tests can fake `simctl`/`adb`. */
|
|
14
16
|
export type Exec = (cmd: readonly string[], opts?: ExecOptions) => Promise<ExecResult>;
|
|
@@ -31,6 +31,13 @@ export interface DeviceProvider {
|
|
|
31
31
|
bootReady?(id: string, timeoutMs: number): Promise<BootReady>;
|
|
32
32
|
/** Probe for a device `bootReady` just booted and saw ready. Absent = `probe`. */
|
|
33
33
|
probeBooted?(id: string): AsyncResult<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Best effort, called for a device this claim is about to boot from shutdown (after a create, or a
|
|
36
|
+
* shutdown reuse), never for an already-booted one. A failure is logged by the provider, never fatal.
|
|
37
|
+
*/
|
|
38
|
+
prepareBoot?(id: string): Promise<void>;
|
|
39
|
+
/** Called once the device is ready, before the health probe. Best effort. */
|
|
40
|
+
afterReady?(id: string): Promise<void>;
|
|
34
41
|
}
|
|
35
42
|
/** `bootReady`: a boot failure is an error; a device that booted but never got ready is unhealthy. */
|
|
36
43
|
export type BootReady = {
|
|
@@ -6,6 +6,11 @@ export type CdpOptions = {
|
|
|
6
6
|
/** connect timeout and the default per-request timeout */
|
|
7
7
|
timeoutMs?: number;
|
|
8
8
|
};
|
|
9
|
+
/** How a socket closed: the WebSocket close code and reason (Metro's proxy puts a `[TAG]` in the reason). */
|
|
10
|
+
export type CloseInfo = {
|
|
11
|
+
code: number;
|
|
12
|
+
reason: string;
|
|
13
|
+
};
|
|
9
14
|
export type CdpClient = {
|
|
10
15
|
/** `Runtime.evaluate`-style JSON-RPC call; resolves with the reply's `result`. */
|
|
11
16
|
send: (method: string, params?: Record<string, unknown>, options?: {
|
|
@@ -13,8 +18,8 @@ export type CdpClient = {
|
|
|
13
18
|
}) => Promise<DevtoolsResult<unknown>>;
|
|
14
19
|
/** Subscribe to a CDP event by method name; returns the unsubscribe function. */
|
|
15
20
|
on: (event: string, handler: (params: unknown) => void) => () => void;
|
|
16
|
-
/** Called when the socket closes on its own (not after `close()`). */
|
|
17
|
-
onClose: (handler: () => void) => () => void;
|
|
21
|
+
/** Called when the socket closes on its own (not after `close()`), with the close code and reason Metro sent. */
|
|
22
|
+
onClose: (handler: (info: CloseInfo) => void) => () => void;
|
|
18
23
|
/** Every event of every method, in arrival order (a wildcard `on`). */
|
|
19
24
|
onAny: (handler: (method: string, params: unknown) => void) => () => void;
|
|
20
25
|
/**
|
|
@@ -54,6 +54,15 @@ export type CdpTapEvent = {
|
|
|
54
54
|
};
|
|
55
55
|
export type CdpTap = (event: CdpTapEvent) => void;
|
|
56
56
|
export type ConsoleLevel = "log" | "info" | "warn" | "error" | "debug";
|
|
57
|
+
/** One stack frame; `line`/`col` are 1-based. Raw frames carry the bundle URL, symbolicated ones the source file. */
|
|
58
|
+
export type StackFrame = {
|
|
59
|
+
file: string;
|
|
60
|
+
line: number;
|
|
61
|
+
col: number;
|
|
62
|
+
fn?: string;
|
|
63
|
+
/** library / runtime internals Metro marks collapsible */
|
|
64
|
+
collapsed?: boolean;
|
|
65
|
+
};
|
|
57
66
|
/** One captured console message (`console`), uncaught exception (`exception`) or `mark()` (`note`). */
|
|
58
67
|
export type ConsoleLine = {
|
|
59
68
|
/** epoch ms (the runtime's own timestamp when it sent one) */
|
|
@@ -67,6 +76,10 @@ export type ConsoleLine = {
|
|
|
67
76
|
line: number;
|
|
68
77
|
col: number;
|
|
69
78
|
};
|
|
79
|
+
/** the runtime's stack when it sent one; the broker rewrites it to original files (symbolication) */
|
|
80
|
+
stack?: StackFrame[];
|
|
81
|
+
/** Metro's code frame around the first app frame (symbolication) */
|
|
82
|
+
codeFrame?: string;
|
|
70
83
|
/** each console argument rendered on its own (console messages only) */
|
|
71
84
|
args?: string[];
|
|
72
85
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Result } from "../../../types/src/result";
|
|
2
|
+
import { type CloseInfo } from "./cdp";
|
|
2
3
|
import { type CdpTap, type DevtoolsError, type DevtoolsResult, type EvaluateError, type Fetch, type Sleep } from "./devtools.types";
|
|
3
4
|
import { type SelectedTarget } from "./inspector";
|
|
4
5
|
/** The one binding warden installs; scripts call `__warden_cb(JSON.stringify({ requestId, … }))`. */
|
|
@@ -35,6 +36,11 @@ export type ConnectDevtoolsOptions = {
|
|
|
35
36
|
taps?: readonly CdpTap[];
|
|
36
37
|
/** how long `reloadJs` waits for the app to start tearing its JS context down (default 5 s) */
|
|
37
38
|
reloadWaitMs?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Asked when the socket closes on its own. `true` = another debugger took the page: do not reconnect (that would
|
|
41
|
+
* kick it in turn), close the session and emit `yielded`. Default: always reconnect.
|
|
42
|
+
*/
|
|
43
|
+
yieldOnClose?: (info: CloseInfo) => boolean;
|
|
38
44
|
};
|
|
39
45
|
export type DevtoolsSession = {
|
|
40
46
|
/** `Runtime.evaluate` by value. A thrown JS error fails with `exceptionDetails`. Waits out a reconnect. */
|
|
@@ -64,7 +70,7 @@ export type DevtoolsSession = {
|
|
|
64
70
|
/** Tap on every CDP event, handshake events flagged `replay`; survives reconnects. */
|
|
65
71
|
onEvent: (tap: CdpTap) => () => void;
|
|
66
72
|
evaluateWithBinding: (script: string, requestId: string, timeoutMs?: number) => Promise<DevtoolsResult<unknown>>;
|
|
67
|
-
/** `reconnected` (new target attached)
|
|
73
|
+
/** `reconnected` (new target attached), `lost` (gave up reconnecting), `yielded` (`yieldOnClose` said stop) */
|
|
68
74
|
on: <E extends keyof SessionEvents>(event: E, handler: (arg: SessionEvents[E]) => void) => () => void;
|
|
69
75
|
close: () => void;
|
|
70
76
|
};
|
|
@@ -72,6 +78,7 @@ export type ReloadMethod = "expo" | "devsettings" | "http";
|
|
|
72
78
|
export type SessionEvents = {
|
|
73
79
|
reconnected: SelectedTarget;
|
|
74
80
|
lost: DevtoolsError;
|
|
81
|
+
yielded: CloseInfo;
|
|
75
82
|
};
|
|
76
83
|
/**
|
|
77
84
|
* Attach to the app's Hermes runtime through Metro's inspector proxy. Connect sequence (same as Chrome's Fusebox
|
|
@@ -2,20 +2,21 @@ import type { Exec } from "../../../../core/src/exec";
|
|
|
2
2
|
import type { E2eEnv } from "../../env";
|
|
3
3
|
import type { ProbesFrontmost, ReadsAlertOn } from "../alerts";
|
|
4
4
|
import type { Driver, DriverResult, KeyName, Logger } from "../driver.types";
|
|
5
|
-
import type { SelectsAll } from "../typing";
|
|
5
|
+
import type { PastesText, SelectsAll } from "../typing";
|
|
6
6
|
import { type SwappableClient } from "./ios-session";
|
|
7
7
|
import { type Simctl } from "./simctl";
|
|
8
8
|
import { type SimdClient } from "./simd-client";
|
|
9
9
|
import type { SimdSpawn } from "./simd-transport";
|
|
10
10
|
/**
|
|
11
|
-
* The iOS `Driver`: input and
|
|
12
|
-
* helper spawned on stdio)
|
|
13
|
-
* `typeText` pastes what the HID keyboard map
|
|
14
|
-
* helper has no native quiescence, `screenshot` uses
|
|
11
|
+
* The iOS `Driver`: input, AX and app launch / terminate / openUrl through `warden-simd` (socket when
|
|
12
|
+
* `WARDEN_SIMD_SOCKET` is set, else a helper spawned on stdio); install / uninstall / isRunning / clearData,
|
|
13
|
+
* clipboard and appearance through `xcrun simctl`. Fallbacks: `typeText` pastes what the HID keyboard map
|
|
14
|
+
* cannot type, `settle` polls snapshot hashes when the helper has no native quiescence, `screenshot` uses
|
|
15
|
+
* `simctl io` when the helper cannot, and a helper built before the app ops leaves app lifecycle to `simctl`.
|
|
15
16
|
*/
|
|
16
17
|
export type IosDriverEnv = Pick<E2eEnv, "simd" | "simdSocket" | "helperDir">;
|
|
17
18
|
/** The iOS driver is a `Driver` plus the optional capabilities of `driver/alerts.ts`. */
|
|
18
|
-
export type IosDriver = Driver & SelectsAll & ReadsAlertOn & ProbesFrontmost;
|
|
19
|
+
export type IosDriver = Driver & SelectsAll & PastesText & ReadsAlertOn & ProbesFrontmost;
|
|
19
20
|
export type OpenIosDriverOptions = {
|
|
20
21
|
udid: string;
|
|
21
22
|
env?: IosDriverEnv;
|
|
@@ -275,6 +275,21 @@ export declare const frontmostDataSchema: z.ZodObject<{
|
|
|
275
275
|
frontmostPid: z.ZodOptional<z.ZodNumber>;
|
|
276
276
|
}, z.core.$loose>;
|
|
277
277
|
export type FrontmostData = z.infer<typeof frontmostDataSchema>;
|
|
278
|
+
/**
|
|
279
|
+
* `launch{bundleId, args?, env?, terminateRunning?}`: the helper starts the app in the process that already holds
|
|
280
|
+
* CoreSimulator (no `simctl` process per launch). `args` follow the executable, `env` reaches the app as given, and
|
|
281
|
+
* `terminateRunning` (default true) ends a running instance first; false brings a running app to the front instead.
|
|
282
|
+
* `pid` is the process that is running when the op answers.
|
|
283
|
+
*/
|
|
284
|
+
export declare const launchDataSchema: z.ZodObject<{
|
|
285
|
+
pid: z.ZodNumber;
|
|
286
|
+
}, z.core.$loose>;
|
|
287
|
+
export type LaunchData = z.infer<typeof launchDataSchema>;
|
|
288
|
+
/** `terminate{bundleId}`: `terminated: false` says nothing was running, which is the wanted state, not an error. */
|
|
289
|
+
export declare const terminateDataSchema: z.ZodObject<{
|
|
290
|
+
terminated: z.ZodBoolean;
|
|
291
|
+
}, z.core.$loose>;
|
|
292
|
+
export type TerminateData = z.infer<typeof terminateDataSchema>;
|
|
278
293
|
/**
|
|
279
294
|
* `orientation`: a real read may also say `faceUp` / `faceDown` / `unknown` (flat device); `source` tells
|
|
280
295
|
* whether the value was read from the device, is the last one this helper wrote, or is an assumed portrait.
|
|
@@ -401,6 +416,12 @@ export declare const opSchemas: {
|
|
|
401
416
|
assumed: "assumed";
|
|
402
417
|
}>>;
|
|
403
418
|
}, z.core.$strip>;
|
|
419
|
+
readonly launch: z.ZodObject<{
|
|
420
|
+
pid: z.ZodNumber;
|
|
421
|
+
}, z.core.$loose>;
|
|
422
|
+
readonly terminate: z.ZodObject<{
|
|
423
|
+
terminated: z.ZodBoolean;
|
|
424
|
+
}, z.core.$loose>;
|
|
404
425
|
};
|
|
405
426
|
export type OpDataMap = {
|
|
406
427
|
health: HealthData;
|
|
@@ -412,6 +433,8 @@ export type OpDataMap = {
|
|
|
412
433
|
alertButtons: AlertButtonsData;
|
|
413
434
|
frontmost: FrontmostData;
|
|
414
435
|
orientation: OrientationData;
|
|
436
|
+
launch: LaunchData;
|
|
437
|
+
terminate: TerminateData;
|
|
415
438
|
};
|
|
416
439
|
export type OpData<Op extends string> = Op extends keyof OpDataMap ? OpDataMap[Op] : unknown;
|
|
417
440
|
export declare function hasOpSchema(op: string): op is keyof typeof opSchemas;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Per-request budget for the helper's `typeText`: it holds, paces and (for a repeated key) waits per character.
|
|
3
|
+
* The helper's defaults (pace 5, hold 20, repeat gap 50) cost ~38 ms per character on a quiet local simulator; the
|
|
4
|
+
* budget allows ~100 ms (the EAS VirtualMac ran ~3.7x the nominal sleeps) and 200 ms per repeated key.
|
|
5
|
+
*/
|
|
2
6
|
export declare const TYPE_BASE_MS = 10000;
|
|
3
|
-
export declare const TYPE_PER_CHAR_MS =
|
|
4
|
-
export declare const TYPE_PER_REPEAT_MS =
|
|
5
|
-
/** Base + ~
|
|
7
|
+
export declare const TYPE_PER_CHAR_MS = 100;
|
|
8
|
+
export declare const TYPE_PER_REPEAT_MS = 200;
|
|
9
|
+
/** Base + ~100 ms per character + 200 ms per character equal to the one before it. */
|
|
6
10
|
export declare function typeTextBudgetMs(text: string): number;
|
|
@@ -8,7 +8,9 @@ import type { AlertInfo, Driver, DriverResult, Platform, Snapshot } from "./driv
|
|
|
8
8
|
* the one driver: whatever calls it, an input moves the *generation* on and the kept snapshot dies with the old one.
|
|
9
9
|
* A snapshot is also dropped once it is older than the platform's max age, input or not (the app can change by itself).
|
|
10
10
|
* Which reads may reuse one is the caller's call (`observe({ reuse })`): only the first look of a wait does, so a
|
|
11
|
-
* poll never spins on a tree that stopped being true.
|
|
11
|
+
* poll never spins on a tree that stopped being true. A `settle` is a wait, not an input: the kept snapshot predates
|
|
12
|
+
* it and is dropped, but nothing was pressed, so `previous().sinceInputMs` still counts from the last real input; one
|
|
13
|
+
* that reports `quiet` is the app's word that the input has landed, and then no input is recent at all.
|
|
12
14
|
*/
|
|
13
15
|
/** How old a kept snapshot may be and still answer a read. iOS reads cost ~100-300 ms; an Android dump takes ~1.9 s. */
|
|
14
16
|
export declare const OBSERVATION_MAX_AGE_MS: Record<Platform, number>;
|
|
@@ -42,6 +42,16 @@ export type SetsText = {
|
|
|
42
42
|
setText(text: string): DriverResult<void>;
|
|
43
43
|
};
|
|
44
44
|
export declare function canSetText(driver: Driver): driver is Driver & SetsText;
|
|
45
|
+
/**
|
|
46
|
+
* Optional fast path next to the frozen `Driver`: put `text` on the device pasteboard and paste it into the focused
|
|
47
|
+
* field with one Cmd+V (iOS `simctl.pbcopy` then `warden-simd` `paste`), replacing the selection. Cost is flat (~400 ms)
|
|
48
|
+
* where typing is 70-260 ms per character. A pasteboard write that cannot be confirmed is an error (nothing was
|
|
49
|
+
* pasted) and the caller types the old way.
|
|
50
|
+
*/
|
|
51
|
+
export type PastesText = {
|
|
52
|
+
pasteText(text: string): DriverResult<void>;
|
|
53
|
+
};
|
|
54
|
+
export declare function canPasteText(driver: Driver): driver is Driver & PastesText;
|
|
45
55
|
/** Types `text` at the caret. Android types the whole string in one call. */
|
|
46
56
|
export declare function typeText(driver: Driver, sleep: Sleep, text: string): DriverResult<void>;
|
|
47
57
|
/** Presses `key` `times` times, paced like typed characters. */
|
|
@@ -27,3 +27,8 @@ export declare function isCi(env: Readonly<Record<string, string | undefined>>):
|
|
|
27
27
|
* session eagerly and, in `afterAll`, closes it and (under `CI`) fails the run if a `.only` was collected.
|
|
28
28
|
*/
|
|
29
29
|
export declare function installPreload(deps: PreloadDeps): void;
|
|
30
|
+
/**
|
|
31
|
+
* `warden e2e` may run this process as a compiled warden with `BUN_BE_BUN=1`; drop it so a `warden` the spec spawns
|
|
32
|
+
* (`WARDEN_BIN`, `device.perf`) runs as warden, not bun.
|
|
33
|
+
*/
|
|
34
|
+
export declare function scrubRunnerEnv(env: Record<string, string | undefined>): void;
|
|
@@ -101,6 +101,12 @@ export type ClearMethod = "auto" | "counted" | "setText";
|
|
|
101
101
|
export declare const SETTEXT_ENV = "WARDEN_ANDROID_SETTEXT";
|
|
102
102
|
/** Whether `env` opts `auto` fills into `setText` (`SETTEXT_ENV`). */
|
|
103
103
|
export declare function setTextOptedIn(env: Readonly<Record<string, string | undefined>>): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* How `focusClearType` puts text in. `paste` writes it to the pasteboard and sends one Cmd+V on a driver that offers
|
|
106
|
+
* `pasteText` (iOS: ~400 ms flat against 70-260 ms per typed character), and types when it does not or when the paste
|
|
107
|
+
* fails; `type` always types.
|
|
108
|
+
*/
|
|
109
|
+
export type FillVia = "paste" | "type";
|
|
104
110
|
/**
|
|
105
111
|
* Taps `point` and empties what `entry` shows: select-all and one Backspace when the driver offers it, else one
|
|
106
112
|
* Backspace per character (see `focusAndSelect`). The clear routine behind the spec `clear` and `warden ui clear`.
|
|
@@ -117,5 +123,9 @@ export declare function focusAndClear(driver: Driver, sleep: Sleep, point: Point
|
|
|
117
123
|
* select-all then typing; the read-back after a `fill` checks it took, and the counted retry types. An empty field gains
|
|
118
124
|
* nothing from it, and a secure one cannot be read back, so both are typed as before. A `setText` that fails falls back
|
|
119
125
|
* to select-all and typing, without a second tap.
|
|
126
|
+
*
|
|
127
|
+
* With `via: "paste"` (the default) on a driver with `pasteText` (iOS), the text replaces the selection with one paste,
|
|
128
|
+
* secure fields included (they keep their Backspace-per-dot clearing). A paste that fails (pasteboard write not
|
|
129
|
+
* confirmed) is typed in the same attempt; nothing was pasted, so the selection is still there to be replaced.
|
|
120
130
|
*/
|
|
121
|
-
export declare function focusClearType(driver: Driver, sleep: Sleep, point: Point, entry: NodeEntry | undefined, text: string, method?: ClearMethod, env?: Readonly<Record<string, string | undefined
|
|
131
|
+
export declare function focusClearType(driver: Driver, sleep: Sleep, point: Point, entry: NodeEntry | undefined, text: string, method?: ClearMethod, env?: Readonly<Record<string, string | undefined>>, via?: FillVia): DriverResult<void>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { KeyName, Modifier, Point, Rect } from "../driver/driver.types";
|
|
2
2
|
import { type Momentum, type SwipeDirection } from "./actions";
|
|
3
3
|
import type { Observed, Runtime } from "./context";
|
|
4
|
+
import { type FillVia } from "./field";
|
|
4
5
|
import type { NodeEntry } from "./nodes";
|
|
5
6
|
import { type RoleOptions, type TestIdOptions, type TextQueryOptions } from "./queries";
|
|
6
7
|
import { type LocatorExpression } from "./query";
|
|
@@ -39,6 +40,10 @@ export type TapOptions = ActionOptions & {
|
|
|
39
40
|
/** gap between checks of `until`. Default 250 ms. Needs `until`. */
|
|
40
41
|
pollMs?: number;
|
|
41
42
|
};
|
|
43
|
+
/** `fill` puts the text in by paste where the driver can (iOS) or types it: see `FillVia`. A retry always types. */
|
|
44
|
+
export type FillOptions = ActionOptions & {
|
|
45
|
+
via?: FillVia;
|
|
46
|
+
};
|
|
42
47
|
export type LongPressOptions = ActionOptions & {
|
|
43
48
|
durationMs?: number;
|
|
44
49
|
};
|
|
@@ -137,12 +142,21 @@ export declare class Locator {
|
|
|
137
142
|
* driver with a cheap hit-test first tries `aimFromKept`, which checks the kept snapshot with a hit-test instead.)
|
|
138
143
|
* A fresh snapshot can also be of a screen still moving, so a match is taken only once its frame held still across
|
|
139
144
|
* two looks with no input between them (Playwright's "stable"): the look a read or an `expect` kept, when there is
|
|
140
|
-
* one, costs nothing; with none, a look less than `STABLE_INPUT_GAP_MS` after an input is checked by one more
|
|
141
|
-
*
|
|
145
|
+
* one, costs nothing; with none, a look less than `STABLE_INPUT_GAP_MS` after an input is checked by one more: a
|
|
146
|
+
* hit-test at the element where that is cheap (`stillByHitTest`), else a snapshot. An element that never stops
|
|
147
|
+
* moving is taken after `MAX_STABILITY_LOOKS` extra looks.
|
|
142
148
|
*/
|
|
143
149
|
private actionable;
|
|
144
150
|
/** One look of `actionable`: done, not yet, or `moving` (the match has not held still yet; look again at once). */
|
|
145
151
|
private actionableLook;
|
|
152
|
+
/**
|
|
153
|
+
* The second look of the stable-frame rule as a hit-test, where that is cheap (`capabilities.cheapHitTest`, iOS:
|
|
154
|
+
* one element back instead of the whole tree): the element held still when the hit at its tap point is that element
|
|
155
|
+
* (`sameElement`, as in `aimFromKept`). Only for a look with nothing to compare against (the one that followed an
|
|
156
|
+
* input): a look that differs from the one before it is known to be moving. Never with system alerts answered: the
|
|
157
|
+
* app-scoped hit-test may not see an alert that came up since the look, the full second look does.
|
|
158
|
+
*/
|
|
159
|
+
private stillByHitTest;
|
|
146
160
|
/**
|
|
147
161
|
* Whether `entry`'s frame, to 1 pt, is where the look before this one had this locator's one match. Without that look:
|
|
148
162
|
* still unless an input ended less than `STABLE_INPUT_GAP_MS` before this one. No observation cache = still (no history).
|
|
@@ -173,7 +187,10 @@ export declare class Locator {
|
|
|
173
187
|
*/
|
|
174
188
|
private tapOnce;
|
|
175
189
|
/**
|
|
176
|
-
* Polls `plan.until` for `plan.windowMs`, one look at the screen per check
|
|
190
|
+
* Polls `plan.until` for `plan.windowMs`, one look at the screen per check, the first one right after the press
|
|
191
|
+
* (`driver.tap` returns once the HID press is over, ~85 ms, and an AX read queues behind the app's main thread, so
|
|
192
|
+
* that look usually already shows the screen the press made; measured: holding it back to `pollMs` saved no read
|
|
193
|
+
* and cost 250 ms a press). A locator is checked on the snapshot of
|
|
177
194
|
* the poll itself, which is also compared with `pressedOn` (the hash of the screen the press aimed from): once the
|
|
178
195
|
* screen changed the press landed, so it is never sent again (`landed`, even when `until` is late); a screen still
|
|
179
196
|
* unchanged on `UNCHANGED_CHECKS` checks in a row from `UNCHANGED_AFTER_MS` on is pressed again at once. A function
|
|
@@ -188,10 +205,12 @@ export declare class Locator {
|
|
|
188
205
|
* Replaces the content of a text field: focus with the caret at the end, empty it (select all and type over the selection when
|
|
189
206
|
* the driver can, nothing for a field that is empty or shows a placeholder), type, then poll until the field holds
|
|
190
207
|
* the text on consecutive reads (the accessibility value lags the keys). A mismatch is retried once with one Backspace
|
|
191
|
-
* per character and a retype (a field that refuses select-all).
|
|
208
|
+
* per character and a retype (a field that refuses select-all). `via` (default `"paste"`) is how the text goes in on
|
|
209
|
+
* the first attempt: a paste on iOS, typing where the driver cannot paste or when the pasteboard write fails; the
|
|
210
|
+
* retry always types, so a field that mangles a paste (a mask, a maxLength) still ends up right. Secure fields cannot be read back, so they are not
|
|
192
211
|
* verified.
|
|
193
212
|
*/
|
|
194
|
-
fill(text: string, options?:
|
|
213
|
+
fill(text: string, options?: FillOptions): Promise<void>;
|
|
195
214
|
/** Empties a text field (select all and one Backspace when the driver can), then checks it is empty (one more pass of counted Backspaces when it is not). */
|
|
196
215
|
clear(options?: ActionOptions): Promise<void>;
|
|
197
216
|
/** Types at the caret; the field is tapped (caret to the end) first unless it already has focus. */
|