@delacour/warden-e2e 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{chunk-7ns30pbp.mjs → chunk-3874jt2j.mjs} +295 -172
- package/index.mjs +37 -37
- package/package.json +2 -2
- package/preload.mjs +103 -42
- package/types/core/src/exec.d.ts +2 -0
- package/types/core/src/providers/provider.types.d.ts +7 -0
- package/types/e2e/src/devtools/cdp.d.ts +7 -2
- package/types/e2e/src/devtools/devtools.types.d.ts +13 -0
- package/types/e2e/src/devtools/session.d.ts +8 -1
- package/types/e2e/src/driver/ios/ios-driver.d.ts +7 -6
- package/types/e2e/src/driver/ios/simd-protocol.d.ts +23 -0
- package/types/e2e/src/driver/ios/type-budget.d.ts +8 -4
- package/types/e2e/src/driver/observation-cache.d.ts +3 -1
- package/types/e2e/src/driver/typing.d.ts +10 -0
- package/types/e2e/src/preload.d.ts +5 -0
- package/types/e2e/src/screen/field.d.ts +11 -1
- package/types/e2e/src/screen/locator.d.ts +24 -5
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-3874jt2j.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.4.0",
|
|
4
4
|
"description": "TypeScript spec API for `warden e2e`: test, expect, screen, device, app",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"LICENSE"
|
|
38
38
|
],
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@delacour/warden": "0.
|
|
40
|
+
"@delacour/warden": "0.4.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"zod": "^4.4.3"
|
package/preload.mjs
CHANGED
|
@@ -68,7 +68,7 @@ import {
|
|
|
68
68
|
onlyWasSeen,
|
|
69
69
|
WORKSPACE_VERSION,
|
|
70
70
|
WARDEN_E2E_VERSION2
|
|
71
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-3874jt2j.mjs";
|
|
72
72
|
|
|
73
73
|
// ../../packages/e2e/src/preload.ts
|
|
74
74
|
import { afterAll, setDefaultTimeout } from "bun:test";
|
|
@@ -3238,8 +3238,8 @@ function createSimctl(deps) {
|
|
|
3238
3238
|
|
|
3239
3239
|
// ../../packages/e2e/src/driver/ios/type-budget.ts
|
|
3240
3240
|
var TYPE_BASE_MS = 1e4;
|
|
3241
|
-
var TYPE_PER_CHAR_MS =
|
|
3242
|
-
var TYPE_PER_REPEAT_MS =
|
|
3241
|
+
var TYPE_PER_CHAR_MS = 100;
|
|
3242
|
+
var TYPE_PER_REPEAT_MS = 200;
|
|
3243
3243
|
function typeTextBudgetMs(text) {
|
|
3244
3244
|
const characters = Array.from(text);
|
|
3245
3245
|
let repeats = 0;
|
|
@@ -3257,6 +3257,8 @@ var SETTLE_MARGIN_MS = 2000;
|
|
|
3257
3257
|
var REBIND_TIMEOUT_MS = 3000;
|
|
3258
3258
|
var GESTURE_MARGIN_MS = 5000;
|
|
3259
3259
|
var FRONTMOST_TIMEOUT_MS = 3000;
|
|
3260
|
+
var LAUNCH_TIMEOUT_MS = 30000;
|
|
3261
|
+
var APP_OP_TIMEOUT_MS = 15000;
|
|
3260
3262
|
var KEY_USAGE = {
|
|
3261
3263
|
Enter: 40,
|
|
3262
3264
|
Escape: 41,
|
|
@@ -3311,6 +3313,25 @@ function nativeDetail(data) {
|
|
|
3311
3313
|
noteSpanDetail("frontmostPid", frontmostPid);
|
|
3312
3314
|
return detail;
|
|
3313
3315
|
}
|
|
3316
|
+
function noteRebind(data) {
|
|
3317
|
+
noteSpanDetail("rebind", data.bound ? "bound" : data.reason ?? "unbound");
|
|
3318
|
+
noteSpanDetail("rebindMs", data.waitedMs);
|
|
3319
|
+
if (data.restarts !== undefined && data.restarts > 0)
|
|
3320
|
+
noteSpanDetail("rebindRestarts", data.restarts);
|
|
3321
|
+
if (data.recycled === true)
|
|
3322
|
+
noteSpanDetail("rebindRecycled", true);
|
|
3323
|
+
}
|
|
3324
|
+
function healthDetail(d) {
|
|
3325
|
+
return {
|
|
3326
|
+
axDefault: d.ax.default,
|
|
3327
|
+
...d.ax.bridge === "degraded" ? { axBridge: "degraded" } : {},
|
|
3328
|
+
...d.ax.lastError ? { axLastError: d.ax.lastError } : {},
|
|
3329
|
+
...d.ax.error !== undefined ? { axError: d.ax.error } : {},
|
|
3330
|
+
...d.socket ? { socket: d.socket } : {},
|
|
3331
|
+
...d.xcode ? { xcode: d.xcode } : {},
|
|
3332
|
+
...d.state ? { state: d.state } : {}
|
|
3333
|
+
};
|
|
3334
|
+
}
|
|
3314
3335
|
|
|
3315
3336
|
class DriverFailure extends Error {
|
|
3316
3337
|
error;
|
|
@@ -3329,6 +3350,9 @@ function restOf(error, text) {
|
|
|
3329
3350
|
function decodeBase64(png) {
|
|
3330
3351
|
return new Uint8Array(Buffer.from(png, "base64"));
|
|
3331
3352
|
}
|
|
3353
|
+
function predatesOp(error) {
|
|
3354
|
+
return error.code === "E_UNSUPPORTED" || error.code === "E_BAD_REQUEST" && /unknown (input )?op/.test(error.message);
|
|
3355
|
+
}
|
|
3332
3356
|
function createIosDriver(swap, simctl, opts) {
|
|
3333
3357
|
const { udid, now, sleep } = opts;
|
|
3334
3358
|
const { client } = swap;
|
|
@@ -3340,6 +3364,7 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
3340
3364
|
let settleUnsupported = !nativeSettle;
|
|
3341
3365
|
let rebindUnsupported = false;
|
|
3342
3366
|
let gestureUnsupported = false;
|
|
3367
|
+
let appOpsOutdated = false;
|
|
3343
3368
|
const capabilities = {
|
|
3344
3369
|
pinch: true,
|
|
3345
3370
|
maxPointers: 2,
|
|
@@ -3407,6 +3432,30 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
3407
3432
|
}
|
|
3408
3433
|
return result;
|
|
3409
3434
|
};
|
|
3435
|
+
async function appOp(op, p, timeoutMs) {
|
|
3436
|
+
if (appOpsOutdated) {
|
|
3437
|
+
noteSpanDetail("via", "simctl");
|
|
3438
|
+
return;
|
|
3439
|
+
}
|
|
3440
|
+
const result = await request(op, p, { timeoutMs });
|
|
3441
|
+
if (!result.success && predatesOp(result.error)) {
|
|
3442
|
+
appOpsOutdated = true;
|
|
3443
|
+
noteSpanDetail("via", "simctl");
|
|
3444
|
+
noteSpanDetail("reason", "helper_outdated");
|
|
3445
|
+
opts.log?.(`[ios] this warden-simd predates the \`${op}\` op (${result.error.code}: ${result.error.message}); app lifecycle goes through simctl until the helper is restarted (warden ui close --udid ${udid}) or rebuilt (bun run --cwd packages/e2e native:build)`);
|
|
3446
|
+
return;
|
|
3447
|
+
}
|
|
3448
|
+
noteSpanDetail("via", "simd");
|
|
3449
|
+
return result;
|
|
3450
|
+
}
|
|
3451
|
+
async function appOutcome(viaHelper, fallback, note) {
|
|
3452
|
+
if (viaHelper === undefined)
|
|
3453
|
+
return fallback();
|
|
3454
|
+
if (!viaHelper.success)
|
|
3455
|
+
return viaHelper;
|
|
3456
|
+
note(viaHelper.data);
|
|
3457
|
+
return okVoid;
|
|
3458
|
+
}
|
|
3410
3459
|
const fetchSnapshot = async (o) => {
|
|
3411
3460
|
const result = await request("snapshot", params({ interactiveOnly: o?.interactiveOnly, backend: o?.backend }));
|
|
3412
3461
|
if (!result.success)
|
|
@@ -3444,11 +3493,17 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
3444
3493
|
const rest = restOf(typed.error, text);
|
|
3445
3494
|
if (rest === undefined)
|
|
3446
3495
|
return typed;
|
|
3447
|
-
const copied = await simctl.pbcopy(rest);
|
|
3448
|
-
if (!copied.success)
|
|
3449
|
-
return copied;
|
|
3450
3496
|
opts.log?.(`[ios] typeText: pasting ${rest.length} unmappable character(s)`);
|
|
3451
|
-
return
|
|
3497
|
+
return pasteText(rest);
|
|
3498
|
+
}
|
|
3499
|
+
async function pasteText(text) {
|
|
3500
|
+
if (text === "")
|
|
3501
|
+
return ok(undefined);
|
|
3502
|
+
const copied = await simctl.pbcopy(text);
|
|
3503
|
+
const result = copied.success ? await sendVoid("paste") : copied;
|
|
3504
|
+
if (!result.success)
|
|
3505
|
+
opts.log?.(`[ios] pasteText failed (${result.error.code}): ${result.error.message}`);
|
|
3506
|
+
return result;
|
|
3452
3507
|
}
|
|
3453
3508
|
async function hashSettle(o) {
|
|
3454
3509
|
try {
|
|
@@ -3477,36 +3532,37 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
3477
3532
|
opts.log?.(`[ios] rebind after launch failed (${reply.error.code}): ${reply.error.message}`);
|
|
3478
3533
|
return false;
|
|
3479
3534
|
}
|
|
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);
|
|
3535
|
+
noteRebind(reply.data);
|
|
3486
3536
|
return reply.data.bound;
|
|
3487
3537
|
}
|
|
3538
|
+
async function settleViaHelper(quietMs, timeoutMs) {
|
|
3539
|
+
const expected = launchedBundle;
|
|
3540
|
+
const native = await request("settle", { quietMs, settleTimeoutMs: timeoutMs, ...expected === undefined ? {} : { bundleId: expected } }, { timeoutMs: timeoutMs + SETTLE_MARGIN_MS });
|
|
3541
|
+
if (native.success) {
|
|
3542
|
+
if (expected !== undefined && native.data.reason !== "frontmost_mismatch")
|
|
3543
|
+
launchedBundle = undefined;
|
|
3544
|
+
return { success: true, data: nativeDetail(native.data) };
|
|
3545
|
+
}
|
|
3546
|
+
if (native.error.code === "E_TIMEOUT") {
|
|
3547
|
+
noteSpanDetail("reason", "helper_timeout");
|
|
3548
|
+
return {
|
|
3549
|
+
success: true,
|
|
3550
|
+
data: { quiet: false, waitedMs: timeoutMs + SETTLE_MARGIN_MS, reason: "helper_timeout" }
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
3553
|
+
if (native.error.code !== "E_UNSUPPORTED")
|
|
3554
|
+
return native;
|
|
3555
|
+
settleUnsupported = true;
|
|
3556
|
+
opts.log?.("[ios] native settle unsupported, polling snapshot hashes");
|
|
3557
|
+
return;
|
|
3558
|
+
}
|
|
3488
3559
|
async function settle(o = {}) {
|
|
3489
3560
|
const quietMs = o.quietMs ?? DEFAULT_QUIET_MS;
|
|
3490
3561
|
const timeoutMs = o.timeoutMs ?? DEFAULT_SETTLE_TIMEOUT_MS2;
|
|
3491
3562
|
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")
|
|
3563
|
+
const native = await settleViaHelper(quietMs, timeoutMs);
|
|
3564
|
+
if (native)
|
|
3507
3565
|
return native;
|
|
3508
|
-
settleUnsupported = true;
|
|
3509
|
-
opts.log?.("[ios] native settle unsupported, polling snapshot hashes");
|
|
3510
3566
|
}
|
|
3511
3567
|
return hashSettle({ quietMs, timeoutMs });
|
|
3512
3568
|
}
|
|
@@ -3564,15 +3620,7 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
3564
3620
|
transport: d.hid.transport,
|
|
3565
3621
|
bridge: d.ax.bridge === "degraded" ? "available" : d.ax.bridge,
|
|
3566
3622
|
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
|
-
}
|
|
3623
|
+
detail: healthDetail(d)
|
|
3576
3624
|
}
|
|
3577
3625
|
};
|
|
3578
3626
|
}
|
|
@@ -3619,6 +3667,7 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
3619
3667
|
typeText,
|
|
3620
3668
|
key: (key, modifiers) => sendVoid("key", params({ usage: KEY_USAGE[key], modifiers: modifiers && [...modifiers] })),
|
|
3621
3669
|
selectAll: () => sendVoid("selectAll"),
|
|
3670
|
+
pasteText,
|
|
3622
3671
|
pressButton,
|
|
3623
3672
|
screenshot,
|
|
3624
3673
|
orientation,
|
|
@@ -3635,7 +3684,8 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
3635
3684
|
},
|
|
3636
3685
|
app: {
|
|
3637
3686
|
launch: async (bundleId, o) => {
|
|
3638
|
-
const
|
|
3687
|
+
const viaHelper = await appOp("launch", params({ bundleId, args: o?.args && [...o.args], env: o?.env, terminateRunning: o?.relaunch !== false }), LAUNCH_TIMEOUT_MS);
|
|
3688
|
+
const launched = await appOutcome(viaHelper, () => simctl.launch(bundleId, o), (data) => noteSpanDetail("pid", data.pid));
|
|
3639
3689
|
if (launched.success) {
|
|
3640
3690
|
launchedBundle = bundleId;
|
|
3641
3691
|
if (await rebind(bundleId))
|
|
@@ -3644,12 +3694,18 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
3644
3694
|
return launched;
|
|
3645
3695
|
},
|
|
3646
3696
|
terminate: async (bundleId) => {
|
|
3647
|
-
const
|
|
3697
|
+
const viaHelper = await appOp("terminate", { bundleId }, APP_OP_TIMEOUT_MS);
|
|
3698
|
+
const terminated = await appOutcome(viaHelper, () => simctl.terminate(bundleId), (data) => noteSpanDetail("terminated", data.terminated));
|
|
3648
3699
|
if (terminated.success && launchedBundle === bundleId)
|
|
3649
3700
|
launchedBundle = undefined;
|
|
3650
3701
|
return terminated;
|
|
3651
3702
|
},
|
|
3652
|
-
openUrl: (url) =>
|
|
3703
|
+
openUrl: async (url) => {
|
|
3704
|
+
const viaHelper = await appOp("openUrl", { url }, APP_OP_TIMEOUT_MS);
|
|
3705
|
+
return appOutcome(viaHelper, () => simctl.openUrl(url), () => {
|
|
3706
|
+
return;
|
|
3707
|
+
});
|
|
3708
|
+
},
|
|
3653
3709
|
install: (path) => simctl.install(path),
|
|
3654
3710
|
uninstall: (bundleId) => simctl.uninstall(bundleId),
|
|
3655
3711
|
isInstalled: (bundleId) => simctl.isInstalled(bundleId),
|
|
@@ -4761,6 +4817,10 @@ ${read.error}
|
|
|
4761
4817
|
}
|
|
4762
4818
|
});
|
|
4763
4819
|
}
|
|
4820
|
+
function scrubRunnerEnv(env) {
|
|
4821
|
+
delete env.BUN_BE_BUN;
|
|
4822
|
+
}
|
|
4823
|
+
scrubRunnerEnv(process.env);
|
|
4764
4824
|
if (!hasDefaultDriverFactory()) {
|
|
4765
4825
|
setDefaultDriverFactory(defaultDriverFactory);
|
|
4766
4826
|
setDefaultNativeLog(realNativeLogDeps(bunExec, androidTools(process.env).adb));
|
|
@@ -4784,5 +4844,6 @@ export {
|
|
|
4784
4844
|
BUN_TIMEOUT_MARGIN_MS2 as BUN_TIMEOUT_MARGIN_MS,
|
|
4785
4845
|
installPreload,
|
|
4786
4846
|
isCi,
|
|
4847
|
+
scrubRunnerEnv,
|
|
4787
4848
|
versionMismatch
|
|
4788
4849
|
};
|
package/types/core/src/exec.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type ExecOptions = {
|
|
|
9
9
|
timeoutMs?: number;
|
|
10
10
|
/** aborting kills the command (SIGTERM), like a timeout */
|
|
11
11
|
signal?: AbortSignal;
|
|
12
|
+
/** each line of stdout and stderr as it arrives (no trailing newline); the result still carries the whole output */
|
|
13
|
+
onLine?: (line: string) => void;
|
|
12
14
|
};
|
|
13
15
|
/** Injectable process runner — providers take this so tests can fake `simctl`/`adb`. */
|
|
14
16
|
export type Exec = (cmd: readonly string[], opts?: ExecOptions) => Promise<ExecResult>;
|
|
@@ -31,6 +31,13 @@ export interface DeviceProvider {
|
|
|
31
31
|
bootReady?(id: string, timeoutMs: number): Promise<BootReady>;
|
|
32
32
|
/** Probe for a device `bootReady` just booted and saw ready. Absent = `probe`. */
|
|
33
33
|
probeBooted?(id: string): AsyncResult<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Best effort, called for a device this claim is about to boot from shutdown (after a create, or a
|
|
36
|
+
* shutdown reuse), never for an already-booted one. A failure is logged by the provider, never fatal.
|
|
37
|
+
*/
|
|
38
|
+
prepareBoot?(id: string): Promise<void>;
|
|
39
|
+
/** Called once the device is ready, before the health probe. Best effort. */
|
|
40
|
+
afterReady?(id: string): Promise<void>;
|
|
34
41
|
}
|
|
35
42
|
/** `bootReady`: a boot failure is an error; a device that booted but never got ready is unhealthy. */
|
|
36
43
|
export type BootReady = {
|
|
@@ -6,6 +6,11 @@ export type CdpOptions = {
|
|
|
6
6
|
/** connect timeout and the default per-request timeout */
|
|
7
7
|
timeoutMs?: number;
|
|
8
8
|
};
|
|
9
|
+
/** How a socket closed: the WebSocket close code and reason (Metro's proxy puts a `[TAG]` in the reason). */
|
|
10
|
+
export type CloseInfo = {
|
|
11
|
+
code: number;
|
|
12
|
+
reason: string;
|
|
13
|
+
};
|
|
9
14
|
export type CdpClient = {
|
|
10
15
|
/** `Runtime.evaluate`-style JSON-RPC call; resolves with the reply's `result`. */
|
|
11
16
|
send: (method: string, params?: Record<string, unknown>, options?: {
|
|
@@ -13,8 +18,8 @@ export type CdpClient = {
|
|
|
13
18
|
}) => Promise<DevtoolsResult<unknown>>;
|
|
14
19
|
/** Subscribe to a CDP event by method name; returns the unsubscribe function. */
|
|
15
20
|
on: (event: string, handler: (params: unknown) => void) => () => void;
|
|
16
|
-
/** Called when the socket closes on its own (not after `close()`). */
|
|
17
|
-
onClose: (handler: () => void) => () => void;
|
|
21
|
+
/** Called when the socket closes on its own (not after `close()`), with the close code and reason Metro sent. */
|
|
22
|
+
onClose: (handler: (info: CloseInfo) => void) => () => void;
|
|
18
23
|
/** Every event of every method, in arrival order (a wildcard `on`). */
|
|
19
24
|
onAny: (handler: (method: string, params: unknown) => void) => () => void;
|
|
20
25
|
/**
|
|
@@ -54,6 +54,15 @@ export type CdpTapEvent = {
|
|
|
54
54
|
};
|
|
55
55
|
export type CdpTap = (event: CdpTapEvent) => void;
|
|
56
56
|
export type ConsoleLevel = "log" | "info" | "warn" | "error" | "debug";
|
|
57
|
+
/** One stack frame; `line`/`col` are 1-based. Raw frames carry the bundle URL, symbolicated ones the source file. */
|
|
58
|
+
export type StackFrame = {
|
|
59
|
+
file: string;
|
|
60
|
+
line: number;
|
|
61
|
+
col: number;
|
|
62
|
+
fn?: string;
|
|
63
|
+
/** library / runtime internals Metro marks collapsible */
|
|
64
|
+
collapsed?: boolean;
|
|
65
|
+
};
|
|
57
66
|
/** One captured console message (`console`), uncaught exception (`exception`) or `mark()` (`note`). */
|
|
58
67
|
export type ConsoleLine = {
|
|
59
68
|
/** epoch ms (the runtime's own timestamp when it sent one) */
|
|
@@ -67,6 +76,10 @@ export type ConsoleLine = {
|
|
|
67
76
|
line: number;
|
|
68
77
|
col: number;
|
|
69
78
|
};
|
|
79
|
+
/** the runtime's stack when it sent one; the broker rewrites it to original files (symbolication) */
|
|
80
|
+
stack?: StackFrame[];
|
|
81
|
+
/** Metro's code frame around the first app frame (symbolication) */
|
|
82
|
+
codeFrame?: string;
|
|
70
83
|
/** each console argument rendered on its own (console messages only) */
|
|
71
84
|
args?: string[];
|
|
72
85
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Result } from "../../../types/src/result";
|
|
2
|
+
import { type CloseInfo } from "./cdp";
|
|
2
3
|
import { type CdpTap, type DevtoolsError, type DevtoolsResult, type EvaluateError, type Fetch, type Sleep } from "./devtools.types";
|
|
3
4
|
import { type SelectedTarget } from "./inspector";
|
|
4
5
|
/** The one binding warden installs; scripts call `__warden_cb(JSON.stringify({ requestId, … }))`. */
|
|
@@ -35,6 +36,11 @@ export type ConnectDevtoolsOptions = {
|
|
|
35
36
|
taps?: readonly CdpTap[];
|
|
36
37
|
/** how long `reloadJs` waits for the app to start tearing its JS context down (default 5 s) */
|
|
37
38
|
reloadWaitMs?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Asked when the socket closes on its own. `true` = another debugger took the page: do not reconnect (that would
|
|
41
|
+
* kick it in turn), close the session and emit `yielded`. Default: always reconnect.
|
|
42
|
+
*/
|
|
43
|
+
yieldOnClose?: (info: CloseInfo) => boolean;
|
|
38
44
|
};
|
|
39
45
|
export type DevtoolsSession = {
|
|
40
46
|
/** `Runtime.evaluate` by value. A thrown JS error fails with `exceptionDetails`. Waits out a reconnect. */
|
|
@@ -64,7 +70,7 @@ export type DevtoolsSession = {
|
|
|
64
70
|
/** Tap on every CDP event, handshake events flagged `replay`; survives reconnects. */
|
|
65
71
|
onEvent: (tap: CdpTap) => () => void;
|
|
66
72
|
evaluateWithBinding: (script: string, requestId: string, timeoutMs?: number) => Promise<DevtoolsResult<unknown>>;
|
|
67
|
-
/** `reconnected` (new target attached)
|
|
73
|
+
/** `reconnected` (new target attached), `lost` (gave up reconnecting), `yielded` (`yieldOnClose` said stop) */
|
|
68
74
|
on: <E extends keyof SessionEvents>(event: E, handler: (arg: SessionEvents[E]) => void) => () => void;
|
|
69
75
|
close: () => void;
|
|
70
76
|
};
|
|
@@ -72,6 +78,7 @@ export type ReloadMethod = "expo" | "devsettings" | "http";
|
|
|
72
78
|
export type SessionEvents = {
|
|
73
79
|
reconnected: SelectedTarget;
|
|
74
80
|
lost: DevtoolsError;
|
|
81
|
+
yielded: CloseInfo;
|
|
75
82
|
};
|
|
76
83
|
/**
|
|
77
84
|
* Attach to the app's Hermes runtime through Metro's inspector proxy. Connect sequence (same as Chrome's Fusebox
|
|
@@ -2,20 +2,21 @@ import type { Exec } from "../../../../core/src/exec";
|
|
|
2
2
|
import type { E2eEnv } from "../../env";
|
|
3
3
|
import type { ProbesFrontmost, ReadsAlertOn } from "../alerts";
|
|
4
4
|
import type { Driver, DriverResult, KeyName, Logger } from "../driver.types";
|
|
5
|
-
import type { SelectsAll } from "../typing";
|
|
5
|
+
import type { PastesText, SelectsAll } from "../typing";
|
|
6
6
|
import { type SwappableClient } from "./ios-session";
|
|
7
7
|
import { type Simctl } from "./simctl";
|
|
8
8
|
import { type SimdClient } from "./simd-client";
|
|
9
9
|
import type { SimdSpawn } from "./simd-transport";
|
|
10
10
|
/**
|
|
11
|
-
* The iOS `Driver`: input and
|
|
12
|
-
* helper spawned on stdio)
|
|
13
|
-
* `typeText` pastes what the HID keyboard map
|
|
14
|
-
* helper has no native quiescence, `screenshot` uses
|
|
11
|
+
* The iOS `Driver`: input, AX and app launch / terminate / openUrl through `warden-simd` (socket when
|
|
12
|
+
* `WARDEN_SIMD_SOCKET` is set, else a helper spawned on stdio); install / uninstall / isRunning / clearData,
|
|
13
|
+
* clipboard and appearance through `xcrun simctl`. Fallbacks: `typeText` pastes what the HID keyboard map
|
|
14
|
+
* cannot type, `settle` polls snapshot hashes when the helper has no native quiescence, `screenshot` uses
|
|
15
|
+
* `simctl io` when the helper cannot, and a helper built before the app ops leaves app lifecycle to `simctl`.
|
|
15
16
|
*/
|
|
16
17
|
export type IosDriverEnv = Pick<E2eEnv, "simd" | "simdSocket" | "helperDir">;
|
|
17
18
|
/** The iOS driver is a `Driver` plus the optional capabilities of `driver/alerts.ts`. */
|
|
18
|
-
export type IosDriver = Driver & SelectsAll & ReadsAlertOn & ProbesFrontmost;
|
|
19
|
+
export type IosDriver = Driver & SelectsAll & PastesText & ReadsAlertOn & ProbesFrontmost;
|
|
19
20
|
export type OpenIosDriverOptions = {
|
|
20
21
|
udid: string;
|
|
21
22
|
env?: IosDriverEnv;
|
|
@@ -275,6 +275,21 @@ export declare const frontmostDataSchema: z.ZodObject<{
|
|
|
275
275
|
frontmostPid: z.ZodOptional<z.ZodNumber>;
|
|
276
276
|
}, z.core.$loose>;
|
|
277
277
|
export type FrontmostData = z.infer<typeof frontmostDataSchema>;
|
|
278
|
+
/**
|
|
279
|
+
* `launch{bundleId, args?, env?, terminateRunning?}`: the helper starts the app in the process that already holds
|
|
280
|
+
* CoreSimulator (no `simctl` process per launch). `args` follow the executable, `env` reaches the app as given, and
|
|
281
|
+
* `terminateRunning` (default true) ends a running instance first; false brings a running app to the front instead.
|
|
282
|
+
* `pid` is the process that is running when the op answers.
|
|
283
|
+
*/
|
|
284
|
+
export declare const launchDataSchema: z.ZodObject<{
|
|
285
|
+
pid: z.ZodNumber;
|
|
286
|
+
}, z.core.$loose>;
|
|
287
|
+
export type LaunchData = z.infer<typeof launchDataSchema>;
|
|
288
|
+
/** `terminate{bundleId}`: `terminated: false` says nothing was running, which is the wanted state, not an error. */
|
|
289
|
+
export declare const terminateDataSchema: z.ZodObject<{
|
|
290
|
+
terminated: z.ZodBoolean;
|
|
291
|
+
}, z.core.$loose>;
|
|
292
|
+
export type TerminateData = z.infer<typeof terminateDataSchema>;
|
|
278
293
|
/**
|
|
279
294
|
* `orientation`: a real read may also say `faceUp` / `faceDown` / `unknown` (flat device); `source` tells
|
|
280
295
|
* whether the value was read from the device, is the last one this helper wrote, or is an assumed portrait.
|
|
@@ -401,6 +416,12 @@ export declare const opSchemas: {
|
|
|
401
416
|
assumed: "assumed";
|
|
402
417
|
}>>;
|
|
403
418
|
}, z.core.$strip>;
|
|
419
|
+
readonly launch: z.ZodObject<{
|
|
420
|
+
pid: z.ZodNumber;
|
|
421
|
+
}, z.core.$loose>;
|
|
422
|
+
readonly terminate: z.ZodObject<{
|
|
423
|
+
terminated: z.ZodBoolean;
|
|
424
|
+
}, z.core.$loose>;
|
|
404
425
|
};
|
|
405
426
|
export type OpDataMap = {
|
|
406
427
|
health: HealthData;
|
|
@@ -412,6 +433,8 @@ export type OpDataMap = {
|
|
|
412
433
|
alertButtons: AlertButtonsData;
|
|
413
434
|
frontmost: FrontmostData;
|
|
414
435
|
orientation: OrientationData;
|
|
436
|
+
launch: LaunchData;
|
|
437
|
+
terminate: TerminateData;
|
|
415
438
|
};
|
|
416
439
|
export type OpData<Op extends string> = Op extends keyof OpDataMap ? OpDataMap[Op] : unknown;
|
|
417
440
|
export declare function hasOpSchema(op: string): op is keyof typeof opSchemas;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Per-request budget for the helper's `typeText`: it holds, paces and (for a repeated key) waits per character.
|
|
3
|
+
* The helper's defaults (pace 5, hold 20, repeat gap 50) cost ~38 ms per character on a quiet local simulator; the
|
|
4
|
+
* budget allows ~100 ms (the EAS VirtualMac ran ~3.7x the nominal sleeps) and 200 ms per repeated key.
|
|
5
|
+
*/
|
|
2
6
|
export declare const TYPE_BASE_MS = 10000;
|
|
3
|
-
export declare const TYPE_PER_CHAR_MS =
|
|
4
|
-
export declare const TYPE_PER_REPEAT_MS =
|
|
5
|
-
/** Base + ~
|
|
7
|
+
export declare const TYPE_PER_CHAR_MS = 100;
|
|
8
|
+
export declare const TYPE_PER_REPEAT_MS = 200;
|
|
9
|
+
/** Base + ~100 ms per character + 200 ms per character equal to the one before it. */
|
|
6
10
|
export declare function typeTextBudgetMs(text: string): number;
|
|
@@ -8,7 +8,9 @@ import type { AlertInfo, Driver, DriverResult, Platform, Snapshot } from "./driv
|
|
|
8
8
|
* the one driver: whatever calls it, an input moves the *generation* on and the kept snapshot dies with the old one.
|
|
9
9
|
* A snapshot is also dropped once it is older than the platform's max age, input or not (the app can change by itself).
|
|
10
10
|
* Which reads may reuse one is the caller's call (`observe({ reuse })`): only the first look of a wait does, so a
|
|
11
|
-
* poll never spins on a tree that stopped being true.
|
|
11
|
+
* poll never spins on a tree that stopped being true. A `settle` is a wait, not an input: the kept snapshot predates
|
|
12
|
+
* it and is dropped, but nothing was pressed, so `previous().sinceInputMs` still counts from the last real input; one
|
|
13
|
+
* that reports `quiet` is the app's word that the input has landed, and then no input is recent at all.
|
|
12
14
|
*/
|
|
13
15
|
/** How old a kept snapshot may be and still answer a read. iOS reads cost ~100-300 ms; an Android dump takes ~1.9 s. */
|
|
14
16
|
export declare const OBSERVATION_MAX_AGE_MS: Record<Platform, number>;
|
|
@@ -42,6 +42,16 @@ export type SetsText = {
|
|
|
42
42
|
setText(text: string): DriverResult<void>;
|
|
43
43
|
};
|
|
44
44
|
export declare function canSetText(driver: Driver): driver is Driver & SetsText;
|
|
45
|
+
/**
|
|
46
|
+
* Optional fast path next to the frozen `Driver`: put `text` on the device pasteboard and paste it into the focused
|
|
47
|
+
* field with one Cmd+V (iOS `simctl.pbcopy` then `warden-simd` `paste`), replacing the selection. Cost is flat (~400 ms)
|
|
48
|
+
* where typing is 70-260 ms per character. A pasteboard write that cannot be confirmed is an error (nothing was
|
|
49
|
+
* pasted) and the caller types the old way.
|
|
50
|
+
*/
|
|
51
|
+
export type PastesText = {
|
|
52
|
+
pasteText(text: string): DriverResult<void>;
|
|
53
|
+
};
|
|
54
|
+
export declare function canPasteText(driver: Driver): driver is Driver & PastesText;
|
|
45
55
|
/** Types `text` at the caret. Android types the whole string in one call. */
|
|
46
56
|
export declare function typeText(driver: Driver, sleep: Sleep, text: string): DriverResult<void>;
|
|
47
57
|
/** Presses `key` `times` times, paced like typed characters. */
|
|
@@ -27,3 +27,8 @@ export declare function isCi(env: Readonly<Record<string, string | undefined>>):
|
|
|
27
27
|
* session eagerly and, in `afterAll`, closes it and (under `CI`) fails the run if a `.only` was collected.
|
|
28
28
|
*/
|
|
29
29
|
export declare function installPreload(deps: PreloadDeps): void;
|
|
30
|
+
/**
|
|
31
|
+
* `warden e2e` may run this process as a compiled warden with `BUN_BE_BUN=1`; drop it so a `warden` the spec spawns
|
|
32
|
+
* (`WARDEN_BIN`, `device.perf`) runs as warden, not bun.
|
|
33
|
+
*/
|
|
34
|
+
export declare function scrubRunnerEnv(env: Record<string, string | undefined>): void;
|
|
@@ -101,6 +101,12 @@ export type ClearMethod = "auto" | "counted" | "setText";
|
|
|
101
101
|
export declare const SETTEXT_ENV = "WARDEN_ANDROID_SETTEXT";
|
|
102
102
|
/** Whether `env` opts `auto` fills into `setText` (`SETTEXT_ENV`). */
|
|
103
103
|
export declare function setTextOptedIn(env: Readonly<Record<string, string | undefined>>): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* How `focusClearType` puts text in. `paste` writes it to the pasteboard and sends one Cmd+V on a driver that offers
|
|
106
|
+
* `pasteText` (iOS: ~400 ms flat against 70-260 ms per typed character), and types when it does not or when the paste
|
|
107
|
+
* fails; `type` always types.
|
|
108
|
+
*/
|
|
109
|
+
export type FillVia = "paste" | "type";
|
|
104
110
|
/**
|
|
105
111
|
* Taps `point` and empties what `entry` shows: select-all and one Backspace when the driver offers it, else one
|
|
106
112
|
* Backspace per character (see `focusAndSelect`). The clear routine behind the spec `clear` and `warden ui clear`.
|
|
@@ -117,5 +123,9 @@ export declare function focusAndClear(driver: Driver, sleep: Sleep, point: Point
|
|
|
117
123
|
* select-all then typing; the read-back after a `fill` checks it took, and the counted retry types. An empty field gains
|
|
118
124
|
* nothing from it, and a secure one cannot be read back, so both are typed as before. A `setText` that fails falls back
|
|
119
125
|
* to select-all and typing, without a second tap.
|
|
126
|
+
*
|
|
127
|
+
* With `via: "paste"` (the default) on a driver with `pasteText` (iOS), the text replaces the selection with one paste,
|
|
128
|
+
* secure fields included (they keep their Backspace-per-dot clearing). A paste that fails (pasteboard write not
|
|
129
|
+
* confirmed) is typed in the same attempt; nothing was pasted, so the selection is still there to be replaced.
|
|
120
130
|
*/
|
|
121
|
-
export declare function focusClearType(driver: Driver, sleep: Sleep, point: Point, entry: NodeEntry | undefined, text: string, method?: ClearMethod, env?: Readonly<Record<string, string | undefined
|
|
131
|
+
export declare function focusClearType(driver: Driver, sleep: Sleep, point: Point, entry: NodeEntry | undefined, text: string, method?: ClearMethod, env?: Readonly<Record<string, string | undefined>>, via?: FillVia): DriverResult<void>;
|