@delacour/warden-e2e 0.13.0 → 0.14.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-tnr47n9c.mjs → chunk-zk3wxa70.mjs} +552 -229
- package/index.mjs +113 -12
- package/package.json +2 -2
- package/preload.mjs +72 -34
- package/types/e2e/src/driver/bridge-stall.d.ts +37 -0
- package/types/e2e/src/driver/driver.types.d.ts +5 -1
- package/types/e2e/src/driver/ios/ios-driver.d.ts +10 -0
- package/types/e2e/src/driver/ios/simd-client.d.ts +2 -0
- package/types/e2e/src/driver/ios/simd-protocol.d.ts +30 -0
- package/types/e2e/src/driver/settle.d.ts +8 -0
- package/types/e2e/src/errors.d.ts +3 -1
- package/types/e2e/src/expect/matchers.d.ts +5 -0
- package/types/e2e/src/runner/bridge-evidence.d.ts +12 -0
- package/types/e2e/src/runner/results.d.ts +29 -0
- package/types/e2e/src/screen/context.d.ts +8 -11
- package/types/e2e/src/screen/degraded-read.d.ts +23 -0
- package/types/e2e/src/screen/locator.d.ts +46 -1
- package/types/e2e/src/screen/react-locator.d.ts +17 -2
- package/types/e2e/src/screen/react-merge.d.ts +10 -0
package/index.mjs
CHANGED
|
@@ -3,17 +3,19 @@ import {
|
|
|
3
3
|
E2eError2,
|
|
4
4
|
a11yTargetOf,
|
|
5
5
|
canObserve,
|
|
6
|
+
isOnScreen,
|
|
6
7
|
fieldValue,
|
|
7
8
|
normalizeText,
|
|
8
9
|
matchesText,
|
|
9
10
|
renderSnapshot,
|
|
10
11
|
renderHeader,
|
|
11
|
-
resolve,
|
|
12
12
|
isLocator,
|
|
13
13
|
pickOne,
|
|
14
14
|
textOf,
|
|
15
15
|
formatNativeTail,
|
|
16
16
|
captureNativeLog,
|
|
17
|
+
bridgeStallFromSpan,
|
|
18
|
+
formatBridgeStall,
|
|
17
19
|
isRecoverableLoss,
|
|
18
20
|
parseDuration,
|
|
19
21
|
runStep,
|
|
@@ -44,7 +46,7 @@ import {
|
|
|
44
46
|
registrationDefaults,
|
|
45
47
|
markOnly,
|
|
46
48
|
WARDEN_E2E_VERSION2
|
|
47
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-zk3wxa70.mjs";
|
|
48
50
|
// ../../packages/e2e/src/expect/expect.ts
|
|
49
51
|
import { expect as bunExpect } from "bun:test";
|
|
50
52
|
|
|
@@ -148,6 +150,15 @@ ${screen}`, screen);
|
|
|
148
150
|
var NEGATION_GRACE_MS = 1000;
|
|
149
151
|
var NEGATION_QUIET_MS = 250;
|
|
150
152
|
var NOT_FOUND = "element not found";
|
|
153
|
+
var UNKNOWN = "unknown (degraded read)";
|
|
154
|
+
function reactShown(matches, screen) {
|
|
155
|
+
const [only] = matches;
|
|
156
|
+
return only !== undefined && only.frame.width > 0 && only.frame.height > 0 && isOnScreen(only.tapPoint, screen);
|
|
157
|
+
}
|
|
158
|
+
function reactText(matches) {
|
|
159
|
+
const [only] = matches;
|
|
160
|
+
return only?.text === undefined ? undefined : normalizeText(only.text);
|
|
161
|
+
}
|
|
151
162
|
function missing() {
|
|
152
163
|
return { kind: "missing" };
|
|
153
164
|
}
|
|
@@ -205,7 +216,11 @@ class LocatorAssertions {
|
|
|
205
216
|
call: "toBeVisible()",
|
|
206
217
|
expected: "visible",
|
|
207
218
|
strict: true,
|
|
208
|
-
judge: (entry) => entry === undefined ? judged(false, "not found") : judged(entry.visible, entry.visible ? "visible" : "hidden")
|
|
219
|
+
judge: (entry) => entry === undefined ? judged(false, "not found") : judged(entry.visible, entry.visible ? "visible" : "hidden"),
|
|
220
|
+
judgeReact: (matches, screen) => {
|
|
221
|
+
const shown = reactShown(matches, screen);
|
|
222
|
+
return judged(shown, shown ? "visible (React tree)" : "hidden (React tree)");
|
|
223
|
+
}
|
|
209
224
|
}, options);
|
|
210
225
|
}
|
|
211
226
|
async toBeHidden(options = {}) {
|
|
@@ -214,7 +229,11 @@ class LocatorAssertions {
|
|
|
214
229
|
call: "toBeHidden()",
|
|
215
230
|
expected: "hidden",
|
|
216
231
|
strict: true,
|
|
217
|
-
judge: (entry) => entry === undefined ? judged(true, "not found") : judged(!entry.visible, entry.visible ? "visible" : "hidden")
|
|
232
|
+
judge: (entry) => entry === undefined ? judged(true, "not found") : judged(!entry.visible, entry.visible ? "visible" : "hidden"),
|
|
233
|
+
judgeReact: (matches, screen) => {
|
|
234
|
+
const shown = reactShown(matches, screen);
|
|
235
|
+
return judged(!shown, shown ? "visible (React tree)" : "hidden (React tree)");
|
|
236
|
+
}
|
|
218
237
|
}, options);
|
|
219
238
|
}
|
|
220
239
|
async toHaveText(expected, options = {}) {
|
|
@@ -229,6 +248,12 @@ class LocatorAssertions {
|
|
|
229
248
|
return missing();
|
|
230
249
|
const value = textOf(entry);
|
|
231
250
|
return judged(matchesText(value, expected, { exact }), JSON.stringify(value));
|
|
251
|
+
},
|
|
252
|
+
judgeReact: (matches) => {
|
|
253
|
+
const value = reactText(matches);
|
|
254
|
+
if (value === undefined)
|
|
255
|
+
return;
|
|
256
|
+
return judged(matchesText(value, expected, { exact }), `${JSON.stringify(value)} (React tree)`);
|
|
232
257
|
}
|
|
233
258
|
}, options);
|
|
234
259
|
}
|
|
@@ -244,6 +269,12 @@ class LocatorAssertions {
|
|
|
244
269
|
return missing();
|
|
245
270
|
const value = textOf(entry);
|
|
246
271
|
return judged(containsText(value, expected, ignoreCase), JSON.stringify(value));
|
|
272
|
+
},
|
|
273
|
+
judgeReact: (matches) => {
|
|
274
|
+
const value = reactText(matches);
|
|
275
|
+
if (value === undefined)
|
|
276
|
+
return;
|
|
277
|
+
return judged(containsText(value, expected, ignoreCase), `${JSON.stringify(value)} (React tree)`);
|
|
247
278
|
}
|
|
248
279
|
}, options);
|
|
249
280
|
}
|
|
@@ -379,7 +410,8 @@ class LocatorAssertions {
|
|
|
379
410
|
call: `toHaveCount(${expected})`,
|
|
380
411
|
expected: String(expected),
|
|
381
412
|
strict: false,
|
|
382
|
-
judge: (_entry, count) => judged(count === expected, String(count))
|
|
413
|
+
judge: (_entry, count) => judged(count === expected, String(count)),
|
|
414
|
+
judgeReact: (matches) => judged(matches.length === expected, `${matches.length} (React tree)`)
|
|
383
415
|
}, options);
|
|
384
416
|
}
|
|
385
417
|
async assert(spec, options) {
|
|
@@ -407,11 +439,12 @@ class LocatorAssertions {
|
|
|
407
439
|
return { done: false, state: { received: "(screen unreadable)" } };
|
|
408
440
|
}
|
|
409
441
|
const { observed } = observation;
|
|
410
|
-
const
|
|
411
|
-
if (
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
442
|
+
const look = await this.judgeLook(spec, observed);
|
|
443
|
+
if (look.kind === "unsettled") {
|
|
444
|
+
hold.broken();
|
|
445
|
+
return { done: false, state: look.last };
|
|
446
|
+
}
|
|
447
|
+
const { judgement, seen } = look;
|
|
415
448
|
const verdict = await hold.next(judgement.kind === "judged" && judgement.matches !== negate, observed.driver);
|
|
416
449
|
if (verdict === "pass")
|
|
417
450
|
return { done: true, value: true };
|
|
@@ -426,8 +459,35 @@ class LocatorAssertions {
|
|
|
426
459
|
}, timeoutMs);
|
|
427
460
|
if (result.status === "done")
|
|
428
461
|
return;
|
|
462
|
+
const degraded = result.last?.degraded;
|
|
463
|
+
if (degraded !== undefined)
|
|
464
|
+
throw this.locator.degradedFailure(degraded, timeoutMs, result.last?.snapshot);
|
|
429
465
|
throw this.failure(spec, result.last);
|
|
430
466
|
}
|
|
467
|
+
async judgeLook(spec, observed) {
|
|
468
|
+
const rt = this.locator.runtime;
|
|
469
|
+
const resolved = await this.locator.resolveIn(observed);
|
|
470
|
+
if (resolved.kind === "unknown") {
|
|
471
|
+
return { kind: "unsettled", last: { received: UNKNOWN, snapshot: observed.snapshot, degraded: resolved.read } };
|
|
472
|
+
}
|
|
473
|
+
if (resolved.kind === "react") {
|
|
474
|
+
const { matches } = resolved;
|
|
475
|
+
if (spec.strict && matches.length > 1 && spec.judgeReact !== undefined) {
|
|
476
|
+
const message = `${this.locator} matched ${matches.length} components in the React tree`;
|
|
477
|
+
throw new E2eError2("LOCATOR_AMBIGUOUS", message, rt.excerpt(observed.snapshot), matches.length);
|
|
478
|
+
}
|
|
479
|
+
const judgement = spec.judgeReact?.(matches, observed.snapshot.screen);
|
|
480
|
+
if (judgement === undefined) {
|
|
481
|
+
return { kind: "unsettled", last: { received: "only in the React tree", snapshot: observed.snapshot } };
|
|
482
|
+
}
|
|
483
|
+
return { kind: "judged", judgement, seen: observed.snapshot };
|
|
484
|
+
}
|
|
485
|
+
const { entries } = resolved;
|
|
486
|
+
if (spec.strict && entries.length > 1)
|
|
487
|
+
pickOne(rt, this.locator.expr, entries, observed);
|
|
488
|
+
const { seen, context } = await judgementContext(spec, observed);
|
|
489
|
+
return { kind: "judged", judgement: spec.judge(entries[0], entries.length, context), seen };
|
|
490
|
+
}
|
|
431
491
|
failure(spec, last) {
|
|
432
492
|
const { negate } = this.context;
|
|
433
493
|
const lines = [
|
|
@@ -710,6 +770,36 @@ function missingRequirements(target, requires) {
|
|
|
710
770
|
import { readdirSync } from "fs";
|
|
711
771
|
import { join as join2 } from "path";
|
|
712
772
|
|
|
773
|
+
// ../../packages/e2e/src/runner/bridge-evidence.ts
|
|
774
|
+
function bridgeEvidence() {
|
|
775
|
+
let fallbacks = 0;
|
|
776
|
+
let first;
|
|
777
|
+
let last;
|
|
778
|
+
return {
|
|
779
|
+
observe(event) {
|
|
780
|
+
if (event.type !== "span" || event.detail === undefined)
|
|
781
|
+
return;
|
|
782
|
+
const detail = event.detail;
|
|
783
|
+
const stall = bridgeStallFromSpan(detail);
|
|
784
|
+
const reason = typeof detail.fallback === "string" ? detail.fallback : undefined;
|
|
785
|
+
const fellBack = reason !== undefined && detail.backend !== "axbridge";
|
|
786
|
+
if (stall === undefined && !fellBack)
|
|
787
|
+
return;
|
|
788
|
+
const read = {
|
|
789
|
+
...fellBack ? { reason } : {},
|
|
790
|
+
...event.ok || event.code === undefined ? {} : { code: event.code },
|
|
791
|
+
...stall
|
|
792
|
+
};
|
|
793
|
+
fallbacks += 1;
|
|
794
|
+
first ??= read;
|
|
795
|
+
last = read;
|
|
796
|
+
},
|
|
797
|
+
summary() {
|
|
798
|
+
return first === undefined || last === undefined ? undefined : { fallbacks, first, last };
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
713
803
|
// ../../packages/e2e/src/runner/failure.ts
|
|
714
804
|
import { mkdirSync, writeFileSync } from "fs";
|
|
715
805
|
import { join } from "path";
|
|
@@ -782,7 +872,9 @@ async function captureSnapshot(driver, dir, ms, capture) {
|
|
|
782
872
|
}
|
|
783
873
|
const rendered = renderSnapshot(result.data, { interactiveOnly: false, refs: new Map });
|
|
784
874
|
const header = renderHeader({ snapshot: result.data, rendered, interactiveOnly: false });
|
|
785
|
-
|
|
875
|
+
const fellBack = fallbackLine(result.data);
|
|
876
|
+
const lines = fellBack === undefined ? [header, ...rendered.lines] : [header, fellBack, ...rendered.lines];
|
|
877
|
+
writeFileSync(join(dir, SCREEN_AT_FAILURE), `${lines.join(`
|
|
786
878
|
`)}
|
|
787
879
|
`);
|
|
788
880
|
capture.files.push(SCREEN_AT_FAILURE);
|
|
@@ -790,6 +882,12 @@ async function captureSnapshot(driver, dir, ms, capture) {
|
|
|
790
882
|
capture.problems.push(`snapshot: ${message(cause)}`);
|
|
791
883
|
}
|
|
792
884
|
}
|
|
885
|
+
function fallbackLine(snapshot) {
|
|
886
|
+
if (snapshot.fellBackFrom === undefined)
|
|
887
|
+
return;
|
|
888
|
+
const head = `# read on ${snapshot.backend}: ${snapshot.fellBackFrom}`;
|
|
889
|
+
return snapshot.bridgeStall === undefined ? `${head} did not answer` : `${head} stalled (${formatBridgeStall(snapshot.bridgeStall)})`;
|
|
890
|
+
}
|
|
793
891
|
async function captureDevtoolsArtifacts(facet, dir, timeoutMs = CAPTURE_TIMEOUT_MS, budget = unlimited) {
|
|
794
892
|
const capture = { files: [], problems: [] };
|
|
795
893
|
const ms = Math.min(timeoutMs, budget());
|
|
@@ -1059,8 +1157,10 @@ async function runAttemptOnce(session, spec) {
|
|
|
1059
1157
|
};
|
|
1060
1158
|
const files = new Set;
|
|
1061
1159
|
let binding;
|
|
1160
|
+
const bridge = bridgeEvidence();
|
|
1062
1161
|
const trace = openTrace(join2(dir, "trace.jsonl"), (event) => {
|
|
1063
1162
|
files.add("trace.jsonl");
|
|
1163
|
+
bridge.observe(event);
|
|
1064
1164
|
print(formatConsoleLine(event, { clock, short, depth: binding?.context.depth ?? 0 }));
|
|
1065
1165
|
});
|
|
1066
1166
|
const controller = new AbortController;
|
|
@@ -1093,7 +1193,7 @@ async function runAttemptOnce(session, spec) {
|
|
|
1093
1193
|
session.leaveAttempt(ctx);
|
|
1094
1194
|
trace.flush();
|
|
1095
1195
|
outcome = failIf(outcome, cleanupErrors.length > 0, () => cleanupErrors[0]);
|
|
1096
|
-
return concludeAttempt(session, spec, ctx, { startedAt, device }, outcome, print, clock);
|
|
1196
|
+
return concludeAttempt(session, spec, ctx, { startedAt, device, bridge: bridge.summary() }, outcome, print, clock);
|
|
1097
1197
|
}
|
|
1098
1198
|
async function raceDeadline(controller, timeoutMs, run) {
|
|
1099
1199
|
let deadlineHit = false;
|
|
@@ -1154,6 +1254,7 @@ function buildResult(spec, ctx, run, outcome) {
|
|
|
1154
1254
|
...spec.only ? { only: true } : {},
|
|
1155
1255
|
...outcome.failed ? { error: errorInfo(outcome.error) } : {},
|
|
1156
1256
|
...ctx.files.size > 0 ? { artifacts: { dir: ctx.dir, files: [...ctx.files].sort() } } : {},
|
|
1257
|
+
...run.bridge === undefined ? {} : { bridge: run.bridge },
|
|
1157
1258
|
device: run.device
|
|
1158
1259
|
};
|
|
1159
1260
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delacour/warden-e2e",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "TypeScript e2e testing for React Native and Expo on iOS simulators and Android emulators, run by `warden e2e`: test, expect, screen, device, app",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"LICENSE"
|
|
42
42
|
],
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@delacour/warden": "0.
|
|
44
|
+
"@delacour/warden": "0.14.0"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"zod": "^4.4.3"
|
package/preload.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
settleDetailOf,
|
|
12
12
|
DEFAULT_SETTLE_QUIET_MS,
|
|
13
13
|
DEFAULT_SETTLE_TIMEOUT_MS,
|
|
14
|
+
hashFallbackQuietMs,
|
|
14
15
|
settleByHash,
|
|
15
16
|
snapshotHash,
|
|
16
17
|
ok,
|
|
@@ -35,7 +36,7 @@ import {
|
|
|
35
36
|
selectorExpression,
|
|
36
37
|
renderSnapshot,
|
|
37
38
|
renderHeader,
|
|
38
|
-
|
|
39
|
+
lookUpTestIdViaReact,
|
|
39
40
|
isLocator,
|
|
40
41
|
Locator,
|
|
41
42
|
scrollUntilVisible,
|
|
@@ -44,6 +45,10 @@ import {
|
|
|
44
45
|
execError,
|
|
45
46
|
driverError,
|
|
46
47
|
driverErr,
|
|
48
|
+
bridgeStallSchema,
|
|
49
|
+
toBridgeStall,
|
|
50
|
+
noteBridgeStall,
|
|
51
|
+
formatBridgeStall,
|
|
47
52
|
logEventSchema,
|
|
48
53
|
fatalEventSchema,
|
|
49
54
|
responseSchema,
|
|
@@ -109,7 +114,7 @@ import {
|
|
|
109
114
|
onlyWasSeen,
|
|
110
115
|
WORKSPACE_VERSION,
|
|
111
116
|
WARDEN_E2E_VERSION2
|
|
112
|
-
} from "./chunk-
|
|
117
|
+
} from "./chunk-zk3wxa70.mjs";
|
|
113
118
|
|
|
114
119
|
// ../../packages/e2e/src/preload.ts
|
|
115
120
|
import { afterAll, setDefaultTimeout } from "bun:test";
|
|
@@ -7819,6 +7824,8 @@ function toSnapshot(data, takenAt) {
|
|
|
7819
7824
|
snapshot.modal = data.modal;
|
|
7820
7825
|
if (data.fallback?.from === "axbridge")
|
|
7821
7826
|
snapshot.fellBackFrom = "axbridge";
|
|
7827
|
+
if (data.fallback?.stall)
|
|
7828
|
+
snapshot.bridgeStall = toBridgeStall(data.fallback.stall);
|
|
7822
7829
|
return snapshot;
|
|
7823
7830
|
}
|
|
7824
7831
|
|
|
@@ -8407,6 +8414,14 @@ var REBIND_TIMEOUT_MS = 1e4;
|
|
|
8407
8414
|
var COLD_BRIDGE_BUDGET_MS = 60000;
|
|
8408
8415
|
var COLD_BRIDGE_FIRST_WAIT_MS = 2000;
|
|
8409
8416
|
var COLD_BRIDGE_WAIT_MS = 5000;
|
|
8417
|
+
var BRIDGE_COLD_DEADLINE_MS = 12000;
|
|
8418
|
+
var AX_FALLBACK_MARGIN_MS = 8000;
|
|
8419
|
+
var COLD_READ_TIMEOUT_MS = BRIDGE_COLD_DEADLINE_MS + AX_FALLBACK_MARGIN_MS;
|
|
8420
|
+
function coldReadTimeoutMs(env = process.env) {
|
|
8421
|
+
const raw = Number(env.WARDEN_SIMD_BRIDGE_COLD_DEADLINE_MS);
|
|
8422
|
+
const deadline = Number.isFinite(raw) && raw > BRIDGE_COLD_DEADLINE_MS ? raw : BRIDGE_COLD_DEADLINE_MS;
|
|
8423
|
+
return deadline + AX_FALLBACK_MARGIN_MS;
|
|
8424
|
+
}
|
|
8410
8425
|
var GESTURE_MARGIN_MS = 5000;
|
|
8411
8426
|
var FRONTMOST_TIMEOUT_MS = 3000;
|
|
8412
8427
|
var LAUNCH_TIMEOUT_MS = 30000;
|
|
@@ -8480,6 +8495,23 @@ function noteSnapshotError(detail) {
|
|
|
8480
8495
|
if (typeof value === "string")
|
|
8481
8496
|
noteSpanDetail(to, value);
|
|
8482
8497
|
}
|
|
8498
|
+
const stall = bridgeStallSchema.safeParse(detail.stall);
|
|
8499
|
+
if (stall.success)
|
|
8500
|
+
noteBridgeStall(toBridgeStall(stall.data));
|
|
8501
|
+
}
|
|
8502
|
+
function noteFallback(fallback, log) {
|
|
8503
|
+
const stall = fallback.stall && toBridgeStall(fallback.stall);
|
|
8504
|
+
const why = stall ? ` (${formatBridgeStall(stall)})` : "";
|
|
8505
|
+
log?.(`[ios] snapshot fell back ${fallback.from} -> ${fallback.to}: ${fallback.reason}${why}`);
|
|
8506
|
+
noteSpanDetail("fallback", fallback.reason);
|
|
8507
|
+
if (stall)
|
|
8508
|
+
noteBridgeStall(stall);
|
|
8509
|
+
}
|
|
8510
|
+
function noteSnapshotTarget(target) {
|
|
8511
|
+
if (!target)
|
|
8512
|
+
return;
|
|
8513
|
+
noteSpanDetail("targetPid", target.pid);
|
|
8514
|
+
noteSpanDetail("targetSource", target.source);
|
|
8483
8515
|
}
|
|
8484
8516
|
function noteHelperHealth(health) {
|
|
8485
8517
|
noteSpanDetail("bridge", health.ax.bridge);
|
|
@@ -8495,6 +8527,10 @@ function noteHelperHealth(health) {
|
|
|
8495
8527
|
noteSpanDetail("coreSimulator", health.coreSimulatorVersion);
|
|
8496
8528
|
if (health.frontmost)
|
|
8497
8529
|
noteSpanDetail("frontmostUnresolvedAgoMs", health.frontmost.unresolvedAgoMs);
|
|
8530
|
+
if (health.bridgeRecycles) {
|
|
8531
|
+
noteSpanDetail("bridgeRecycles", health.bridgeRecycles.count);
|
|
8532
|
+
noteSpanDetail("bridgeRecyclesExhausted", health.bridgeRecycles.exhausted);
|
|
8533
|
+
}
|
|
8498
8534
|
}
|
|
8499
8535
|
function coldBridgeWait(error, attempt, spentMs, budgetMs) {
|
|
8500
8536
|
if (!bridgeNotReady(error))
|
|
@@ -8595,6 +8631,9 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
8595
8631
|
let settleAxFailures = 0;
|
|
8596
8632
|
let rebindUnsupported = false;
|
|
8597
8633
|
let bridgeProven = false;
|
|
8634
|
+
let warmGeneration;
|
|
8635
|
+
const coldTimeoutMs = coldReadTimeoutMs();
|
|
8636
|
+
const readOpts = () => warmGeneration === swap.generation() ? {} : { minTimeoutMs: coldTimeoutMs };
|
|
8598
8637
|
let gestureUnsupported = false;
|
|
8599
8638
|
let appOpsOutdated = false;
|
|
8600
8639
|
const capabilities = {
|
|
@@ -8690,7 +8729,8 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
8690
8729
|
}
|
|
8691
8730
|
let failureDiagnosed = false;
|
|
8692
8731
|
const fetchSnapshot = async (o) => {
|
|
8693
|
-
const
|
|
8732
|
+
const generation = swap.generation();
|
|
8733
|
+
const result = await request("snapshot", params({ interactiveOnly: o?.interactiveOnly, backend: o?.backend }), readOpts());
|
|
8694
8734
|
if (!result.success) {
|
|
8695
8735
|
noteSnapshotError(result.error.detail);
|
|
8696
8736
|
if (!failureDiagnosed) {
|
|
@@ -8703,15 +8743,12 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
8703
8743
|
}
|
|
8704
8744
|
failureDiagnosed = false;
|
|
8705
8745
|
const { fallback, target } = result.data;
|
|
8746
|
+
if (result.data.backend === "axbridge" && !fallback)
|
|
8747
|
+
warmGeneration = generation;
|
|
8706
8748
|
noteSpanDetail("backend", result.data.backend);
|
|
8707
|
-
if (fallback)
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
}
|
|
8711
|
-
if (target) {
|
|
8712
|
-
noteSpanDetail("targetPid", target.pid);
|
|
8713
|
-
noteSpanDetail("targetSource", target.source);
|
|
8714
|
-
}
|
|
8749
|
+
if (fallback)
|
|
8750
|
+
noteFallback(fallback, opts.log);
|
|
8751
|
+
noteSnapshotTarget(target);
|
|
8715
8752
|
return { success: true, data: toSnapshot(result.data, now()) };
|
|
8716
8753
|
};
|
|
8717
8754
|
async function snapshotOnce(o) {
|
|
@@ -8774,7 +8811,7 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
8774
8811
|
if (!snap.success)
|
|
8775
8812
|
throw new DriverFailure(snap.error);
|
|
8776
8813
|
return snapshotHash(snap.data);
|
|
8777
|
-
}, { quietMs: o.quietMs, timeoutMs: o.timeoutMs, now, sleep });
|
|
8814
|
+
}, { quietMs: hashFallbackQuietMs(o.quietMs), timeoutMs: o.timeoutMs, now, sleep });
|
|
8778
8815
|
noteSpanDetail("via", "hash");
|
|
8779
8816
|
return { success: true, data: { ...outcome, via: "hash" } };
|
|
8780
8817
|
} catch (error) {
|
|
@@ -8785,6 +8822,7 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
8785
8822
|
}
|
|
8786
8823
|
async function settleAfterLaunch(bundleId) {
|
|
8787
8824
|
lastLaunched = bundleId;
|
|
8825
|
+
warmGeneration = undefined;
|
|
8788
8826
|
launchedBundle = bundleId;
|
|
8789
8827
|
if (await rebind(bundleId))
|
|
8790
8828
|
launchedBundle = undefined;
|
|
@@ -8882,7 +8920,7 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
8882
8920
|
return info;
|
|
8883
8921
|
}
|
|
8884
8922
|
async function readAlert(backend) {
|
|
8885
|
-
const result = await request("alertButtons", params({ backend }));
|
|
8923
|
+
const result = await request("alertButtons", params({ backend }), readOpts());
|
|
8886
8924
|
if (!result.success)
|
|
8887
8925
|
return result;
|
|
8888
8926
|
return { success: true, data: result.data === null ? null : toAlertInfo(result.data) };
|
|
@@ -8938,7 +8976,7 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
8938
8976
|
health,
|
|
8939
8977
|
snapshot,
|
|
8940
8978
|
async elementAtPoint(point, o) {
|
|
8941
|
-
const result = await request("elementAtPoint", params({ x: point.x, y: point.y, reachability: o?.reachability }));
|
|
8979
|
+
const result = await request("elementAtPoint", params({ x: point.x, y: point.y, reachability: o?.reachability }), readOpts());
|
|
8942
8980
|
if (!result.success)
|
|
8943
8981
|
return result;
|
|
8944
8982
|
return {
|
|
@@ -9466,7 +9504,12 @@ function createIosDeviceDriver(client, devicectl, opts) {
|
|
|
9466
9504
|
if (!snap.success)
|
|
9467
9505
|
throw new DriverFailure2(snap.error);
|
|
9468
9506
|
return snapshotHash(snap.data);
|
|
9469
|
-
}, {
|
|
9507
|
+
}, {
|
|
9508
|
+
quietMs: hashFallbackQuietMs(o.quietMs ?? DEFAULT_QUIET_MS2),
|
|
9509
|
+
timeoutMs: o.timeoutMs ?? DEFAULT_SETTLE_TIMEOUT_MS3,
|
|
9510
|
+
now,
|
|
9511
|
+
sleep
|
|
9512
|
+
});
|
|
9470
9513
|
noteSpanDetail("via", "hash");
|
|
9471
9514
|
return { success: true, data: { ...outcome, via: "hash" } };
|
|
9472
9515
|
} catch (error) {
|
|
@@ -10455,7 +10498,7 @@ function createDevice({
|
|
|
10455
10498
|
if (native.error.code !== "E_UNSUPPORTED")
|
|
10456
10499
|
throw toE2eError(native.error, "settle");
|
|
10457
10500
|
const polled = await settleByHash(async () => snapshotHash(await readSnapshot()), {
|
|
10458
|
-
quietMs,
|
|
10501
|
+
quietMs: hashFallbackQuietMs(quietMs),
|
|
10459
10502
|
timeoutMs,
|
|
10460
10503
|
now: clock.now,
|
|
10461
10504
|
sleep: clock.sleep
|
|
@@ -10534,13 +10577,11 @@ var EXCERPT_LINES = 40;
|
|
|
10534
10577
|
var MAX_ALERT_ANSWERS = 3;
|
|
10535
10578
|
var REACT_THROTTLE_MS = 450;
|
|
10536
10579
|
var REACT_TIMEOUT_MS = 2000;
|
|
10537
|
-
var FIRST_READER_RETRY_MS = 5000;
|
|
10538
10580
|
|
|
10539
10581
|
class Runtime {
|
|
10540
10582
|
host;
|
|
10541
10583
|
reactCache;
|
|
10542
10584
|
indexed = new WeakMap;
|
|
10543
|
-
firstReaderRetryAt = 0;
|
|
10544
10585
|
constructor(host) {
|
|
10545
10586
|
this.host = host;
|
|
10546
10587
|
}
|
|
@@ -10580,7 +10621,7 @@ class Runtime {
|
|
|
10580
10621
|
const result = canObserve(driver) ? await driver.observe({ reuse }) : await driver.snapshot();
|
|
10581
10622
|
if (!result.success)
|
|
10582
10623
|
return { ok: false, error: result.error };
|
|
10583
|
-
const snapshot =
|
|
10624
|
+
const snapshot = result.data;
|
|
10584
10625
|
const observation = {
|
|
10585
10626
|
ok: true,
|
|
10586
10627
|
observed: { driver, snapshot, index: this.indexOf(snapshot) }
|
|
@@ -10594,16 +10635,6 @@ class Runtime {
|
|
|
10594
10635
|
reuse = false;
|
|
10595
10636
|
}
|
|
10596
10637
|
}
|
|
10597
|
-
async restoreDegraded(driver, snapshot) {
|
|
10598
|
-
const first = snapshot.fellBackFrom;
|
|
10599
|
-
if (first === undefined || this.clock.now() < this.firstReaderRetryAt)
|
|
10600
|
-
return snapshot;
|
|
10601
|
-
const again = await driver.snapshot({ backend: first });
|
|
10602
|
-
if (again.success && again.data.backend === first)
|
|
10603
|
-
return again.data;
|
|
10604
|
-
this.firstReaderRetryAt = this.clock.now() + FIRST_READER_RETRY_MS;
|
|
10605
|
-
return snapshot;
|
|
10606
|
-
}
|
|
10607
10638
|
earlierThan(driver, before, look) {
|
|
10608
10639
|
const after = canObserve(driver) ? driver.previous?.() : undefined;
|
|
10609
10640
|
const same = after !== undefined && after.generation === before.generation && after.snapshot === look;
|
|
@@ -10640,18 +10671,25 @@ class Runtime {
|
|
|
10640
10671
|
throw driverFailure("snapshot", result.last ?? { code: "E_TIMEOUT", message: "no snapshot", retryable: true });
|
|
10641
10672
|
}
|
|
10642
10673
|
async reactMatches(testId, observed) {
|
|
10674
|
+
const lookup = await this.reactLookup(testId, observed);
|
|
10675
|
+
return lookup.answered ? lookup.matches : [];
|
|
10676
|
+
}
|
|
10677
|
+
hasReactTree() {
|
|
10678
|
+
return this.host.devtools?.() !== undefined;
|
|
10679
|
+
}
|
|
10680
|
+
async reactLookup(testId, observed) {
|
|
10643
10681
|
const facet = this.host.devtools?.();
|
|
10644
10682
|
if (facet === undefined)
|
|
10645
|
-
return
|
|
10683
|
+
return { answered: false };
|
|
10646
10684
|
const key = typeof testId === "string" ? `s:${testId}` : `r:${testId.toString()}`;
|
|
10647
10685
|
const cached = this.reactCache;
|
|
10648
10686
|
if (cached !== undefined && cached.key === key && this.clock.now() - cached.at < REACT_THROTTLE_MS) {
|
|
10649
|
-
return cached.
|
|
10687
|
+
return cached.lookup;
|
|
10650
10688
|
}
|
|
10651
10689
|
const view = { platform: observed.snapshot.platform, screen: observed.snapshot.screen };
|
|
10652
|
-
const
|
|
10653
|
-
this.reactCache = { key, at: this.clock.now(),
|
|
10654
|
-
return
|
|
10690
|
+
const lookup = await lookUpTestIdViaReact(facet, testId, view, { timeoutMs: REACT_TIMEOUT_MS });
|
|
10691
|
+
this.reactCache = { key, at: this.clock.now(), lookup };
|
|
10692
|
+
return lookup;
|
|
10655
10693
|
}
|
|
10656
10694
|
excerpt(snapshot) {
|
|
10657
10695
|
const rendered = renderSnapshot(snapshot, { interactiveOnly: true, refs: new Map, maxLines: EXCERPT_LINES });
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type SpanDetail } from "./span-detail";
|
|
3
|
+
/**
|
|
4
|
+
* What one iOS `axbridge` fallback or stall looked like, as `warden-simd` reports it (`fallback.stall` on a snapshot,
|
|
5
|
+
* `detail.stall` on a failed read). Recorded in `trace.jsonl` (the `stall*` span fields), `results.jsonl` (`bridge`)
|
|
6
|
+
* and under the header of `screen-at-failure.txt`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const bridgeStallSchema: z.ZodObject<{
|
|
9
|
+
stage: z.ZodString;
|
|
10
|
+
readMs: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
deadlineMs: z.ZodOptional<z.ZodNumber>;
|
|
12
|
+
guestAgeMs: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
cold: z.ZodBoolean;
|
|
14
|
+
cooldownRemainingMs: z.ZodOptional<z.ZodNumber>;
|
|
15
|
+
probes: z.ZodNumber;
|
|
16
|
+
recycles: z.ZodOptional<z.ZodNumber>;
|
|
17
|
+
recyclesExhausted: z.ZodOptional<z.ZodBoolean>;
|
|
18
|
+
}, z.core.$loose>;
|
|
19
|
+
export type BridgeStall = {
|
|
20
|
+
stage: string;
|
|
21
|
+
readMs?: number;
|
|
22
|
+
deadlineMs?: number;
|
|
23
|
+
guestAgeMs?: number;
|
|
24
|
+
cold: boolean;
|
|
25
|
+
cooldownRemainingMs?: number;
|
|
26
|
+
probes: number;
|
|
27
|
+
recycles?: number;
|
|
28
|
+
recyclesExhausted?: boolean;
|
|
29
|
+
};
|
|
30
|
+
/** Only the fields of the schema, so a newer helper's extra keys stay off the span and out of the results. */
|
|
31
|
+
export declare function toBridgeStall(wire: z.infer<typeof bridgeStallSchema>): BridgeStall;
|
|
32
|
+
/** Leaves the stall on the span timing the read (`stallStage`, `stallReadMs`, …). */
|
|
33
|
+
export declare function noteBridgeStall(stall: BridgeStall): void;
|
|
34
|
+
/** The stall a span recorded, or undefined when it has none. */
|
|
35
|
+
export declare function bridgeStallFromSpan(detail: SpanDetail | undefined): BridgeStall | undefined;
|
|
36
|
+
/** One line for a log or a report: `stage read · read 4012 ms of a 4000 ms deadline · warm · …`. */
|
|
37
|
+
export declare function formatBridgeStall(stall: BridgeStall): string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CameraCondition, ClockCondition, HostFault, LocationCondition, NetworkCondition, PermissionName, PermissionState, TextSize } from "../../../core/src/conditions/conditions.types";
|
|
2
2
|
import type { AsyncResult } from "../../../types/src/result";
|
|
3
3
|
import type { ScreenReaderState } from "../a11y/a11y.types";
|
|
4
|
+
import type { BridgeStall } from "./bridge-stall";
|
|
4
5
|
/**
|
|
5
6
|
* The device driver contract every layer of warden-e2e builds on: the spec API (`screen`, `device`,
|
|
6
7
|
* `app`), `warden ui`, specs and the devtools facet all talk to a `Driver`. Implementations:
|
|
@@ -136,9 +137,12 @@ export type Snapshot = {
|
|
|
136
137
|
modal?: ModalInfo;
|
|
137
138
|
/**
|
|
138
139
|
* Set when `backend` answered because this reader failed or was skipped first (iOS: `axbridge` stalled, `ax` read in
|
|
139
|
-
* its place). The fallback reader can lack what the first one has, so a missing node on such a snapshot is not final
|
|
140
|
+
* its place). The fallback reader can lack what the first one has, so a missing node on such a snapshot is not final:
|
|
141
|
+
* the iOS helper probes the first reader in the background, and a later read is back on it once it answers.
|
|
140
142
|
*/
|
|
141
143
|
fellBackFrom?: SnapshotBackend;
|
|
144
|
+
/** iOS: what the bridge failure behind `fellBackFrom` looked like, when the helper reported it */
|
|
145
|
+
bridgeStall?: BridgeStall;
|
|
142
146
|
};
|
|
143
147
|
export type Orientation = "portrait" | "portraitUpsideDown" | "landscapeLeft" | "landscapeRight";
|
|
144
148
|
export type HardwareButton = "home" | "back" | "lock" | "volumeUp" | "volumeDown" | "shake";
|
|
@@ -91,6 +91,16 @@ export type OpenIosDriverOptions = {
|
|
|
91
91
|
* Once a read or a rebind has succeeded the normal per-call budgets apply.
|
|
92
92
|
*/
|
|
93
93
|
export declare const COLD_BRIDGE_BUDGET_MS = 60000;
|
|
94
|
+
/**
|
|
95
|
+
* warden-simd's deadline for the first `axbridge` read of a cold guest (helper start, a recycled guest, an app just
|
|
96
|
+
* launched): `BridgeDeadline.defaultColdReadMs`, or `WARDEN_SIMD_BRIDGE_COLD_DEADLINE_MS`. On a 3-vCPU runner that
|
|
97
|
+
* first read takes 4-11 s; warm reads keep the 4 s deadline.
|
|
98
|
+
*/
|
|
99
|
+
export declare const BRIDGE_COLD_DEADLINE_MS = 12000;
|
|
100
|
+
/** The request timeout of a read while the bridge is cold, so the client never gives up before the helper's deadline. */
|
|
101
|
+
export declare const COLD_READ_TIMEOUT_MS: number;
|
|
102
|
+
/** {@link COLD_READ_TIMEOUT_MS}, following a `WARDEN_SIMD_BRIDGE_COLD_DEADLINE_MS` the helper also reads. */
|
|
103
|
+
export declare function coldReadTimeoutMs(env?: Readonly<Record<string, string | undefined>>): number;
|
|
94
104
|
/** USB HID keyboard-page usage ids for the keys `Driver.key` offers. */
|
|
95
105
|
export declare const KEY_USAGE: Readonly<Record<KeyName, number>>;
|
|
96
106
|
/** The fields of a failed snapshot's `detail` the helper sets (which reader, why, both readers' errors), on its span. */
|
|
@@ -21,6 +21,8 @@ export type SimdEventMap = {
|
|
|
21
21
|
};
|
|
22
22
|
export type RequestOptions = {
|
|
23
23
|
timeoutMs?: number;
|
|
24
|
+
/** a floor for this request's timeout (a cold bridge read): raises the default or `timeoutMs`, never lowers it */
|
|
25
|
+
minTimeoutMs?: number;
|
|
24
26
|
};
|
|
25
27
|
export type SimdClient = {
|
|
26
28
|
/** the handshake of the current helper process (refreshed after a respawn) */
|
|
@@ -192,6 +192,10 @@ export declare const healthDataSchema: z.ZodObject<{
|
|
|
192
192
|
unresolvedAgoMs: z.ZodNumber;
|
|
193
193
|
method: z.ZodString;
|
|
194
194
|
}, z.core.$strip>>;
|
|
195
|
+
bridgeRecycles: z.ZodOptional<z.ZodObject<{
|
|
196
|
+
count: z.ZodNumber;
|
|
197
|
+
exhausted: z.ZodBoolean;
|
|
198
|
+
}, z.core.$strip>>;
|
|
195
199
|
state: z.ZodOptional<z.ZodString>;
|
|
196
200
|
uptimeMs: z.ZodOptional<z.ZodNumber>;
|
|
197
201
|
}, z.core.$loose>;
|
|
@@ -214,6 +218,17 @@ export declare const snapshotDataSchema: z.ZodObject<{
|
|
|
214
218
|
from: z.ZodString;
|
|
215
219
|
to: z.ZodString;
|
|
216
220
|
reason: z.ZodString;
|
|
221
|
+
stall: z.ZodOptional<z.ZodObject<{
|
|
222
|
+
stage: z.ZodString;
|
|
223
|
+
readMs: z.ZodOptional<z.ZodNumber>;
|
|
224
|
+
deadlineMs: z.ZodOptional<z.ZodNumber>;
|
|
225
|
+
guestAgeMs: z.ZodOptional<z.ZodNumber>;
|
|
226
|
+
cold: z.ZodBoolean;
|
|
227
|
+
cooldownRemainingMs: z.ZodOptional<z.ZodNumber>;
|
|
228
|
+
probes: z.ZodNumber;
|
|
229
|
+
recycles: z.ZodOptional<z.ZodNumber>;
|
|
230
|
+
recyclesExhausted: z.ZodOptional<z.ZodBoolean>;
|
|
231
|
+
}, z.core.$loose>>;
|
|
217
232
|
}, z.core.$strip>>;
|
|
218
233
|
target: z.ZodOptional<z.ZodObject<{
|
|
219
234
|
pid: z.ZodNumber;
|
|
@@ -362,6 +377,10 @@ export declare const opSchemas: {
|
|
|
362
377
|
unresolvedAgoMs: z.ZodNumber;
|
|
363
378
|
method: z.ZodString;
|
|
364
379
|
}, z.core.$strip>>;
|
|
380
|
+
bridgeRecycles: z.ZodOptional<z.ZodObject<{
|
|
381
|
+
count: z.ZodNumber;
|
|
382
|
+
exhausted: z.ZodBoolean;
|
|
383
|
+
}, z.core.$strip>>;
|
|
365
384
|
state: z.ZodOptional<z.ZodString>;
|
|
366
385
|
uptimeMs: z.ZodOptional<z.ZodNumber>;
|
|
367
386
|
}, z.core.$loose>;
|
|
@@ -383,6 +402,17 @@ export declare const opSchemas: {
|
|
|
383
402
|
from: z.ZodString;
|
|
384
403
|
to: z.ZodString;
|
|
385
404
|
reason: z.ZodString;
|
|
405
|
+
stall: z.ZodOptional<z.ZodObject<{
|
|
406
|
+
stage: z.ZodString;
|
|
407
|
+
readMs: z.ZodOptional<z.ZodNumber>;
|
|
408
|
+
deadlineMs: z.ZodOptional<z.ZodNumber>;
|
|
409
|
+
guestAgeMs: z.ZodOptional<z.ZodNumber>;
|
|
410
|
+
cold: z.ZodBoolean;
|
|
411
|
+
cooldownRemainingMs: z.ZodOptional<z.ZodNumber>;
|
|
412
|
+
probes: z.ZodNumber;
|
|
413
|
+
recycles: z.ZodOptional<z.ZodNumber>;
|
|
414
|
+
recyclesExhausted: z.ZodOptional<z.ZodBoolean>;
|
|
415
|
+
}, z.core.$loose>>;
|
|
386
416
|
}, z.core.$strip>>;
|
|
387
417
|
target: z.ZodOptional<z.ZodObject<{
|
|
388
418
|
pid: z.ZodNumber;
|