@delacour/warden-e2e 0.2.0 → 0.3.1
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-7ns30pbp.mjs → chunk-8em9kgsk.mjs} +171 -143
- package/index.mjs +37 -37
- package/package.json +2 -2
- package/preload.mjs +45 -33
|
@@ -834,34 +834,34 @@ async function confirmFieldValue(request) {
|
|
|
834
834
|
const { now, sleep } = request;
|
|
835
835
|
const start = now();
|
|
836
836
|
const cap = request.maxMs === undefined ? Number.POSITIVE_INFINITY : start + request.maxMs;
|
|
837
|
-
|
|
838
|
-
let
|
|
839
|
-
let streak = 0;
|
|
837
|
+
const reads = request.reads ?? CONFIRM_READS;
|
|
838
|
+
let poll = { deadline: Math.min(start + request.budgetMs, cap), held: "(gone)", streak: 0 };
|
|
840
839
|
for (;; ) {
|
|
841
840
|
const read = await request.read();
|
|
842
841
|
if (!read.ok)
|
|
843
842
|
return { status: "failed", error: read.error };
|
|
844
|
-
|
|
845
|
-
if (
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
streak += 1;
|
|
850
|
-
if (streak >= (request.reads ?? CONFIRM_READS))
|
|
851
|
-
return { status: "confirmed" };
|
|
852
|
-
held = reading.value;
|
|
853
|
-
deadline = Math.max(deadline, Math.min(now() + 2 * FIELD_POLL_MS, cap));
|
|
854
|
-
} else {
|
|
855
|
-
streak = 0;
|
|
856
|
-
if (reading.value !== held)
|
|
857
|
-
deadline = Math.max(deadline, Math.min(now() + FIELD_PROGRESS_MS, cap));
|
|
858
|
-
held = reading.value;
|
|
859
|
-
}
|
|
860
|
-
if (now() >= deadline)
|
|
861
|
-
return { status: "mismatch", held };
|
|
843
|
+
poll = foldReading(poll, read.reading, { text: request.text, now: now(), cap, reads });
|
|
844
|
+
if (poll.streak > 0 && poll.streak >= reads)
|
|
845
|
+
return { status: "confirmed" };
|
|
846
|
+
if (now() >= poll.deadline)
|
|
847
|
+
return { status: "mismatch", held: poll.held };
|
|
862
848
|
await sleep(FIELD_POLL_MS);
|
|
863
849
|
}
|
|
864
850
|
}
|
|
851
|
+
function foldReading(poll, reading, at) {
|
|
852
|
+
if (reading.kind === "unreadable")
|
|
853
|
+
return { ...poll, streak: 0, held: reading.note };
|
|
854
|
+
if (reading.value === at.text) {
|
|
855
|
+
const streak = poll.streak + 1;
|
|
856
|
+
if (streak >= at.reads)
|
|
857
|
+
return { ...poll, streak };
|
|
858
|
+
const deadline = Math.max(poll.deadline, Math.min(at.now + 2 * FIELD_POLL_MS, at.cap));
|
|
859
|
+
return { deadline, held: reading.value, streak };
|
|
860
|
+
}
|
|
861
|
+
const progressed = reading.value !== poll.held;
|
|
862
|
+
const deadline = progressed ? Math.max(poll.deadline, Math.min(at.now + FIELD_PROGRESS_MS, at.cap)) : poll.deadline;
|
|
863
|
+
return { deadline, held: reading.value, streak: 0 };
|
|
864
|
+
}
|
|
865
865
|
async function hitTestField(driver, wanted) {
|
|
866
866
|
const { x, y, width, height } = wanted.element.frame;
|
|
867
867
|
const hit = await driver.elementAtPoint({ x: x + width / 2, y: y + height / 2 });
|
|
@@ -2201,6 +2201,32 @@ function textOf2(data) {
|
|
|
2201
2201
|
return new TextDecoder().decode(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
2202
2202
|
return;
|
|
2203
2203
|
}
|
|
2204
|
+
function taggedBindingPayload(params) {
|
|
2205
|
+
const call = bindingCalledSchema.safeParse(params);
|
|
2206
|
+
if (!call.success)
|
|
2207
|
+
return;
|
|
2208
|
+
let payload;
|
|
2209
|
+
try {
|
|
2210
|
+
payload = JSON.parse(call.data.payload);
|
|
2211
|
+
} catch {
|
|
2212
|
+
payload = undefined;
|
|
2213
|
+
}
|
|
2214
|
+
const tagged = bindingPayloadSchema.safeParse(payload);
|
|
2215
|
+
return tagged.success ? { requestId: tagged.data.requestId, payload } : undefined;
|
|
2216
|
+
}
|
|
2217
|
+
function parseEnvelope(data) {
|
|
2218
|
+
const text = textOf2(data);
|
|
2219
|
+
if (text === undefined)
|
|
2220
|
+
return;
|
|
2221
|
+
let json;
|
|
2222
|
+
try {
|
|
2223
|
+
json = JSON.parse(text);
|
|
2224
|
+
} catch {
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
const message = envelopeSchema.safeParse(json);
|
|
2228
|
+
return message.success ? message.data : undefined;
|
|
2229
|
+
}
|
|
2204
2230
|
function inspectorOrigin(wsUrl) {
|
|
2205
2231
|
if (!URL.canParse(wsUrl))
|
|
2206
2232
|
return;
|
|
@@ -2261,18 +2287,9 @@ async function openCdp(wsUrl, options = {}) {
|
|
|
2261
2287
|
};
|
|
2262
2288
|
const dispatch = (method, params) => {
|
|
2263
2289
|
if (method === "Runtime.bindingCalled") {
|
|
2264
|
-
const
|
|
2265
|
-
if (
|
|
2266
|
-
|
|
2267
|
-
try {
|
|
2268
|
-
payload = JSON.parse(call.data.payload);
|
|
2269
|
-
} catch {
|
|
2270
|
-
payload = undefined;
|
|
2271
|
-
}
|
|
2272
|
-
const tagged = bindingPayloadSchema.safeParse(payload);
|
|
2273
|
-
if (tagged.success)
|
|
2274
|
-
bindings.get(tagged.data.requestId)?.settle(ok(payload));
|
|
2275
|
-
}
|
|
2290
|
+
const tagged = taggedBindingPayload(params);
|
|
2291
|
+
if (tagged)
|
|
2292
|
+
bindings.get(tagged.requestId)?.settle(ok(tagged.payload));
|
|
2276
2293
|
}
|
|
2277
2294
|
for (const handler of [...anyHandlers])
|
|
2278
2295
|
handler(method, params);
|
|
@@ -2280,19 +2297,10 @@ async function openCdp(wsUrl, options = {}) {
|
|
|
2280
2297
|
handler(params);
|
|
2281
2298
|
};
|
|
2282
2299
|
ws.addEventListener("message", (event) => {
|
|
2283
|
-
const
|
|
2284
|
-
if (
|
|
2285
|
-
return;
|
|
2286
|
-
let json;
|
|
2287
|
-
try {
|
|
2288
|
-
json = JSON.parse(text);
|
|
2289
|
-
} catch {
|
|
2300
|
+
const message = parseEnvelope(event.data);
|
|
2301
|
+
if (!message)
|
|
2290
2302
|
return;
|
|
2291
|
-
}
|
|
2292
|
-
const message = envelopeSchema.safeParse(json);
|
|
2293
|
-
if (!message.success)
|
|
2294
|
-
return;
|
|
2295
|
-
const { id, error, result, method, params } = message.data;
|
|
2303
|
+
const { id, error, result, method, params } = message;
|
|
2296
2304
|
if (id !== undefined) {
|
|
2297
2305
|
const request = pending.get(id);
|
|
2298
2306
|
if (!request)
|
|
@@ -2455,6 +2463,20 @@ function formatPreview(object) {
|
|
|
2455
2463
|
const name = object.className && object.className !== "Object" ? `${object.className} ` : "";
|
|
2456
2464
|
return `${name}{${body}${more}}`;
|
|
2457
2465
|
}
|
|
2466
|
+
function formatObject(object) {
|
|
2467
|
+
if (object.subtype === "null")
|
|
2468
|
+
return "null";
|
|
2469
|
+
if (object.value !== undefined)
|
|
2470
|
+
return JSON.stringify(object.value);
|
|
2471
|
+
if (object.subtype === "error")
|
|
2472
|
+
return object.description ?? "Error";
|
|
2473
|
+
return formatPreview(object) ?? object.description ?? object.className ?? "Object";
|
|
2474
|
+
}
|
|
2475
|
+
function formatFunction(object) {
|
|
2476
|
+
const first = (object.description ?? "function").split(`
|
|
2477
|
+
`)[0] ?? "function";
|
|
2478
|
+
return first.length > MAX_FUNCTION_TEXT ? `${first.slice(0, MAX_FUNCTION_TEXT)}\u2026` : first;
|
|
2479
|
+
}
|
|
2458
2480
|
function formatValue2(object) {
|
|
2459
2481
|
switch (object.type) {
|
|
2460
2482
|
case "string":
|
|
@@ -2465,24 +2487,18 @@ function formatValue2(object) {
|
|
|
2465
2487
|
case "boolean":
|
|
2466
2488
|
case "bigint":
|
|
2467
2489
|
return object.unserializableValue ?? object.description ?? String(object.value);
|
|
2468
|
-
case "function":
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
return
|
|
2472
|
-
}
|
|
2473
|
-
case "object": {
|
|
2474
|
-
if (object.subtype === "null")
|
|
2475
|
-
return "null";
|
|
2476
|
-
if (object.value !== undefined)
|
|
2477
|
-
return JSON.stringify(object.value);
|
|
2478
|
-
if (object.subtype === "error")
|
|
2479
|
-
return object.description ?? "Error";
|
|
2480
|
-
return formatPreview(object) ?? object.description ?? object.className ?? "Object";
|
|
2481
|
-
}
|
|
2490
|
+
case "function":
|
|
2491
|
+
return formatFunction(object);
|
|
2492
|
+
case "object":
|
|
2493
|
+
return formatObject(object);
|
|
2482
2494
|
default:
|
|
2483
2495
|
return object.description ?? `[${object.type}]`;
|
|
2484
2496
|
}
|
|
2485
2497
|
}
|
|
2498
|
+
function formatNumeric(spec, arg) {
|
|
2499
|
+
const n = Number(arg.value ?? arg.description);
|
|
2500
|
+
return Number.isNaN(n) ? "NaN" : String(spec === "f" ? n : Math.trunc(n));
|
|
2501
|
+
}
|
|
2486
2502
|
function applyFormat(format, rest) {
|
|
2487
2503
|
let used = 0;
|
|
2488
2504
|
const text = format.replace(/%([sdifoOjc%])/g, (whole, spec) => {
|
|
@@ -2494,10 +2510,8 @@ function applyFormat(format, rest) {
|
|
|
2494
2510
|
used++;
|
|
2495
2511
|
if (spec === "c")
|
|
2496
2512
|
return "";
|
|
2497
|
-
if (spec === "d" || spec === "i" || spec === "f")
|
|
2498
|
-
|
|
2499
|
-
return Number.isNaN(n) ? "NaN" : String(spec === "f" ? n : Math.trunc(n));
|
|
2500
|
-
}
|
|
2513
|
+
if (spec === "d" || spec === "i" || spec === "f")
|
|
2514
|
+
return formatNumeric(spec, arg);
|
|
2501
2515
|
return formatValue2(arg);
|
|
2502
2516
|
});
|
|
2503
2517
|
return { text, used };
|
|
@@ -3078,51 +3092,61 @@ async function connectDevtools(o) {
|
|
|
3078
3092
|
reconnect("JS context cleared");
|
|
3079
3093
|
});
|
|
3080
3094
|
}
|
|
3095
|
+
const closedError = () => devtoolsErr("cdp_closed", "devtools session is closed");
|
|
3096
|
+
function adopt(attached) {
|
|
3097
|
+
attach(attached);
|
|
3098
|
+
log(`devtools: reconnected to ${attached.target.id}`);
|
|
3099
|
+
for (const handler of [...eventHandlers.reconnected])
|
|
3100
|
+
handler(attached.target);
|
|
3101
|
+
}
|
|
3102
|
+
function adoptIfOpen(attached) {
|
|
3103
|
+
if (phase() === "closed") {
|
|
3104
|
+
attached.cdp.close();
|
|
3105
|
+
return closedError();
|
|
3106
|
+
}
|
|
3107
|
+
adopt(attached);
|
|
3108
|
+
return ok(attached);
|
|
3109
|
+
}
|
|
3110
|
+
function markLost(error) {
|
|
3111
|
+
if (phase() === "closed")
|
|
3112
|
+
return;
|
|
3113
|
+
state = { kind: "lost", error };
|
|
3114
|
+
for (const handler of [...eventHandlers.lost])
|
|
3115
|
+
handler(error);
|
|
3116
|
+
}
|
|
3117
|
+
async function reconnectLoop(previous) {
|
|
3118
|
+
previous.cdp.close();
|
|
3119
|
+
let last = { code: "devtools_unavailable", message: "reconnect failed" };
|
|
3120
|
+
const startedAt = now();
|
|
3121
|
+
let slept = 0;
|
|
3122
|
+
const leftMs = () => budgetMs - Math.max(now() - startedAt, slept);
|
|
3123
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
3124
|
+
if (phase() === "closed")
|
|
3125
|
+
return closedError();
|
|
3126
|
+
const result = await establish();
|
|
3127
|
+
if (result.success)
|
|
3128
|
+
return adoptIfOpen(result.data);
|
|
3129
|
+
last = result.error;
|
|
3130
|
+
const left = leftMs();
|
|
3131
|
+
const cap = Number.isFinite(maxAttempts) ? `/${maxAttempts}` : "";
|
|
3132
|
+
log(`devtools: reconnect attempt ${attempt}${cap} failed: ${last.message}`);
|
|
3133
|
+
if (attempt >= maxAttempts || left <= 0)
|
|
3134
|
+
break;
|
|
3135
|
+
const pause = Math.min(backoffAfter(attempt), left);
|
|
3136
|
+
slept += pause;
|
|
3137
|
+
await sleep(pause);
|
|
3138
|
+
}
|
|
3139
|
+
markLost(last);
|
|
3140
|
+
return { success: false, error: last };
|
|
3141
|
+
}
|
|
3081
3142
|
function reconnect(reason) {
|
|
3082
3143
|
if (state.kind === "reconnecting")
|
|
3083
3144
|
return state.done;
|
|
3084
3145
|
if (state.kind !== "up")
|
|
3085
|
-
return Promise.resolve(
|
|
3146
|
+
return Promise.resolve(closedError());
|
|
3086
3147
|
const previous = state.attached;
|
|
3087
3148
|
log(`devtools: ${reason}, reconnecting`);
|
|
3088
|
-
const done = (
|
|
3089
|
-
previous.cdp.close();
|
|
3090
|
-
let last = { code: "devtools_unavailable", message: "reconnect failed" };
|
|
3091
|
-
const startedAt = now();
|
|
3092
|
-
let slept = 0;
|
|
3093
|
-
const leftMs = () => budgetMs - Math.max(now() - startedAt, slept);
|
|
3094
|
-
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
3095
|
-
if (phase() === "closed")
|
|
3096
|
-
return devtoolsErr("cdp_closed", "devtools session is closed");
|
|
3097
|
-
const result = await establish();
|
|
3098
|
-
if (result.success) {
|
|
3099
|
-
if (phase() === "closed") {
|
|
3100
|
-
result.data.cdp.close();
|
|
3101
|
-
return devtoolsErr("cdp_closed", "devtools session is closed");
|
|
3102
|
-
}
|
|
3103
|
-
attach(result.data);
|
|
3104
|
-
log(`devtools: reconnected to ${result.data.target.id}`);
|
|
3105
|
-
for (const handler of [...eventHandlers.reconnected])
|
|
3106
|
-
handler(result.data.target);
|
|
3107
|
-
return result;
|
|
3108
|
-
}
|
|
3109
|
-
last = result.error;
|
|
3110
|
-
const left = leftMs();
|
|
3111
|
-
const cap = Number.isFinite(maxAttempts) ? `/${maxAttempts}` : "";
|
|
3112
|
-
log(`devtools: reconnect attempt ${attempt}${cap} failed: ${last.message}`);
|
|
3113
|
-
if (attempt >= maxAttempts || left <= 0)
|
|
3114
|
-
break;
|
|
3115
|
-
const pause = Math.min(backoffAfter(attempt), left);
|
|
3116
|
-
slept += pause;
|
|
3117
|
-
await sleep(pause);
|
|
3118
|
-
}
|
|
3119
|
-
if (phase() !== "closed") {
|
|
3120
|
-
state = { kind: "lost", error: last };
|
|
3121
|
-
for (const handler of [...eventHandlers.lost])
|
|
3122
|
-
handler(last);
|
|
3123
|
-
}
|
|
3124
|
-
return { success: false, error: last };
|
|
3125
|
-
})();
|
|
3149
|
+
const done = reconnectLoop(previous);
|
|
3126
3150
|
state = { kind: "reconnecting", done };
|
|
3127
3151
|
epoch++;
|
|
3128
3152
|
for (const wake of [...epochWaiters])
|
|
@@ -3136,16 +3160,13 @@ async function connectDevtools(o) {
|
|
|
3136
3160
|
if (phase() === "closed") {
|
|
3137
3161
|
if (result.success)
|
|
3138
3162
|
result.data.cdp.close();
|
|
3139
|
-
return
|
|
3163
|
+
return closedError();
|
|
3140
3164
|
}
|
|
3141
3165
|
if (!result.success) {
|
|
3142
3166
|
state = { kind: "lost", error: result.error };
|
|
3143
3167
|
return result;
|
|
3144
3168
|
}
|
|
3145
|
-
|
|
3146
|
-
log(`devtools: reconnected to ${result.data.target.id}`);
|
|
3147
|
-
for (const handler of [...eventHandlers.reconnected])
|
|
3148
|
-
handler(result.data.target);
|
|
3169
|
+
adopt(result.data);
|
|
3149
3170
|
return result;
|
|
3150
3171
|
})();
|
|
3151
3172
|
state = { kind: "reconnecting", done };
|
|
@@ -3160,6 +3181,44 @@ async function connectDevtools(o) {
|
|
|
3160
3181
|
return revive();
|
|
3161
3182
|
return devtoolsErr("cdp_closed", "devtools session is closed");
|
|
3162
3183
|
}
|
|
3184
|
+
async function requestReload(method, baseUrl) {
|
|
3185
|
+
if (method !== "http") {
|
|
3186
|
+
log(`devtools: reload via ${method}`);
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
log("devtools: reload via http");
|
|
3190
|
+
try {
|
|
3191
|
+
const res = await o.fetch(`${baseUrl.replace(/\/+$/, "")}/reload`, { method: "POST" });
|
|
3192
|
+
if (!res.ok) {
|
|
3193
|
+
return devtoolsErr("devtools_unavailable", `reload failed: POST /reload answered ${res.status}`);
|
|
3194
|
+
}
|
|
3195
|
+
} catch (error) {
|
|
3196
|
+
return devtoolsErr("devtools_unavailable", `reload failed: POST /reload threw ${String(error)}`);
|
|
3197
|
+
}
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
3200
|
+
async function awaitReload(method, startedAt, waitMs) {
|
|
3201
|
+
if (epoch === startedAt)
|
|
3202
|
+
await waitForEpochChange(waitMs);
|
|
3203
|
+
if (epoch === startedAt && state.kind === "up") {
|
|
3204
|
+
log(`devtools: reload via ${method} not observed after ${waitMs} ms`);
|
|
3205
|
+
return devtoolsErr("reload_failed", `reload via ${method} acknowledged but the JS context did not restart within ${waitMs} ms`);
|
|
3206
|
+
}
|
|
3207
|
+
if (state.kind !== "reconnecting")
|
|
3208
|
+
return;
|
|
3209
|
+
const back = await state.done;
|
|
3210
|
+
return back.success ? undefined : back;
|
|
3211
|
+
}
|
|
3212
|
+
function waitForEpochChange(waitMs) {
|
|
3213
|
+
return new Promise((resolve) => {
|
|
3214
|
+
const wake = () => {
|
|
3215
|
+
epochWaiters.delete(wake);
|
|
3216
|
+
resolve();
|
|
3217
|
+
};
|
|
3218
|
+
epochWaiters.add(wake);
|
|
3219
|
+
sleep(waitMs).then(wake);
|
|
3220
|
+
});
|
|
3221
|
+
}
|
|
3163
3222
|
const first = await establish();
|
|
3164
3223
|
if (!first.success)
|
|
3165
3224
|
return first;
|
|
@@ -3197,39 +3256,8 @@ async function connectDevtools(o) {
|
|
|
3197
3256
|
return devtoolsErr(probe.error.code, `reload probe failed: ${probe.error.message}`);
|
|
3198
3257
|
}
|
|
3199
3258
|
const method = readReloadMethod(probe.data) ?? "http";
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
try {
|
|
3203
|
-
const res = await o.fetch(`${metroUrl.replace(/\/+$/, "")}/reload`, { method: "POST" });
|
|
3204
|
-
if (!res.ok) {
|
|
3205
|
-
return devtoolsErr("devtools_unavailable", `reload failed: POST /reload answered ${res.status}`);
|
|
3206
|
-
}
|
|
3207
|
-
} catch (error) {
|
|
3208
|
-
return devtoolsErr("devtools_unavailable", `reload failed: POST /reload threw ${String(error)}`);
|
|
3209
|
-
}
|
|
3210
|
-
} else {
|
|
3211
|
-
log(`devtools: reload via ${method}`);
|
|
3212
|
-
}
|
|
3213
|
-
if (epoch === startedAt) {
|
|
3214
|
-
await new Promise((resolve) => {
|
|
3215
|
-
const wake = () => {
|
|
3216
|
-
epochWaiters.delete(wake);
|
|
3217
|
-
resolve();
|
|
3218
|
-
};
|
|
3219
|
-
epochWaiters.add(wake);
|
|
3220
|
-
sleep(waitMs).then(wake);
|
|
3221
|
-
});
|
|
3222
|
-
}
|
|
3223
|
-
if (epoch === startedAt && state.kind === "up") {
|
|
3224
|
-
log(`devtools: reload via ${method} not observed after ${waitMs} ms`);
|
|
3225
|
-
return devtoolsErr("reload_failed", `reload via ${method} acknowledged but the JS context did not restart within ${waitMs} ms`);
|
|
3226
|
-
}
|
|
3227
|
-
if (state.kind === "reconnecting") {
|
|
3228
|
-
const back = await state.done;
|
|
3229
|
-
if (!back.success)
|
|
3230
|
-
return back;
|
|
3231
|
-
}
|
|
3232
|
-
return ok({ method });
|
|
3259
|
+
const failure = await requestReload(method, metroUrl) ?? await awaitReload(method, startedAt, waitMs);
|
|
3260
|
+
return failure ?? ok({ method });
|
|
3233
3261
|
},
|
|
3234
3262
|
get target() {
|
|
3235
3263
|
if (state.kind === "up")
|
|
@@ -5456,7 +5484,7 @@ function parseBudgets(raw) {
|
|
|
5456
5484
|
|
|
5457
5485
|
// ../../packages/e2e/src/perf/device-perf.ts
|
|
5458
5486
|
var PERF_DIR = "perf";
|
|
5459
|
-
function
|
|
5487
|
+
function parseEnvelope2(stdout) {
|
|
5460
5488
|
try {
|
|
5461
5489
|
const json = JSON.parse(stdout.trim().split(`
|
|
5462
5490
|
`).at(-1) ?? "");
|
|
@@ -5509,7 +5537,7 @@ function createDevicePerf(deps) {
|
|
|
5509
5537
|
if (deps.unavailable !== undefined)
|
|
5510
5538
|
throw new E2eError2("SESSION", `device.perf unavailable: ${deps.unavailable}`);
|
|
5511
5539
|
const { code, stdout, stderr } = await deps.exec(["perf", sub, "--json", "--udid", deps.udid, ...extra]);
|
|
5512
|
-
const env =
|
|
5540
|
+
const env = parseEnvelope2(stdout);
|
|
5513
5541
|
if (env === undefined || !env.ok) {
|
|
5514
5542
|
const why = env === undefined ? stderr.trim() || `exit ${code}` : `${env.error.code}: ${env.error.message}`;
|
|
5515
5543
|
throw new E2eError2("SESSION", `warden perf ${sub} failed: ${why}`);
|
|
@@ -5827,6 +5855,6 @@ function onlyWasSeen() {
|
|
|
5827
5855
|
|
|
5828
5856
|
// ../../packages/e2e/src/version.ts
|
|
5829
5857
|
var WORKSPACE_VERSION = "0.0.0-workspace";
|
|
5830
|
-
var WARDEN_E2E_VERSION2 = "0.
|
|
5858
|
+
var WARDEN_E2E_VERSION2 = "0.3.1";
|
|
5831
5859
|
|
|
5832
5860
|
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-
|
|
34
|
+
} from "./chunk-8em9kgsk.mjs";
|
|
35
35
|
// ../../packages/e2e/src/expect/expect.ts
|
|
36
36
|
import { expect as bunExpect } from "bun:test";
|
|
37
37
|
|
|
@@ -742,60 +742,60 @@ async function runAttemptOnce(session, spec) {
|
|
|
742
742
|
const bound = attemptSession(session, () => binding ?? { context: ctx, finished: false });
|
|
743
743
|
print(formatConsoleLine({ type: "test-start", test: { file: spec.file, title: [...spec.title], startedAt, device } }, { clock }));
|
|
744
744
|
session.enterAttempt(ctx);
|
|
745
|
+
const teardowns = [];
|
|
746
|
+
const state = {};
|
|
747
|
+
let outcome = await raceDeadline(controller, use.timeoutMs, () => setUpAndRun(bound, spec, use, controller.signal, state, teardowns));
|
|
748
|
+
outcome = failIf(outcome, ctx.softFailures.length > 0, () => softFailureError(ctx.softFailures));
|
|
749
|
+
if (outcome.failed)
|
|
750
|
+
await captureFailureArtifacts(session, ctx, budget, startedAt);
|
|
751
|
+
const cleanupErrors = await runCleanup(session, spec, ctx, state.bag, teardowns, budget, (context) => {
|
|
752
|
+
binding = { context, finished: false };
|
|
753
|
+
});
|
|
754
|
+
const finished = new E2eError2("SESSION", "attempt finished");
|
|
755
|
+
if (!controller.signal.aborted)
|
|
756
|
+
controller.abort(finished);
|
|
757
|
+
binding = { context: { ...ctx, signal: AbortSignal.abort(finished), depth: 0 }, finished: true };
|
|
758
|
+
session.leaveAttempt(ctx);
|
|
759
|
+
trace.flush();
|
|
760
|
+
outcome = failIf(outcome, cleanupErrors.length > 0, () => cleanupErrors[0]);
|
|
761
|
+
return concludeAttempt(session, spec, ctx, { startedAt, device }, outcome, print, clock);
|
|
762
|
+
}
|
|
763
|
+
async function raceDeadline(controller, timeoutMs, run) {
|
|
745
764
|
let deadlineHit = false;
|
|
746
765
|
const timer = setTimeout(() => {
|
|
747
766
|
deadlineHit = true;
|
|
748
|
-
controller.abort(new E2eError2("TIMEOUT", `test timed out after ${
|
|
749
|
-
},
|
|
767
|
+
controller.abort(new E2eError2("TIMEOUT", `test timed out after ${timeoutMs}ms`));
|
|
768
|
+
}, timeoutMs);
|
|
750
769
|
const deadline = new Promise((_, reject) => {
|
|
751
770
|
controller.signal.addEventListener("abort", () => reject(controller.signal.reason), { once: true });
|
|
752
771
|
});
|
|
753
772
|
deadline.catch(() => {
|
|
754
773
|
return;
|
|
755
774
|
});
|
|
756
|
-
const teardowns = [];
|
|
757
|
-
const state = {};
|
|
758
|
-
let failed = false;
|
|
759
|
-
let failure;
|
|
760
775
|
try {
|
|
761
|
-
await Promise.race([
|
|
776
|
+
await Promise.race([run(), deadline]);
|
|
777
|
+
return { failed: false, failure: undefined, deadlineHit };
|
|
762
778
|
} catch (error) {
|
|
763
|
-
failed
|
|
764
|
-
failure = error;
|
|
779
|
+
return { failed: true, failure: error, deadlineHit };
|
|
765
780
|
} finally {
|
|
766
781
|
clearTimeout(timer);
|
|
767
782
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
});
|
|
777
|
-
const finished = new E2eError2("SESSION", "attempt finished");
|
|
778
|
-
if (!controller.signal.aborted)
|
|
779
|
-
controller.abort(finished);
|
|
780
|
-
binding = { context: { ...ctx, signal: AbortSignal.abort(finished), depth: 0 }, finished: true };
|
|
781
|
-
session.leaveAttempt(ctx);
|
|
782
|
-
trace.flush();
|
|
783
|
-
if (!failed && cleanupErrors.length > 0) {
|
|
784
|
-
failed = true;
|
|
785
|
-
failure = cleanupErrors[0];
|
|
786
|
-
}
|
|
787
|
-
for (const shot of listScreenshots(dir))
|
|
788
|
-
files.add(shot);
|
|
789
|
-
const outcome = failed ? { failed: true, error: failure, timedOut: deadlineHit } : { failed: false };
|
|
790
|
-
const result = buildResult(spec, ctx, { startedAt, device }, outcome);
|
|
783
|
+
}
|
|
784
|
+
function failIf(outcome, present, failure) {
|
|
785
|
+
return outcome.failed || !present ? outcome : { ...outcome, failed: true, failure: failure() };
|
|
786
|
+
}
|
|
787
|
+
async function concludeAttempt(session, spec, ctx, run, outcome, print, clock) {
|
|
788
|
+
for (const shot of listScreenshots(ctx.dir))
|
|
789
|
+
ctx.files.add(shot);
|
|
790
|
+
const result = buildResult(spec, ctx, run, outcome.failed ? { failed: true, error: outcome.failure, timedOut: outcome.deadlineHit } : { failed: false });
|
|
791
791
|
print(formatConsoleLine({ type: "test-end", result }, { clock }));
|
|
792
|
-
if (failed && session.recoverHid !== undefined && isHidLost(failure) && await session.recoverHid()) {
|
|
792
|
+
if (outcome.failed && session.recoverHid !== undefined && isHidLost(outcome.failure) && await session.recoverHid()) {
|
|
793
793
|
return "again";
|
|
794
794
|
}
|
|
795
|
-
if (!failed || spec.final)
|
|
795
|
+
if (!outcome.failed || spec.final)
|
|
796
796
|
session.results.append(result);
|
|
797
|
-
if (failed)
|
|
798
|
-
throw failure instanceof Error ? failure : new Error(String(failure));
|
|
797
|
+
if (outcome.failed)
|
|
798
|
+
throw outcome.failure instanceof Error ? outcome.failure : new Error(String(outcome.failure));
|
|
799
799
|
return;
|
|
800
800
|
}
|
|
801
801
|
function softFailureError(failures) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delacour/warden-e2e",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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.3.1"
|
|
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-8em9kgsk.mjs";
|
|
72
72
|
|
|
73
73
|
// ../../packages/e2e/src/preload.ts
|
|
74
74
|
import { afterAll, setDefaultTimeout } from "bun:test";
|
|
@@ -3311,6 +3311,25 @@ function nativeDetail(data) {
|
|
|
3311
3311
|
noteSpanDetail("frontmostPid", frontmostPid);
|
|
3312
3312
|
return detail;
|
|
3313
3313
|
}
|
|
3314
|
+
function noteRebind(data) {
|
|
3315
|
+
noteSpanDetail("rebind", data.bound ? "bound" : data.reason ?? "unbound");
|
|
3316
|
+
noteSpanDetail("rebindMs", data.waitedMs);
|
|
3317
|
+
if (data.restarts !== undefined && data.restarts > 0)
|
|
3318
|
+
noteSpanDetail("rebindRestarts", data.restarts);
|
|
3319
|
+
if (data.recycled === true)
|
|
3320
|
+
noteSpanDetail("rebindRecycled", true);
|
|
3321
|
+
}
|
|
3322
|
+
function healthDetail(d) {
|
|
3323
|
+
return {
|
|
3324
|
+
axDefault: d.ax.default,
|
|
3325
|
+
...d.ax.bridge === "degraded" ? { axBridge: "degraded" } : {},
|
|
3326
|
+
...d.ax.lastError ? { axLastError: d.ax.lastError } : {},
|
|
3327
|
+
...d.ax.error !== undefined ? { axError: d.ax.error } : {},
|
|
3328
|
+
...d.socket ? { socket: d.socket } : {},
|
|
3329
|
+
...d.xcode ? { xcode: d.xcode } : {},
|
|
3330
|
+
...d.state ? { state: d.state } : {}
|
|
3331
|
+
};
|
|
3332
|
+
}
|
|
3314
3333
|
|
|
3315
3334
|
class DriverFailure extends Error {
|
|
3316
3335
|
error;
|
|
@@ -3477,36 +3496,37 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
3477
3496
|
opts.log?.(`[ios] rebind after launch failed (${reply.error.code}): ${reply.error.message}`);
|
|
3478
3497
|
return false;
|
|
3479
3498
|
}
|
|
3480
|
-
|
|
3481
|
-
noteSpanDetail("rebindMs", reply.data.waitedMs);
|
|
3482
|
-
if (reply.data.restarts !== undefined && reply.data.restarts > 0)
|
|
3483
|
-
noteSpanDetail("rebindRestarts", reply.data.restarts);
|
|
3484
|
-
if (reply.data.recycled === true)
|
|
3485
|
-
noteSpanDetail("rebindRecycled", true);
|
|
3499
|
+
noteRebind(reply.data);
|
|
3486
3500
|
return reply.data.bound;
|
|
3487
3501
|
}
|
|
3502
|
+
async function settleViaHelper(quietMs, timeoutMs) {
|
|
3503
|
+
const expected = launchedBundle;
|
|
3504
|
+
const native = await request("settle", { quietMs, settleTimeoutMs: timeoutMs, ...expected === undefined ? {} : { bundleId: expected } }, { timeoutMs: timeoutMs + SETTLE_MARGIN_MS });
|
|
3505
|
+
if (native.success) {
|
|
3506
|
+
if (expected !== undefined && native.data.reason !== "frontmost_mismatch")
|
|
3507
|
+
launchedBundle = undefined;
|
|
3508
|
+
return { success: true, data: nativeDetail(native.data) };
|
|
3509
|
+
}
|
|
3510
|
+
if (native.error.code === "E_TIMEOUT") {
|
|
3511
|
+
noteSpanDetail("reason", "helper_timeout");
|
|
3512
|
+
return {
|
|
3513
|
+
success: true,
|
|
3514
|
+
data: { quiet: false, waitedMs: timeoutMs + SETTLE_MARGIN_MS, reason: "helper_timeout" }
|
|
3515
|
+
};
|
|
3516
|
+
}
|
|
3517
|
+
if (native.error.code !== "E_UNSUPPORTED")
|
|
3518
|
+
return native;
|
|
3519
|
+
settleUnsupported = true;
|
|
3520
|
+
opts.log?.("[ios] native settle unsupported, polling snapshot hashes");
|
|
3521
|
+
return;
|
|
3522
|
+
}
|
|
3488
3523
|
async function settle(o = {}) {
|
|
3489
3524
|
const quietMs = o.quietMs ?? DEFAULT_QUIET_MS;
|
|
3490
3525
|
const timeoutMs = o.timeoutMs ?? DEFAULT_SETTLE_TIMEOUT_MS2;
|
|
3491
3526
|
if (!settleUnsupported) {
|
|
3492
|
-
const
|
|
3493
|
-
|
|
3494
|
-
if (native.success) {
|
|
3495
|
-
if (expected !== undefined && native.data.reason !== "frontmost_mismatch")
|
|
3496
|
-
launchedBundle = undefined;
|
|
3497
|
-
return { success: true, data: nativeDetail(native.data) };
|
|
3498
|
-
}
|
|
3499
|
-
if (native.error.code === "E_TIMEOUT") {
|
|
3500
|
-
noteSpanDetail("reason", "helper_timeout");
|
|
3501
|
-
return {
|
|
3502
|
-
success: true,
|
|
3503
|
-
data: { quiet: false, waitedMs: timeoutMs + SETTLE_MARGIN_MS, reason: "helper_timeout" }
|
|
3504
|
-
};
|
|
3505
|
-
}
|
|
3506
|
-
if (native.error.code !== "E_UNSUPPORTED")
|
|
3527
|
+
const native = await settleViaHelper(quietMs, timeoutMs);
|
|
3528
|
+
if (native)
|
|
3507
3529
|
return native;
|
|
3508
|
-
settleUnsupported = true;
|
|
3509
|
-
opts.log?.("[ios] native settle unsupported, polling snapshot hashes");
|
|
3510
3530
|
}
|
|
3511
3531
|
return hashSettle({ quietMs, timeoutMs });
|
|
3512
3532
|
}
|
|
@@ -3564,15 +3584,7 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
3564
3584
|
transport: d.hid.transport,
|
|
3565
3585
|
bridge: d.ax.bridge === "degraded" ? "available" : d.ax.bridge,
|
|
3566
3586
|
screen: d.screen,
|
|
3567
|
-
detail:
|
|
3568
|
-
axDefault: d.ax.default,
|
|
3569
|
-
...d.ax.bridge === "degraded" ? { axBridge: "degraded" } : {},
|
|
3570
|
-
...d.ax.lastError ? { axLastError: d.ax.lastError } : {},
|
|
3571
|
-
...d.ax.error !== undefined ? { axError: d.ax.error } : {},
|
|
3572
|
-
...d.socket ? { socket: d.socket } : {},
|
|
3573
|
-
...d.xcode ? { xcode: d.xcode } : {},
|
|
3574
|
-
...d.state ? { state: d.state } : {}
|
|
3575
|
-
}
|
|
3587
|
+
detail: healthDetail(d)
|
|
3576
3588
|
}
|
|
3577
3589
|
};
|
|
3578
3590
|
}
|