@delacour/warden-e2e 0.12.0 → 0.13.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-qz94hsb6.mjs → chunk-tnr47n9c.mjs} +7 -3
- package/index.mjs +114 -1
- package/package.json +2 -2
- package/preload.mjs +67 -5
- package/types/core/src/affected/affected.schema.d.ts +2 -0
- package/types/core/src/builds/config.schema.d.ts +1 -0
- package/types/core/src/builds/define-config.d.ts +10 -0
- package/types/e2e/src/driver/driver.types.d.ts +5 -0
- package/types/e2e/src/driver/ios/ios-driver.d.ts +12 -1
- package/types/e2e/src/errors.d.ts +1 -1
- package/types/e2e/src/index.d.ts +2 -0
- package/types/e2e/src/run.d.ts +48 -0
- package/types/e2e/src/screen/context.d.ts +10 -0
- package/types/e2e/src/session.d.ts +2 -0
|
@@ -723,8 +723,9 @@ var UNCHECKED_WORDS = new Set(["0", "off", "false", "unchecked"]);
|
|
|
723
723
|
function nonEmpty(value) {
|
|
724
724
|
return value === undefined || value === "" ? undefined : value;
|
|
725
725
|
}
|
|
726
|
+
var MASKED_VALUE = /^[\u2022\u25CF]+$/;
|
|
726
727
|
function isSecure(element, traits) {
|
|
727
|
-
return element.role === "securefield" || traits.has("password") || traits.has("secure") || element.rawType.toLowerCase().includes("secure");
|
|
728
|
+
return element.role === "textfield" && element.value !== undefined && MASKED_VALUE.test(element.value) || element.role === "securefield" || traits.has("password") || traits.has("secure") || element.rawType.toLowerCase().includes("secure");
|
|
728
729
|
}
|
|
729
730
|
function deriveChecked(element, role, spelled, traits, value) {
|
|
730
731
|
if (spelled?.checked !== undefined)
|
|
@@ -6298,6 +6299,9 @@ function getSession(opts = {}) {
|
|
|
6298
6299
|
slot()[SESSION_KEY] = session;
|
|
6299
6300
|
return session;
|
|
6300
6301
|
}
|
|
6302
|
+
function peekSession() {
|
|
6303
|
+
return slot()[SESSION_KEY];
|
|
6304
|
+
}
|
|
6301
6305
|
|
|
6302
6306
|
// ../../packages/e2e/src/a11y/speech.ts
|
|
6303
6307
|
var ACTIONABLE_ROLES = new Set([
|
|
@@ -9323,6 +9327,6 @@ function onlyWasSeen() {
|
|
|
9323
9327
|
|
|
9324
9328
|
// ../../packages/e2e/src/version.ts
|
|
9325
9329
|
var WORKSPACE_VERSION = "0.0.0-workspace";
|
|
9326
|
-
var WARDEN_E2E_VERSION2 = "0.
|
|
9330
|
+
var WARDEN_E2E_VERSION2 = "0.13.0";
|
|
9327
9331
|
|
|
9328
|
-
export { E2eError2, registerA11yTarget, a11yTargetOf, canActOnElements, 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, formatNativeTail, realNativeLogDeps, captureNativeLog, bunExec, execError, driverError, driverErr, logEventSchema, fatalEventSchema, responseSchema, mapWireError, createLineSplitter, encodeRequest, bunSpawn, stdioTransport, socketTransport, createSimdClient, spawnSimd, connectSimd, isRecoverableLoss, CAMERA_FACINGS, CONDITION_AREAS, resolveNetwork, resolveTextSize, androidFontScale, ENV_NAMES, DEFAULT_ASSERTION_TIMEOUT_MS, readEnv, runStep, setDefaultHidRecovery, setDefaultNativeLog, setDefaultDriverFactory, hasDefaultDriverFactory, setDefaultFixtureFactory, hasDefaultFixtureFactory, attemptSession, getSession, accessibleName, announce, failing, formatIssues, toE2eError, settled, unwrap, parseUiadA11yEvents, parseInjectEvents, withA11yRead, renderA11yScreen, createDeviceA11y, IOS_DEVICE_CAPABILITIES, sniffCameraMedia, cameraFeedDir, clearCameraFeeds, isLeaseAlive, processAlive, describeOwner, isPhysicalIosId, wardenHome, defaultDbPath, ownerKey, openStore, callerFile, formatCoord, DEFAULT_SPEED_MPS, DEFAULT_INTERVAL_S, realTickerClock, startRouteTicker, startFlap, registerConditions, conditionsOf, conditionSinkFor, createDeviceConditions, parseBudgets, createDevicePerf, perfStartFor, spawnWarden, hash6, attemptDir, screenshotPath, specIdOf, BUN_TIMEOUT_MARGIN_MS2, ownHook, currentScope, createScope, runInScope, hooksFor, resolveUse, registrationDefaults, markOnly, onlyWasSeen, WORKSPACE_VERSION, WARDEN_E2E_VERSION2 };
|
|
9332
|
+
export { E2eError2, registerA11yTarget, a11yTargetOf, canActOnElements, 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, formatNativeTail, realNativeLogDeps, captureNativeLog, bunExec, execError, driverError, driverErr, logEventSchema, fatalEventSchema, responseSchema, mapWireError, createLineSplitter, encodeRequest, bunSpawn, stdioTransport, socketTransport, createSimdClient, spawnSimd, connectSimd, isRecoverableLoss, parseDuration, CAMERA_FACINGS, CONDITION_AREAS, resolveNetwork, resolveTextSize, androidFontScale, ENV_NAMES, DEFAULT_ASSERTION_TIMEOUT_MS, readEnv, runStep, setDefaultHidRecovery, setDefaultNativeLog, setDefaultDriverFactory, hasDefaultDriverFactory, setDefaultFixtureFactory, hasDefaultFixtureFactory, attemptSession, getSession, peekSession, accessibleName, announce, failing, formatIssues, toE2eError, settled, unwrap, parseUiadA11yEvents, parseInjectEvents, withA11yRead, renderA11yScreen, createDeviceA11y, IOS_DEVICE_CAPABILITIES, sniffCameraMedia, cameraFeedDir, clearCameraFeeds, isLeaseAlive, processAlive, describeOwner, isPhysicalIosId, wardenHome, defaultDbPath, ownerKey, openStore, callerFile, formatCoord, DEFAULT_SPEED_MPS, DEFAULT_INTERVAL_S, realTickerClock, startRouteTicker, startFlap, registerConditions, conditionsOf, conditionSinkFor, createDeviceConditions, parseBudgets, createDevicePerf, perfStartFor, spawnWarden, hash6, attemptDir, screenshotPath, specIdOf, BUN_TIMEOUT_MARGIN_MS2, ownHook, currentScope, createScope, runInScope, hooksFor, resolveUse, registrationDefaults, markOnly, onlyWasSeen, WORKSPACE_VERSION, WARDEN_E2E_VERSION2 };
|
package/index.mjs
CHANGED
|
@@ -15,9 +15,11 @@ import {
|
|
|
15
15
|
formatNativeTail,
|
|
16
16
|
captureNativeLog,
|
|
17
17
|
isRecoverableLoss,
|
|
18
|
+
parseDuration,
|
|
18
19
|
runStep,
|
|
19
20
|
attemptSession,
|
|
20
21
|
getSession,
|
|
22
|
+
peekSession,
|
|
21
23
|
accessibleName,
|
|
22
24
|
announce,
|
|
23
25
|
failing,
|
|
@@ -42,7 +44,7 @@ import {
|
|
|
42
44
|
registrationDefaults,
|
|
43
45
|
markOnly,
|
|
44
46
|
WARDEN_E2E_VERSION2
|
|
45
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-tnr47n9c.mjs";
|
|
46
48
|
// ../../packages/e2e/src/expect/expect.ts
|
|
47
49
|
import { expect as bunExpect } from "bun:test";
|
|
48
50
|
|
|
@@ -572,6 +574,116 @@ function createExpect(deps) {
|
|
|
572
574
|
return call;
|
|
573
575
|
}
|
|
574
576
|
var expect = createExpect({ softSink: currentSoftSink });
|
|
577
|
+
// ../../packages/e2e/src/run.ts
|
|
578
|
+
var DEFAULT_TIMEOUT_MS = 60000;
|
|
579
|
+
var MESSAGE_TAIL = 2000;
|
|
580
|
+
var invalid = (message) => new E2eError2("INVALID_ARGUMENT", `run: ${message}`);
|
|
581
|
+
function timeoutMs(value) {
|
|
582
|
+
if (value === undefined)
|
|
583
|
+
return DEFAULT_TIMEOUT_MS;
|
|
584
|
+
if (typeof value === "number") {
|
|
585
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
586
|
+
throw invalid(`timeout must be over 0, got ${String(value)}`);
|
|
587
|
+
return value;
|
|
588
|
+
}
|
|
589
|
+
const parsed = parseDuration(value);
|
|
590
|
+
if (!parsed.success)
|
|
591
|
+
throw invalid(`timeout: ${parsed.error}`);
|
|
592
|
+
return parsed.data;
|
|
593
|
+
}
|
|
594
|
+
function argvOf(command) {
|
|
595
|
+
if (typeof command === "string") {
|
|
596
|
+
if (command.trim() === "")
|
|
597
|
+
throw invalid("command is empty");
|
|
598
|
+
return ["sh", "-c", command];
|
|
599
|
+
}
|
|
600
|
+
if (command.length === 0 || command[0] === "")
|
|
601
|
+
throw invalid("argv is empty");
|
|
602
|
+
return [...command];
|
|
603
|
+
}
|
|
604
|
+
var programOf = (command) => typeof command === "string" ? "sh -c" : command[0] ?? "";
|
|
605
|
+
function envOf(extra) {
|
|
606
|
+
const merged = { ...process.env, ...extra };
|
|
607
|
+
const env = {};
|
|
608
|
+
for (const [key, value] of Object.entries(merged))
|
|
609
|
+
if (value !== undefined)
|
|
610
|
+
env[key] = value;
|
|
611
|
+
return env;
|
|
612
|
+
}
|
|
613
|
+
function tail(text) {
|
|
614
|
+
const trimmed = text.trim();
|
|
615
|
+
return trimmed.length > MESSAGE_TAIL ? `\u2026${trimmed.slice(-MESSAGE_TAIL)}` : trimmed;
|
|
616
|
+
}
|
|
617
|
+
function resultOf(exitCode, stdout, stderr, ms) {
|
|
618
|
+
return {
|
|
619
|
+
ok: exitCode === 0,
|
|
620
|
+
exitCode,
|
|
621
|
+
stdout,
|
|
622
|
+
stderr,
|
|
623
|
+
ms,
|
|
624
|
+
json() {
|
|
625
|
+
try {
|
|
626
|
+
return JSON.parse(stdout);
|
|
627
|
+
} catch {
|
|
628
|
+
throw invalid(`stdout is not JSON: ${tail(stdout).slice(0, 200) || "(empty)"}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
async function exec(command, options) {
|
|
634
|
+
const argv = argvOf(command);
|
|
635
|
+
const limit = timeoutMs(options.timeout);
|
|
636
|
+
const started = Date.now();
|
|
637
|
+
let proc;
|
|
638
|
+
try {
|
|
639
|
+
proc = Bun.spawn(argv, {
|
|
640
|
+
cwd: options.cwd,
|
|
641
|
+
env: envOf(options.env),
|
|
642
|
+
stdin: "pipe",
|
|
643
|
+
stdout: "pipe",
|
|
644
|
+
stderr: "pipe"
|
|
645
|
+
});
|
|
646
|
+
} catch (error) {
|
|
647
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
648
|
+
throw new E2eError2("RUN_FAILED", `run: could not start ${programOf(command)}: ${reason}`);
|
|
649
|
+
}
|
|
650
|
+
if (options.input !== undefined)
|
|
651
|
+
proc.stdin.write(options.input);
|
|
652
|
+
await proc.stdin.end();
|
|
653
|
+
let timedOut = false;
|
|
654
|
+
const timer = setTimeout(() => {
|
|
655
|
+
timedOut = true;
|
|
656
|
+
proc.kill("SIGKILL");
|
|
657
|
+
}, limit);
|
|
658
|
+
try {
|
|
659
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
660
|
+
new Response(proc.stdout).text(),
|
|
661
|
+
new Response(proc.stderr).text(),
|
|
662
|
+
proc.exited
|
|
663
|
+
]);
|
|
664
|
+
const result = resultOf(exitCode, stdout, stderr, Date.now() - started);
|
|
665
|
+
if (timedOut) {
|
|
666
|
+
throw new E2eError2("TIMEOUT", `run: ${programOf(command)} did not finish within ${limit} ms (killed)`);
|
|
667
|
+
}
|
|
668
|
+
if (!result.ok && options.check !== false) {
|
|
669
|
+
const output = tail(stderr) || tail(stdout);
|
|
670
|
+
throw new E2eError2("RUN_FAILED", `run: ${programOf(command)} failed with exit ${exitCode}${output === "" ? "" : `
|
|
671
|
+
${output}`}`);
|
|
672
|
+
}
|
|
673
|
+
return result;
|
|
674
|
+
} finally {
|
|
675
|
+
clearTimeout(timer);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
function createRun(deps) {
|
|
679
|
+
return (command, options = {}) => deps.span("run", () => exec(command, options), {
|
|
680
|
+
verdict: (result) => result.ok ? { ok: true } : { ok: false, code: "RUN_FAILED" },
|
|
681
|
+
detail: (result) => ({ exitCode: result.exitCode })
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
var run = createRun({
|
|
685
|
+
span: (op, fn, options) => peekSession()?.span(op, fn, options) ?? fn()
|
|
686
|
+
});
|
|
575
687
|
// ../../packages/e2e/src/runner/test.ts
|
|
576
688
|
import { afterAll as bunAfterAll, beforeAll as bunBeforeAll, describe as bunDescribe, test as bunTest } from "bun:test";
|
|
577
689
|
|
|
@@ -1277,5 +1389,6 @@ export {
|
|
|
1277
1389
|
beforeEach,
|
|
1278
1390
|
describe,
|
|
1279
1391
|
expect,
|
|
1392
|
+
run,
|
|
1280
1393
|
test
|
|
1281
1394
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delacour/warden-e2e",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "TypeScript e2e testing for React Native and Expo on iOS simulators and Android emulators, run by `warden e2e`: test, expect, screen, device, app",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"LICENSE"
|
|
42
42
|
],
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@delacour/warden": "0.
|
|
44
|
+
"@delacour/warden": "0.13.0"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"zod": "^4.4.3"
|
package/preload.mjs
CHANGED
|
@@ -109,7 +109,7 @@ import {
|
|
|
109
109
|
onlyWasSeen,
|
|
110
110
|
WORKSPACE_VERSION,
|
|
111
111
|
WARDEN_E2E_VERSION2
|
|
112
|
-
} from "./chunk-
|
|
112
|
+
} from "./chunk-tnr47n9c.mjs";
|
|
113
113
|
|
|
114
114
|
// ../../packages/e2e/src/preload.ts
|
|
115
115
|
import { afterAll, setDefaultTimeout } from "bun:test";
|
|
@@ -7728,6 +7728,7 @@ function locateHelper(input) {
|
|
|
7728
7728
|
function resolveHelper(o) {
|
|
7729
7729
|
if (o.helperPath !== undefined)
|
|
7730
7730
|
return { success: true, data: o.helperPath };
|
|
7731
|
+
const version = o.version ?? o.env?.wardenVersion;
|
|
7731
7732
|
const located = locateHelper({
|
|
7732
7733
|
env: { [ENV_NAMES.simd]: o.env?.simd, [ENV_NAMES.helperDir]: o.env?.helperDir },
|
|
7733
7734
|
execPath: o.execPath ?? process.execPath,
|
|
@@ -7735,7 +7736,7 @@ function resolveHelper(o) {
|
|
|
7735
7736
|
wardenHome: o.wardenHome ?? wardenHome(),
|
|
7736
7737
|
exists: o.exists ?? existsSync7,
|
|
7737
7738
|
realpath: o.realpath ?? realpathSync,
|
|
7738
|
-
...
|
|
7739
|
+
...version ? { version } : {}
|
|
7739
7740
|
});
|
|
7740
7741
|
return located.success ? { success: true, data: located.data.path } : located;
|
|
7741
7742
|
}
|
|
@@ -7816,6 +7817,8 @@ function toSnapshot(data, takenAt) {
|
|
|
7816
7817
|
};
|
|
7817
7818
|
if (data.modal)
|
|
7818
7819
|
snapshot.modal = data.modal;
|
|
7820
|
+
if (data.fallback?.from === "axbridge")
|
|
7821
|
+
snapshot.fellBackFrom = "axbridge";
|
|
7819
7822
|
return snapshot;
|
|
7820
7823
|
}
|
|
7821
7824
|
|
|
@@ -8401,6 +8404,9 @@ var DEFAULT_QUIET_MS = 250;
|
|
|
8401
8404
|
var DEFAULT_SETTLE_TIMEOUT_MS2 = 5000;
|
|
8402
8405
|
var SETTLE_MARGIN_MS = 2000;
|
|
8403
8406
|
var REBIND_TIMEOUT_MS = 1e4;
|
|
8407
|
+
var COLD_BRIDGE_BUDGET_MS = 60000;
|
|
8408
|
+
var COLD_BRIDGE_FIRST_WAIT_MS = 2000;
|
|
8409
|
+
var COLD_BRIDGE_WAIT_MS = 5000;
|
|
8404
8410
|
var GESTURE_MARGIN_MS = 5000;
|
|
8405
8411
|
var FRONTMOST_TIMEOUT_MS = 3000;
|
|
8406
8412
|
var LAUNCH_TIMEOUT_MS = 30000;
|
|
@@ -8490,6 +8496,32 @@ function noteHelperHealth(health) {
|
|
|
8490
8496
|
if (health.frontmost)
|
|
8491
8497
|
noteSpanDetail("frontmostUnresolvedAgoMs", health.frontmost.unresolvedAgoMs);
|
|
8492
8498
|
}
|
|
8499
|
+
function coldBridgeWait(error, attempt, spentMs, budgetMs) {
|
|
8500
|
+
if (!bridgeNotReady(error))
|
|
8501
|
+
return;
|
|
8502
|
+
const wait = attempt === 1 ? COLD_BRIDGE_FIRST_WAIT_MS : COLD_BRIDGE_WAIT_MS;
|
|
8503
|
+
return spentMs + wait >= budgetMs ? undefined : wait;
|
|
8504
|
+
}
|
|
8505
|
+
async function retryColdSnapshot(read, ctx) {
|
|
8506
|
+
const started = ctx.now();
|
|
8507
|
+
let waited = 0;
|
|
8508
|
+
for (let attempt = 1;; attempt++) {
|
|
8509
|
+
const result = await read();
|
|
8510
|
+
const spent = Math.max(ctx.now() - started, waited);
|
|
8511
|
+
const wait = result.success ? undefined : coldBridgeWait(result.error, attempt, spent, ctx.budgetMs);
|
|
8512
|
+
if (wait === undefined) {
|
|
8513
|
+
if (attempt > 1)
|
|
8514
|
+
noteSpanDetail("coldBridgeMs", spent);
|
|
8515
|
+
return result;
|
|
8516
|
+
}
|
|
8517
|
+
ctx.log?.(`[ios] snapshot try ${attempt} failed, the bridge is not up yet: retrying`);
|
|
8518
|
+
await ctx.sleep(wait);
|
|
8519
|
+
waited += wait;
|
|
8520
|
+
}
|
|
8521
|
+
}
|
|
8522
|
+
function bridgeNotReady(error) {
|
|
8523
|
+
return error.code === "E_TIMEOUT" || error.code === "E_AX_UNAVAILABLE";
|
|
8524
|
+
}
|
|
8493
8525
|
function noteRebind(data) {
|
|
8494
8526
|
noteSpanDetail("rebind", data.bound ? "bound" : data.reason ?? "unbound");
|
|
8495
8527
|
noteSpanDetail("rebindMs", data.waitedMs);
|
|
@@ -8562,6 +8594,7 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
8562
8594
|
let settleUnsupported = !nativeSettle;
|
|
8563
8595
|
let settleAxFailures = 0;
|
|
8564
8596
|
let rebindUnsupported = false;
|
|
8597
|
+
let bridgeProven = false;
|
|
8565
8598
|
let gestureUnsupported = false;
|
|
8566
8599
|
let appOpsOutdated = false;
|
|
8567
8600
|
const capabilities = {
|
|
@@ -8681,7 +8714,7 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
8681
8714
|
}
|
|
8682
8715
|
return { success: true, data: toSnapshot(result.data, now()) };
|
|
8683
8716
|
};
|
|
8684
|
-
async function
|
|
8717
|
+
async function snapshotOnce(o) {
|
|
8685
8718
|
const generation = swap.generation();
|
|
8686
8719
|
const first = await fetchSnapshot(o);
|
|
8687
8720
|
if (first.success) {
|
|
@@ -8702,6 +8735,19 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
8702
8735
|
axFailures = 0;
|
|
8703
8736
|
return fetchSnapshot(o);
|
|
8704
8737
|
}
|
|
8738
|
+
async function snapshot(o) {
|
|
8739
|
+
if (bridgeProven)
|
|
8740
|
+
return snapshotOnce(o);
|
|
8741
|
+
const result = await retryColdSnapshot(() => snapshotOnce(o), {
|
|
8742
|
+
now,
|
|
8743
|
+
sleep,
|
|
8744
|
+
budgetMs: opts.coldBridgeBudgetMs ?? COLD_BRIDGE_BUDGET_MS,
|
|
8745
|
+
...opts.log ? { log: opts.log } : {}
|
|
8746
|
+
});
|
|
8747
|
+
if (result.success)
|
|
8748
|
+
bridgeProven = true;
|
|
8749
|
+
return result;
|
|
8750
|
+
}
|
|
8705
8751
|
async function typeText(text) {
|
|
8706
8752
|
const typed = await sendVoid("typeText", { text }, typeTextBudgetMs(text));
|
|
8707
8753
|
if (typed.success || typed.error.code !== "E_UNSUPPORTED_TEXT")
|
|
@@ -8762,6 +8808,8 @@ function createIosDriver(swap, simctl, opts) {
|
|
|
8762
8808
|
return false;
|
|
8763
8809
|
}
|
|
8764
8810
|
noteRebind(reply.data);
|
|
8811
|
+
if (reply.data.bound)
|
|
8812
|
+
bridgeProven = true;
|
|
8765
8813
|
return reply.data.bound;
|
|
8766
8814
|
}
|
|
8767
8815
|
function noteSettleAxFailure(message) {
|
|
@@ -9082,6 +9130,7 @@ async function openIosDriver(o) {
|
|
|
9082
9130
|
now,
|
|
9083
9131
|
sleep,
|
|
9084
9132
|
log,
|
|
9133
|
+
...o.coldBridgeBudgetMs !== undefined ? { coldBridgeBudgetMs: o.coldBridgeBudgetMs } : {},
|
|
9085
9134
|
...network ? { network } : {},
|
|
9086
9135
|
a11y: () => iosA11yFor(o, now, sleep),
|
|
9087
9136
|
nativeAudit: (ctx) => nativeAuditFor(o, exec, log, ctx),
|
|
@@ -10485,11 +10534,13 @@ var EXCERPT_LINES = 40;
|
|
|
10485
10534
|
var MAX_ALERT_ANSWERS = 3;
|
|
10486
10535
|
var REACT_THROTTLE_MS = 450;
|
|
10487
10536
|
var REACT_TIMEOUT_MS = 2000;
|
|
10537
|
+
var FIRST_READER_RETRY_MS = 5000;
|
|
10488
10538
|
|
|
10489
10539
|
class Runtime {
|
|
10490
10540
|
host;
|
|
10491
10541
|
reactCache;
|
|
10492
10542
|
indexed = new WeakMap;
|
|
10543
|
+
firstReaderRetryAt = 0;
|
|
10493
10544
|
constructor(host) {
|
|
10494
10545
|
this.host = host;
|
|
10495
10546
|
}
|
|
@@ -10529,19 +10580,30 @@ class Runtime {
|
|
|
10529
10580
|
const result = canObserve(driver) ? await driver.observe({ reuse }) : await driver.snapshot();
|
|
10530
10581
|
if (!result.success)
|
|
10531
10582
|
return { ok: false, error: result.error };
|
|
10583
|
+
const snapshot = await this.restoreDegraded(driver, result.data);
|
|
10532
10584
|
const observation = {
|
|
10533
10585
|
ok: true,
|
|
10534
|
-
observed: { driver, snapshot
|
|
10586
|
+
observed: { driver, snapshot, index: this.indexOf(snapshot) }
|
|
10535
10587
|
};
|
|
10536
10588
|
if (before !== undefined)
|
|
10537
10589
|
observation.earlier = this.earlierThan(driver, before, result.data);
|
|
10538
10590
|
if (options.alerts === false || answered >= MAX_ALERT_ANSWERS)
|
|
10539
10591
|
return observation;
|
|
10540
|
-
if (!await this.answeredAlert(driver,
|
|
10592
|
+
if (!await this.answeredAlert(driver, snapshot))
|
|
10541
10593
|
return observation;
|
|
10542
10594
|
reuse = false;
|
|
10543
10595
|
}
|
|
10544
10596
|
}
|
|
10597
|
+
async restoreDegraded(driver, snapshot) {
|
|
10598
|
+
const first = snapshot.fellBackFrom;
|
|
10599
|
+
if (first === undefined || this.clock.now() < this.firstReaderRetryAt)
|
|
10600
|
+
return snapshot;
|
|
10601
|
+
const again = await driver.snapshot({ backend: first });
|
|
10602
|
+
if (again.success && again.data.backend === first)
|
|
10603
|
+
return again.data;
|
|
10604
|
+
this.firstReaderRetryAt = this.clock.now() + FIRST_READER_RETRY_MS;
|
|
10605
|
+
return snapshot;
|
|
10606
|
+
}
|
|
10545
10607
|
earlierThan(driver, before, look) {
|
|
10546
10608
|
const after = canObserve(driver) ? driver.previous?.() : undefined;
|
|
10547
10609
|
const same = after !== undefined && after.generation === before.generation && after.snapshot === look;
|
|
@@ -18,6 +18,7 @@ type RunSettings = {
|
|
|
18
18
|
passes: number;
|
|
19
19
|
count?: number | undefined;
|
|
20
20
|
profile?: string | undefined;
|
|
21
|
+
runtime?: string | undefined;
|
|
21
22
|
devices?: Array<{
|
|
22
23
|
profile: string;
|
|
23
24
|
default: boolean;
|
|
@@ -141,6 +142,7 @@ export declare const e2eSuiteSchema: z.ZodObject<{
|
|
|
141
142
|
retry: z.ZodDefault<z.ZodNumber>;
|
|
142
143
|
count: z.ZodOptional<z.ZodNumber>;
|
|
143
144
|
profile: z.ZodOptional<z.ZodString>;
|
|
145
|
+
runtime: z.ZodOptional<z.ZodString>;
|
|
144
146
|
app: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
|
|
145
147
|
installMode: z.ZodOptional<z.ZodEnum<{
|
|
146
148
|
clean: "clean";
|
|
@@ -333,6 +333,7 @@ export declare const wardenConfigSchema: z.ZodObject<{
|
|
|
333
333
|
retry: z.ZodDefault<z.ZodNumber>;
|
|
334
334
|
count: z.ZodOptional<z.ZodNumber>;
|
|
335
335
|
profile: z.ZodOptional<z.ZodString>;
|
|
336
|
+
runtime: z.ZodOptional<z.ZodString>;
|
|
336
337
|
app: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
|
|
337
338
|
installMode: z.ZodOptional<z.ZodEnum<{
|
|
338
339
|
clean: "clean";
|
|
@@ -687,6 +687,16 @@ export interface WardenE2eSuite {
|
|
|
687
687
|
count?: number | undefined;
|
|
688
688
|
/** Device profile (not with `devices`). */
|
|
689
689
|
profile?: string | undefined;
|
|
690
|
+
/**
|
|
691
|
+
* iOS simulator runtime the run's devices boot on, and whose golden they clone: `latest`, `iOS-26-5` or `26.5`.
|
|
692
|
+
* Pin it so the run (locally and on CI) sees one accessibility tree: values change between iOS releases.
|
|
693
|
+
* A runtime that is not installed fails the run before any device is claimed, listing the installed ones; it never
|
|
694
|
+
* falls back to another. Applies to every `devices` leg. Skipped on Android. `--runtime` overrides it.
|
|
695
|
+
* `warden golden key --profile <profile> --runtime <runtime>` names the golden archive the run uses.
|
|
696
|
+
*
|
|
697
|
+
* @example "27.0"
|
|
698
|
+
*/
|
|
699
|
+
runtime?: string | undefined;
|
|
690
700
|
/** Ensure the app on every device first. */
|
|
691
701
|
app?: WardenAppOption | undefined;
|
|
692
702
|
/** Ports leased for the run (`<from>[:<span>]`), exported as `WARDEN_PORT_<i>` / `WARDEN_PORTS` to serve, setup and every spec process. */
|
|
@@ -134,6 +134,11 @@ export type Snapshot = {
|
|
|
134
134
|
flat: Element[];
|
|
135
135
|
truncated: boolean;
|
|
136
136
|
modal?: ModalInfo;
|
|
137
|
+
/**
|
|
138
|
+
* Set when `backend` answered because this reader failed or was skipped first (iOS: `axbridge` stalled, `ax` read in
|
|
139
|
+
* its place). The fallback reader can lack what the first one has, so a missing node on such a snapshot is not final.
|
|
140
|
+
*/
|
|
141
|
+
fellBackFrom?: SnapshotBackend;
|
|
137
142
|
};
|
|
138
143
|
export type Orientation = "portrait" | "portraitUpsideDown" | "landscapeLeft" | "landscapeRight";
|
|
139
144
|
export type HardwareButton = "home" | "back" | "lock" | "volumeUp" | "volumeDown" | "shake";
|
|
@@ -25,7 +25,7 @@ import type { SimdSpawn } from "./simd-transport";
|
|
|
25
25
|
* cannot type, `settle` polls snapshot hashes when the helper has no native quiescence, `screenshot` uses
|
|
26
26
|
* `simctl io` when the helper cannot, and a helper built before the app ops leaves app lifecycle to `simctl`.
|
|
27
27
|
*/
|
|
28
|
-
export type IosDriverEnv = Pick<E2eEnv, "simd" | "simdSocket" | "helperDir"
|
|
28
|
+
export type IosDriverEnv = Pick<E2eEnv, "simd" | "simdSocket" | "helperDir"> & Partial<Pick<E2eEnv, "wardenVersion">>;
|
|
29
29
|
/** The iOS driver is a `Driver` plus the optional capabilities of `driver/alerts.ts`. */
|
|
30
30
|
export type IosDriver = Driver & SelectsAll & PastesText & ReadsAlertOn & ProbesFrontmost & ReadsA11y & ReadsA11yEvents & AuditsNatively;
|
|
31
31
|
export type OpenIosDriverOptions = {
|
|
@@ -46,6 +46,8 @@ export type OpenIosDriverOptions = {
|
|
|
46
46
|
client?: SimdClient;
|
|
47
47
|
now?: () => number;
|
|
48
48
|
sleep?: (ms: number) => Promise<void>;
|
|
49
|
+
/** how long the session's first snapshot waits for an in-sim bridge that is up but silent (default 60 s); 0 tries once */
|
|
50
|
+
coldBridgeBudgetMs?: number;
|
|
49
51
|
/** for `locateHelper` / `$WARDEN_HOME/simd/<version>`; defaults read the process */
|
|
50
52
|
version?: string;
|
|
51
53
|
wardenHome?: string;
|
|
@@ -82,6 +84,13 @@ export type OpenIosDriverOptions = {
|
|
|
82
84
|
/** test seams for the XCUITest audit's runner */
|
|
83
85
|
nativeAudit?: Pick<IosNativeAuditOptions, "ensureBuild" | "spawn" | "connect">;
|
|
84
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* How long the first snapshot of a session waits for an in-sim bridge that is up but silent. A freshly cloned
|
|
89
|
+
* simulator on a loaded host can take longer than one read's budget (axbridge 4 s + AX fallback 5 s) to answer the
|
|
90
|
+
* first time, and every read after it is fast, so the first one retries while the failure says "not ready yet".
|
|
91
|
+
* Once a read or a rebind has succeeded the normal per-call budgets apply.
|
|
92
|
+
*/
|
|
93
|
+
export declare const COLD_BRIDGE_BUDGET_MS = 60000;
|
|
85
94
|
/** USB HID keyboard-page usage ids for the keys `Driver.key` offers. */
|
|
86
95
|
export declare const KEY_USAGE: Readonly<Record<KeyName, number>>;
|
|
87
96
|
/** The fields of a failed snapshot's `detail` the helper sets (which reader, why, both readers' errors), on its span. */
|
|
@@ -93,6 +102,8 @@ export declare function createIosDriver(swap: SwappableClient, simctl: Simctl, o
|
|
|
93
102
|
now: () => number;
|
|
94
103
|
sleep: (ms: number) => Promise<void>;
|
|
95
104
|
log?: Logger;
|
|
105
|
+
/** how long the first snapshot waits for a bridge that is not up yet (default {@link COLD_BRIDGE_BUDGET_MS}) */
|
|
106
|
+
coldBridgeBudgetMs?: number;
|
|
96
107
|
/** told about every successful `clock` op */
|
|
97
108
|
clockRecorder?: ClockRecorder;
|
|
98
109
|
/** device settings (`ios-settings.ts`); absent: those ops answer `E_UNSUPPORTED` */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ErrorResult } from "../../types/src/result";
|
|
2
|
-
export type E2eErrorCode = "LOCATOR_NOT_FOUND" | "LOCATOR_AMBIGUOUS" | "INVALID_LOCATOR" | "ACTION_FAILED" | "TIMEOUT" | "NOT_ACTIONABLE" | "INVALID_ARGUMENT" | "SESSION" | "DRIVER" | "ASSERTION" | "PERF_BUDGET";
|
|
2
|
+
export type E2eErrorCode = "LOCATOR_NOT_FOUND" | "LOCATOR_AMBIGUOUS" | "INVALID_LOCATOR" | "ACTION_FAILED" | "TIMEOUT" | "NOT_ACTIONABLE" | "INVALID_ARGUMENT" | "SESSION" | "DRIVER" | "ASSERTION" | "PERF_BUDGET" | "RUN_FAILED";
|
|
3
3
|
/** Plain shape of a spec error: what `e2eErr()` accepts and what a `Result` carries. */
|
|
4
4
|
export type E2eErrorShape = {
|
|
5
5
|
code: E2eErrorCode;
|
package/types/e2e/src/index.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export { expect } from "./expect/expect";
|
|
|
10
10
|
export type { PerfBreach, PerfBudgets } from "./perf/budgets";
|
|
11
11
|
export type { DevicePerf } from "./perf/device-perf";
|
|
12
12
|
export type { PerfMetrics, PerfReport, PerfStartOptions } from "./perf/perf.types";
|
|
13
|
+
export type { Run, RunCommand, RunDuration, RunOptions, RunResult } from "./run";
|
|
14
|
+
export { run } from "./run";
|
|
13
15
|
export type { App, Device, Screen, SystemAlerts, TestFixtures } from "./runner/fixtures.types";
|
|
14
16
|
export { afterAll, afterEach, beforeAll, beforeEach, describe, test } from "./runner/test";
|
|
15
17
|
export type { Locator, LocatorReading, PinchOptions, RotateOptions, TapOptions, TapUntil } from "./screen/locator";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { SpanRunner } from "./runner/span";
|
|
2
|
+
/** Milliseconds, or a duration string (`500ms`, `30s`, `2m`). */
|
|
3
|
+
export type RunDuration = string | number;
|
|
4
|
+
export type RunOptions = {
|
|
5
|
+
/** working directory; default the spec process's (the project root under `warden e2e`) */
|
|
6
|
+
cwd?: string;
|
|
7
|
+
/** merged over the spec's environment; `undefined` removes a variable */
|
|
8
|
+
env?: Record<string, string | undefined>;
|
|
9
|
+
/** kill the command after this long and throw `TIMEOUT`; default `60s` */
|
|
10
|
+
timeout?: RunDuration;
|
|
11
|
+
/** written to the command's stdin */
|
|
12
|
+
input?: string;
|
|
13
|
+
/** throw `RUN_FAILED` on a non-zero exit; default true. `false` returns the result with `ok: false` */
|
|
14
|
+
check?: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type RunResult = {
|
|
17
|
+
/** exit code was 0 */
|
|
18
|
+
ok: boolean;
|
|
19
|
+
exitCode: number;
|
|
20
|
+
stdout: string;
|
|
21
|
+
stderr: string;
|
|
22
|
+
/** wall time of the command */
|
|
23
|
+
ms: number;
|
|
24
|
+
/** `stdout` parsed as JSON; throws `INVALID_ARGUMENT` when it is not JSON */
|
|
25
|
+
json(): unknown;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* A shell command (`string`, run with `sh -c`) or an argv (`string[]`, no shell: nothing is expanded or split).
|
|
29
|
+
*/
|
|
30
|
+
export type RunCommand = string | readonly string[];
|
|
31
|
+
export type Run = (command: RunCommand, options?: RunOptions) => Promise<RunResult>;
|
|
32
|
+
export type RunDeps = {
|
|
33
|
+
/** times the call as a span in the running attempt's trace */
|
|
34
|
+
span: SpanRunner;
|
|
35
|
+
};
|
|
36
|
+
/** `run` with its span runner injected (tests); specs use {@link run}. */
|
|
37
|
+
export declare function createRun(deps: RunDeps): Run;
|
|
38
|
+
/**
|
|
39
|
+
* Runs a command on the host (the machine running the spec, not the device) and returns its exit code and output.
|
|
40
|
+
* Use it to seed a backend, call a script, or reset state before a test. A non-zero exit throws `RUN_FAILED`
|
|
41
|
+
* (`check: false` returns it instead), a command over `timeout` (default 60 s) is killed and throws `TIMEOUT`.
|
|
42
|
+
* Inside a test it is one `run` span in `trace.jsonl` (exit code only, never the command).
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* await run("bun scripts/seed.ts");
|
|
46
|
+
* const { id } = (await run(["bun", "scripts/create-user.ts", "--json"])).json() as { id: string };
|
|
47
|
+
*/
|
|
48
|
+
export declare const run: Run;
|
|
@@ -78,6 +78,8 @@ export declare class Runtime {
|
|
|
78
78
|
private reactCache;
|
|
79
79
|
/** the index of each snapshot, so a snapshot that several reads share is indexed once */
|
|
80
80
|
private readonly indexed;
|
|
81
|
+
/** the time before which a degraded look is not retried on its first reader, after that reader failed to answer */
|
|
82
|
+
private firstReaderRetryAt;
|
|
81
83
|
constructor(host: ScreenHost);
|
|
82
84
|
get clock(): Clock;
|
|
83
85
|
actionTimeout(): number;
|
|
@@ -95,6 +97,14 @@ export declare class Runtime {
|
|
|
95
97
|
* `systemAlerts` on, an alert the screen shows is answered and the screen looked at again.
|
|
96
98
|
*/
|
|
97
99
|
tryObserve(options?: ObserveOptions): Promise<Observation>;
|
|
100
|
+
/**
|
|
101
|
+
* A snapshot the driver answered from a fallback reader (`Snapshot.fellBackFrom`, iOS `ax` after `axbridge` stalled)
|
|
102
|
+
* can lack what the first reader has: React Native testIDs on static text. Such a look is read once more on the
|
|
103
|
+
* first reader, and that read replaces it when it answers. When it does not (the guest is still silent) the
|
|
104
|
+
* fallback snapshot stands and the first reader is left alone for `FIRST_READER_RETRY_MS`, so a dead guest costs a
|
|
105
|
+
* poll one deadline, not one per attempt. Every locator and matcher looks through here, so none needs its own rule.
|
|
106
|
+
*/
|
|
107
|
+
private restoreDegraded;
|
|
98
108
|
/** What came before `look`, given the cache's answer from just before it was taken. */
|
|
99
109
|
private earlierThan;
|
|
100
110
|
/** Whether `snapshot` showed an alert that has now been answered. A screen without one costs no extra read. */
|
|
@@ -123,5 +123,7 @@ export type AttemptBinding = {
|
|
|
123
123
|
export declare function attemptSession(session: Session, binding: () => AttemptBinding): Session;
|
|
124
124
|
/** The process-wide session (`globalThis[Symbol.for("./session")]`), created on first call. */
|
|
125
125
|
export declare function getSession(opts?: SessionOptions): Session;
|
|
126
|
+
/** The process-wide session if one exists; never creates one (so a caller outside a test opens no device). */
|
|
127
|
+
export declare function peekSession(): Session | undefined;
|
|
126
128
|
/** Closes and forgets the process-wide session, if any. */
|
|
127
129
|
export declare function resetSession(): Promise<void>;
|