@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.
@@ -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 asInput = (fn, target) => {
147
+ const edged = (fn, target, edge) => {
141
148
  return (...args) => {
142
- inputEdge();
149
+ edge();
143
150
  let result;
144
151
  try {
145
152
  result = Reflect.apply(fn, target, args);
146
153
  } catch (error) {
147
- inputEdge();
154
+ edge();
148
155
  throw error;
149
156
  }
150
157
  if (!isThenable(result)) {
151
- inputEdge();
158
+ edge();
152
159
  return result;
153
160
  }
154
- return Promise.resolve(result).finally(inputEdge);
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
- return "moving";
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 === "up" && state.attached.cdp === attached.cdp)
3088
- reconnect("socket closed");
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 [stdout, stderr, exitCode] = await Promise.all([
3952
- new Response(proc.stdout).text(),
3953
- new Response(proc.stderr).text(),
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.3.1";
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
@@ -31,7 +31,7 @@ import {
31
31
  registrationDefaults,
32
32
  markOnly,
33
33
  WARDEN_E2E_VERSION2
34
- } from "./chunk-8em9kgsk.mjs";
34
+ } from "./chunk-3874jt2j.mjs";
35
35
  // ../../packages/e2e/src/expect/expect.ts
36
36
  import { expect as bunExpect } from "bun:test";
37
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delacour/warden-e2e",
3
- "version": "0.3.1",
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.3.1"
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-8em9kgsk.mjs";
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 = 70;
3242
- var TYPE_PER_REPEAT_MS = 300;
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 sendVoid("paste");
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 launched = await simctl.launch(bundleId, o);
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 terminated = await simctl.terminate(bundleId);
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) => simctl.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
  };
@@ -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) and `lost` (gave up reconnecting) */
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 AX through `warden-simd` (socket when `WARDEN_SIMD_SOCKET` is set, else a
12
- * helper spawned on stdio), app lifecycle / clipboard / appearance through `xcrun simctl`. Fallbacks:
13
- * `typeText` pastes what the HID keyboard map cannot type, `settle` polls snapshot hashes when the
14
- * helper has no native quiescence, `screenshot` uses `simctl io` when the helper cannot.
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
- /** Per-request budget for the helper's `typeText`: it holds, paces and (for a repeated key) waits per character. */
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 = 70;
4
- export declare const TYPE_PER_REPEAT_MS = 300;
5
- /** Base + ~70 ms per character + 300 ms per character equal to the one before it. */
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>>): DriverResult<void>;
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. An
141
- * element that never stops moving is taken after `MAX_STABILITY_LOOKS` extra looks.
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. A locator is checked on the snapshot of
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). Secure fields cannot be read back, so they are not
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?: ActionOptions): Promise<void>;
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. */