@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.
@@ -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
- let deadline = Math.min(start + request.budgetMs, cap);
838
- let held = "(gone)";
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
- const { reading } = read;
845
- if (reading.kind === "unreadable") {
846
- streak = 0;
847
- held = reading.note;
848
- } else if (reading.value === request.text) {
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 call = bindingCalledSchema.safeParse(params);
2265
- if (call.success) {
2266
- let payload;
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 text = textOf2(event.data);
2284
- if (text === undefined)
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
- const first = (object.description ?? "function").split(`
2470
- `)[0] ?? "function";
2471
- return first.length > MAX_FUNCTION_TEXT ? `${first.slice(0, MAX_FUNCTION_TEXT)}\u2026` : first;
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
- const n = Number(arg.value ?? arg.description);
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(devtoolsErr("cdp_closed", "devtools session is closed"));
3146
+ return Promise.resolve(closedError());
3086
3147
  const previous = state.attached;
3087
3148
  log(`devtools: ${reason}, reconnecting`);
3088
- const done = (async () => {
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 devtoolsErr("cdp_closed", "devtools session is closed");
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
- attach(result.data);
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
- if (method === "http") {
3201
- log("devtools: reload via http");
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 parseEnvelope(stdout) {
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 = parseEnvelope(stdout);
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.2.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-7ns30pbp.mjs";
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 ${use.timeoutMs}ms`));
749
- }, use.timeoutMs);
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([setUpAndRun(bound, spec, use, controller.signal, state, teardowns), deadline]);
776
+ await Promise.race([run(), deadline]);
777
+ return { failed: false, failure: undefined, deadlineHit };
762
778
  } catch (error) {
763
- failed = true;
764
- failure = error;
779
+ return { failed: true, failure: error, deadlineHit };
765
780
  } finally {
766
781
  clearTimeout(timer);
767
782
  }
768
- if (!failed && ctx.softFailures.length > 0) {
769
- failed = true;
770
- failure = softFailureError(ctx.softFailures);
771
- }
772
- if (failed)
773
- await captureFailureArtifacts(session, ctx, budget, startedAt);
774
- const cleanupErrors = await runCleanup(session, spec, ctx, state.bag, teardowns, budget, (context) => {
775
- binding = { context, finished: false };
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.2.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.2.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-7ns30pbp.mjs";
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
- noteSpanDetail("rebind", reply.data.bound ? "bound" : reply.data.reason ?? "unbound");
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 expected = launchedBundle;
3493
- const native = await request("settle", { quietMs, settleTimeoutMs: timeoutMs, ...expected === undefined ? {} : { bundleId: expected } }, { timeoutMs: timeoutMs + SETTLE_MARGIN_MS });
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
  }