@delacour/warden-e2e 0.6.0 → 0.8.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-yb1hcjqm.mjs → chunk-vs5npy59.mjs} +3370 -218
- package/index.mjs +247 -16
- package/package.json +2 -2
- package/preload.mjs +8264 -2387
- package/types/core/src/a11y/a11y.config.d.ts +47 -0
- package/types/core/src/affected/affected.schema.d.ts +142 -1
- package/types/core/src/allocate.d.ts +2 -1
- package/types/core/src/android-keyguard.d.ts +36 -0
- package/types/core/src/batch/preset.schema.d.ts +13 -1
- package/types/core/src/biometrics.d.ts +15 -0
- package/types/core/src/builds/app-option.schema.d.ts +1 -1
- package/types/core/src/builds/builds.defaults.d.ts +15 -0
- package/types/core/src/builds/config.d.ts +5 -1
- package/types/core/src/builds/config.schema.d.ts +275 -2
- package/types/core/src/builds/define-config.d.ts +159 -0
- package/types/core/src/builds/install-device.d.ts +37 -0
- package/types/core/src/builds/install.d.ts +17 -3
- package/types/core/src/builds/kinds.d.ts +21 -3
- package/types/core/src/builds/lock.d.ts +38 -0
- package/types/core/src/builds/resolve.d.ts +12 -2
- package/types/core/src/camera-mode.d.ts +17 -0
- package/types/core/src/camera-placeholder.d.ts +9 -0
- package/types/core/src/camera-profile.d.ts +12 -0
- package/types/core/src/claim-errors.d.ts +34 -0
- package/types/core/src/conditions/a11y-keys.d.ts +45 -0
- package/types/core/src/conditions/camera-media.d.ts +30 -0
- package/types/core/src/conditions/conditions.config.d.ts +141 -0
- package/types/core/src/conditions/conditions.d.ts +22 -0
- package/types/core/src/conditions/conditions.types.d.ts +171 -0
- package/types/core/src/conditions/inject-state.d.ts +13 -0
- package/types/core/src/conditions/launch-settings.d.ts +14 -0
- package/types/core/src/conditions/network.presets.d.ts +42 -0
- package/types/core/src/conditions/text-size.d.ts +19 -0
- package/types/core/src/golden/android-biometrics.d.ts +35 -0
- package/types/core/src/ios-runner/build.d.ts +28 -0
- package/types/core/src/ios-runner/cache.d.ts +44 -0
- package/types/core/src/ios-runner/classify.d.ts +7 -0
- package/types/core/src/ios-runner/pack.d.ts +17 -0
- package/types/core/src/ios-runner/runner-build.types.d.ts +76 -0
- package/types/core/src/ios-runner/signing.d.ts +40 -0
- package/types/core/src/ios-runner/sim-build.d.ts +36 -0
- package/types/core/src/ios-runner/stage.d.ts +26 -0
- package/types/core/src/lease-events.d.ts +126 -0
- package/types/core/src/netproxy/control.d.ts +75 -0
- package/types/core/src/netproxy/decide.d.ts +22 -0
- package/types/core/src/netproxy/handle-file.d.ts +12 -0
- package/types/core/src/netproxy/lease-proxy.d.ts +53 -0
- package/types/core/src/netproxy/netproxy.main.d.ts +40 -0
- package/types/core/src/netproxy/pipe.d.ts +37 -0
- package/types/core/src/netproxy/proxy.d.ts +48 -0
- package/types/core/src/netproxy/shaping.d.ts +43 -0
- package/types/core/src/ownership.d.ts +44 -0
- package/types/core/src/providers/android.d.ts +46 -2
- package/types/core/src/providers/provider.types.d.ts +9 -0
- package/types/core/src/queue.d.ts +43 -0
- package/types/core/src/store.d.ts +61 -5
- package/types/core/src/types.d.ts +57 -0
- package/types/core/src/xcode.d.ts +22 -0
- package/types/e2e/src/a11y/a11y-target.d.ts +23 -0
- package/types/e2e/src/a11y/a11y.types.d.ts +81 -0
- package/types/e2e/src/a11y/announcements.d.ts +24 -0
- package/types/e2e/src/a11y/audit.d.ts +22 -0
- package/types/e2e/src/a11y/native/atf.d.ts +51 -0
- package/types/e2e/src/a11y/native/xcuitest.d.ts +60 -0
- package/types/e2e/src/a11y/order.d.ts +5 -0
- package/types/e2e/src/a11y/rules/duplicate-name.d.ts +6 -0
- package/types/e2e/src/a11y/rules/hidden-focusable.d.ts +3 -0
- package/types/e2e/src/a11y/rules/image-no-name.d.ts +6 -0
- package/types/e2e/src/a11y/rules/label-in-name.d.ts +7 -0
- package/types/e2e/src/a11y/rules/missing-name.d.ts +3 -0
- package/types/e2e/src/a11y/rules/role-in-name.d.ts +3 -0
- package/types/e2e/src/a11y/rules/rule.context.d.ts +22 -0
- package/types/e2e/src/a11y/rules/target-size.d.ts +21 -0
- package/types/e2e/src/a11y/rules/text-clipped.d.ts +9 -0
- package/types/e2e/src/a11y/speech.d.ts +26 -0
- package/types/e2e/src/a11y/talkback-log.d.ts +13 -0
- package/types/e2e/src/alert-buttons.d.ts +9 -0
- package/types/e2e/src/app.d.ts +3 -2
- package/types/e2e/src/conditions/condition-events.d.ts +68 -0
- package/types/e2e/src/conditions/device-conditions.d.ts +198 -0
- package/types/e2e/src/conditions/geo.d.ts +15 -0
- package/types/e2e/src/conditions/gpx.d.ts +8 -0
- package/types/e2e/src/conditions/lease-recorder.d.ts +27 -0
- package/types/e2e/src/conditions/lease-store.d.ts +9 -0
- package/types/e2e/src/conditions/location-plan.d.ts +17 -0
- package/types/e2e/src/conditions/location-scenarios.data.d.ts +17 -0
- package/types/e2e/src/conditions/route-ticker.d.ts +53 -0
- package/types/e2e/src/conditions/scheduler.d.ts +83 -0
- package/types/e2e/src/device-a11y.d.ts +97 -0
- package/types/e2e/src/device.d.ts +67 -4
- package/types/e2e/src/driver/a11y-events.d.ts +30 -0
- package/types/e2e/src/driver/a11y-read.d.ts +12 -0
- package/types/e2e/src/driver/android/android-animations.d.ts +7 -0
- package/types/e2e/src/driver/android/android-app.d.ts +4 -0
- package/types/e2e/src/driver/android/android-argv.d.ts +21 -0
- package/types/e2e/src/driver/android/android-camera-host.d.ts +23 -0
- package/types/e2e/src/driver/android/android-camera.d.ts +28 -0
- package/types/e2e/src/driver/android/android-clock.d.ts +28 -0
- package/types/e2e/src/driver/android/android-device.d.ts +8 -0
- package/types/e2e/src/driver/android/android-driver.d.ts +22 -0
- package/types/e2e/src/driver/android/android-network.d.ts +66 -0
- package/types/e2e/src/driver/android/android-phone.d.ts +47 -0
- package/types/e2e/src/driver/android/android-screen-reader.d.ts +23 -0
- package/types/e2e/src/driver/android/android-settings.d.ts +41 -0
- package/types/e2e/src/driver/android/android-spoken.d.ts +18 -0
- package/types/e2e/src/driver/android/android-state.d.ts +10 -0
- package/types/e2e/src/driver/android/android-talkback.d.ts +57 -0
- package/types/e2e/src/driver/android/atf-device.d.ts +23 -0
- package/types/e2e/src/driver/android/atf-dex.generated.d.ts +7 -0
- package/types/e2e/src/driver/android/uiad-dex.generated.d.ts +5 -5
- package/types/e2e/src/driver/android/uiad-ops.d.ts +27 -1
- package/types/e2e/src/driver/android/uiad-protocol.d.ts +8 -0
- package/types/e2e/src/driver/android/uiad-screen-reader.d.ts +33 -0
- package/types/e2e/src/driver/clock-recorder.d.ts +9 -0
- package/types/e2e/src/driver/device-settings.d.ts +10 -0
- package/types/e2e/src/driver/driver.types.d.ts +242 -1
- package/types/e2e/src/driver/element-actions.d.ts +16 -0
- package/types/e2e/src/driver/ios/device/capabilities.d.ts +3 -0
- package/types/e2e/src/driver/ios/device/device-driver.d.ts +49 -0
- package/types/e2e/src/driver/ios/device/devicectl.d.ts +38 -0
- package/types/e2e/src/driver/ios/device/runner-protocol.d.ts +21 -0
- package/types/e2e/src/driver/ios/hid-recovery.d.ts +15 -0
- package/types/e2e/src/driver/ios/inject.d.ts +124 -0
- package/types/e2e/src/driver/ios/ios-a11y.d.ts +73 -0
- package/types/e2e/src/driver/ios/ios-audit.d.ts +36 -0
- package/types/e2e/src/driver/ios/ios-driver.d.ts +50 -1
- package/types/e2e/src/driver/ios/ios-network.d.ts +51 -0
- package/types/e2e/src/driver/ios/ios-settings.d.ts +44 -0
- package/types/e2e/src/driver/ios/sim-runner.d.ts +41 -0
- package/types/e2e/src/driver/ios/simctl.d.ts +41 -1
- package/types/e2e/src/driver/ios/simd-transport.d.ts +11 -2
- package/types/e2e/src/driver/ios/type-budget.d.ts +6 -1
- package/types/e2e/src/driver/native-audit.d.ts +26 -0
- package/types/e2e/src/driver/network-proxy.d.ts +52 -0
- package/types/e2e/src/driver/open.d.ts +20 -0
- package/types/e2e/src/driver/physical-conditions.d.ts +11 -0
- package/types/e2e/src/driver/requirements.d.ts +14 -0
- package/types/e2e/src/driver/screen-reader.d.ts +26 -0
- package/types/e2e/src/driver/typing.d.ts +1 -1
- package/types/e2e/src/env.d.ts +47 -0
- package/types/e2e/src/expect/a11y-matchers.d.ts +33 -0
- package/types/e2e/src/expect/expect.d.ts +7 -2
- package/types/e2e/src/expect/matchers.d.ts +14 -0
- package/types/e2e/src/fixture-session.d.ts +4 -0
- package/types/e2e/src/fixtures.d.ts +6 -1
- package/types/e2e/src/index.d.ts +6 -0
- package/types/e2e/src/runner/callsite.d.ts +3 -1
- package/types/e2e/src/runner/scope.d.ts +1 -0
- package/types/e2e/src/runner/test.d.ts +6 -0
- package/types/e2e/src/screen/render.d.ts +8 -0
- package/types/e2e/src/traced-device.d.ts +2 -1
|
@@ -21,6 +21,20 @@ function e2eErr(error) {
|
|
|
21
21
|
return { success: false, error: E2eError2.from(error) };
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// ../../packages/e2e/src/a11y/a11y-target.ts
|
|
25
|
+
var registry = new WeakMap;
|
|
26
|
+
function registerA11yTarget(target, hooks) {
|
|
27
|
+
registry.set(target, hooks);
|
|
28
|
+
}
|
|
29
|
+
function a11yTargetOf(target) {
|
|
30
|
+
return typeof target === "object" && target !== null ? registry.get(target) : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ../../packages/e2e/src/driver/element-actions.ts
|
|
34
|
+
function canActOnElements(driver) {
|
|
35
|
+
return typeof driver.pressElement === "function";
|
|
36
|
+
}
|
|
37
|
+
|
|
24
38
|
// ../../packages/e2e/src/driver/span-detail.ts
|
|
25
39
|
import { AsyncLocalStorage } from "async_hooks";
|
|
26
40
|
var collectors = new AsyncLocalStorage;
|
|
@@ -1272,6 +1286,24 @@ function head(entry) {
|
|
|
1272
1286
|
function describeEntry(entry) {
|
|
1273
1287
|
return head(entry).join(" ");
|
|
1274
1288
|
}
|
|
1289
|
+
function a11yWords(entry) {
|
|
1290
|
+
const a11y = entry.element.a11y;
|
|
1291
|
+
if (!a11y)
|
|
1292
|
+
return [];
|
|
1293
|
+
const words = [];
|
|
1294
|
+
if (a11y.hint)
|
|
1295
|
+
words.push(`hint=${quote(a11y.hint, MAX_NAME)}`);
|
|
1296
|
+
if (a11y.actions && a11y.actions.length > 0) {
|
|
1297
|
+
words.push(`actions=[${a11y.actions.map((action) => quote(action, MAX_VALUE)).join(",")}]`);
|
|
1298
|
+
}
|
|
1299
|
+
if (a11y.a11yFocused)
|
|
1300
|
+
words.push("a11y-focused");
|
|
1301
|
+
if (a11y.important === false)
|
|
1302
|
+
words.push("not-important");
|
|
1303
|
+
if (a11y.liveRegion)
|
|
1304
|
+
words.push(`live=${a11y.liveRegion}`);
|
|
1305
|
+
return words;
|
|
1306
|
+
}
|
|
1275
1307
|
function renderEntryLine(entry, options = {}) {
|
|
1276
1308
|
const parts = [];
|
|
1277
1309
|
if (options.ref !== undefined)
|
|
@@ -1282,6 +1314,8 @@ function renderEntryLine(entry, options = {}) {
|
|
|
1282
1314
|
parts.push(...stateWords(entry));
|
|
1283
1315
|
const { x, y, width, height } = entry.element.frame;
|
|
1284
1316
|
parts.push(`(${Math.round(x)},${Math.round(y)} ${Math.round(width)}\xD7${Math.round(height)})`);
|
|
1317
|
+
if (options.a11y)
|
|
1318
|
+
parts.push(...a11yWords(entry));
|
|
1285
1319
|
return `${" ".repeat(options.indent ?? 0)}${parts.join(" ")}`;
|
|
1286
1320
|
}
|
|
1287
1321
|
function isListed(entry) {
|
|
@@ -1297,6 +1331,7 @@ function renderSnapshot(snapshot, options) {
|
|
|
1297
1331
|
const visible = listed.slice(0, limit);
|
|
1298
1332
|
const lines = visible.map((entry) => renderEntryLine(entry, {
|
|
1299
1333
|
indent: options.interactiveOnly ? 0 : entry.depth,
|
|
1334
|
+
...options.a11y ? { a11y: true } : {},
|
|
1300
1335
|
...options.refs.has(entry.element.id) ? { ref: options.refs.get(entry.element.id) } : {}
|
|
1301
1336
|
}));
|
|
1302
1337
|
const omitted = listed.length - visible.length;
|
|
@@ -1842,7 +1877,12 @@ class Locator {
|
|
|
1842
1877
|
const spec = { enabled: !withPosition, tapPointOnScreen: !withPosition };
|
|
1843
1878
|
const found = (options.until === undefined ? await this.aimFromKept(spec, options.position) : undefined) ?? await this.actionable(spec, options.timeout ?? this.rt.actionTimeout(), true);
|
|
1844
1879
|
const point = "react" in found ? reactTapPoint(found.react, options.position) : tapPointOf(found.entry, options.position);
|
|
1845
|
-
|
|
1880
|
+
const { driver } = found.observed;
|
|
1881
|
+
if (!withPosition && "entry" in found && canActOnElements(driver)) {
|
|
1882
|
+
if (this.rt.drive("tap", await driver.pressElement(found.entry.element)))
|
|
1883
|
+
return found.observed.snapshot;
|
|
1884
|
+
}
|
|
1885
|
+
this.rt.drive("tap", await driver.tap(point));
|
|
1846
1886
|
return found.observed.snapshot;
|
|
1847
1887
|
}
|
|
1848
1888
|
async arrivedWithin(plan, pressedOn) {
|
|
@@ -1953,6 +1993,10 @@ class Locator {
|
|
|
1953
1993
|
}
|
|
1954
1994
|
return this.act("swipe", async () => {
|
|
1955
1995
|
const { entry, observed } = await this.actionable({ enabled: false, tapPointOnScreen: false }, options.timeout ?? this.rt.actionTimeout());
|
|
1996
|
+
if (canActOnElements(observed.driver)) {
|
|
1997
|
+
if (this.rt.drive("swipe", await observed.driver.scrollElement(options.direction, entry.element)))
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
1956
2000
|
const vector = swipeVector(options.direction, visibleRect(entry.element.frame, observed.snapshot.screen), options.momentum);
|
|
1957
2001
|
this.rt.drive("swipe", await observed.driver.swipe(vector.from, vector.to, vector.options));
|
|
1958
2002
|
});
|
|
@@ -2170,7 +2214,9 @@ async function scrollUntilVisible(rt, target, options = {}) {
|
|
|
2170
2214
|
throw scrollGaveUp(target, entry === undefined, `${swipes} swipes ${direction}`, rt.excerpt(observed.snapshot));
|
|
2171
2215
|
}
|
|
2172
2216
|
const vector = swipeVector(direction, screenRect(observed.snapshot.screen), options.momentum);
|
|
2173
|
-
rt.drive("scrollUntilVisible", await observed.driver.
|
|
2217
|
+
const scrolled = canActOnElements(observed.driver) && rt.drive("scrollUntilVisible", await observed.driver.scrollElement(direction));
|
|
2218
|
+
if (!scrolled)
|
|
2219
|
+
rt.drive("scrollUntilVisible", await observed.driver.swipe(vector.from, vector.to, vector.options));
|
|
2174
2220
|
await observed.driver.settle(SCROLL_SETTLE);
|
|
2175
2221
|
}
|
|
2176
2222
|
});
|
|
@@ -3076,6 +3122,9 @@ function describeDevices(targets) {
|
|
|
3076
3122
|
}
|
|
3077
3123
|
return [...seen.values()].join(", ");
|
|
3078
3124
|
}
|
|
3125
|
+
function sameDeviceName(served, wanted) {
|
|
3126
|
+
return wanted !== undefined && served.replaceAll("+", " ") === wanted.replaceAll("+", " ");
|
|
3127
|
+
}
|
|
3079
3128
|
function normalizeWsUrl(wsUrl, metroUrl) {
|
|
3080
3129
|
const ws = new URL(wsUrl);
|
|
3081
3130
|
const metro = new URL(metroUrl);
|
|
@@ -3094,7 +3143,11 @@ function selectTarget(targets, query, metroUrl) {
|
|
|
3094
3143
|
(t) => t.reactNative?.logicalDeviceId === query.logicalDeviceId || urlDevice(t) === query.logicalDeviceId,
|
|
3095
3144
|
"logicalDeviceId"
|
|
3096
3145
|
],
|
|
3097
|
-
[
|
|
3146
|
+
[
|
|
3147
|
+
query.deviceName,
|
|
3148
|
+
(t) => t.deviceName !== undefined && sameDeviceName(t.deviceName, query.deviceName),
|
|
3149
|
+
"deviceName"
|
|
3150
|
+
]
|
|
3098
3151
|
];
|
|
3099
3152
|
for (const [value, matches, label] of wanted) {
|
|
3100
3153
|
if (!value)
|
|
@@ -4390,7 +4443,9 @@ var DRIVER_CODES = new Set([
|
|
|
4390
4443
|
"E_HID",
|
|
4391
4444
|
"E_IO",
|
|
4392
4445
|
"E_BAD_REQUEST",
|
|
4393
|
-
"E_INTERNAL"
|
|
4446
|
+
"E_INTERNAL",
|
|
4447
|
+
"E_DEVICE_LOCKED",
|
|
4448
|
+
"E_REFUSED"
|
|
4394
4449
|
]);
|
|
4395
4450
|
function isDriverCode(code) {
|
|
4396
4451
|
return DRIVER_CODES.has(code);
|
|
@@ -4515,6 +4570,7 @@ function stdioTransport(opts) {
|
|
|
4515
4570
|
};
|
|
4516
4571
|
}
|
|
4517
4572
|
function socketTransport(opts) {
|
|
4573
|
+
const label = opts.tcp ? `${opts.tcp.hostname}:${opts.tcp.port}` : opts.socketPath ?? "";
|
|
4518
4574
|
return async (handlers) => {
|
|
4519
4575
|
const splitter = createLineSplitter();
|
|
4520
4576
|
const encoder = new TextEncoder;
|
|
@@ -4534,37 +4590,35 @@ function socketTransport(opts) {
|
|
|
4534
4590
|
queued = written >= queued.length ? new Uint8Array(0) : queued.slice(Math.max(written, 0));
|
|
4535
4591
|
};
|
|
4536
4592
|
try {
|
|
4537
|
-
const
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
end() {
|
|
4557
|
-
notifyClosed();
|
|
4558
|
-
}
|
|
4593
|
+
const socketHandlers = {
|
|
4594
|
+
open(s) {
|
|
4595
|
+
socket = s;
|
|
4596
|
+
},
|
|
4597
|
+
data(_s, chunk) {
|
|
4598
|
+
for (const line of splitter.push(chunk))
|
|
4599
|
+
handlers.onLine(line);
|
|
4600
|
+
},
|
|
4601
|
+
drain() {
|
|
4602
|
+
flush();
|
|
4603
|
+
},
|
|
4604
|
+
close() {
|
|
4605
|
+
notifyClosed();
|
|
4606
|
+
},
|
|
4607
|
+
error() {
|
|
4608
|
+
notifyClosed();
|
|
4609
|
+
},
|
|
4610
|
+
end() {
|
|
4611
|
+
notifyClosed();
|
|
4559
4612
|
}
|
|
4560
|
-
}
|
|
4613
|
+
};
|
|
4614
|
+
const connected = opts.tcp ? await Bun.connect({ hostname: opts.tcp.hostname, port: opts.tcp.port, socket: socketHandlers }) : await Bun.connect({ unix: opts.socketPath, socket: socketHandlers });
|
|
4561
4615
|
socket = connected;
|
|
4562
4616
|
} catch (error) {
|
|
4563
4617
|
const reason = error instanceof Error ? error.message : String(error);
|
|
4564
4618
|
return {
|
|
4565
4619
|
success: false,
|
|
4566
|
-
error: driverError("E_HELPER_CRASHED", `cannot connect to warden-simd socket ${
|
|
4567
|
-
detail: { socketPath:
|
|
4620
|
+
error: driverError("E_HELPER_CRASHED", `cannot connect to warden-simd socket ${label}: ${reason}`, {
|
|
4621
|
+
detail: { socketPath: label }
|
|
4568
4622
|
})
|
|
4569
4623
|
};
|
|
4570
4624
|
}
|
|
@@ -4914,6 +4968,22 @@ function isHidLost(error) {
|
|
|
4914
4968
|
const hidInvalidated = /\bE_HID\b/.test(error.message) && /dtuhidd connection[\s\S]*invalidated/.test(error.message);
|
|
4915
4969
|
return hidInvalidated || /\bE_NOT_BOOTED\b/.test(error.message);
|
|
4916
4970
|
}
|
|
4971
|
+
function isRunnerLost(error) {
|
|
4972
|
+
if (!(error instanceof E2eError2) || error.code !== "ACTION_FAILED" && error.code !== "DRIVER")
|
|
4973
|
+
return false;
|
|
4974
|
+
return /\bE_HELPER_CRASHED\b/.test(error.message);
|
|
4975
|
+
}
|
|
4976
|
+
function isRecoverableLoss(error, deviceKind) {
|
|
4977
|
+
return deviceKind === "physical" ? isRunnerLost(error) : isHidLost(error);
|
|
4978
|
+
}
|
|
4979
|
+
var RUNNER_RESTART_MS = 180000;
|
|
4980
|
+
async function restartRunnerHost(deps, argv, udid) {
|
|
4981
|
+
if (argv === undefined || argv.length === 0)
|
|
4982
|
+
return err("WARDEN_IOS_RUNNER_RESTART is not set");
|
|
4983
|
+
const cmd = argv.map((arg) => arg.replaceAll("{udid}", udid));
|
|
4984
|
+
const res = await deps.exec(cmd, { timeoutMs: RUNNER_RESTART_MS });
|
|
4985
|
+
return res.exitCode === 0 ? ok(undefined) : err(execError(cmd, res));
|
|
4986
|
+
}
|
|
4917
4987
|
async function rebootSim(deps, udid) {
|
|
4918
4988
|
const down = ["xcrun", "simctl", "shutdown", udid];
|
|
4919
4989
|
const shutdown = await deps.exec(down, { timeoutMs: SHUTDOWN_MS });
|
|
@@ -5042,8 +5112,328 @@ function traceDriver(driver, options) {
|
|
|
5042
5112
|
return wrap(driver, "driver.");
|
|
5043
5113
|
}
|
|
5044
5114
|
|
|
5045
|
-
// ../../packages/
|
|
5115
|
+
// ../../packages/core/src/a11y/a11y.config.ts
|
|
5046
5116
|
import { z as z7 } from "zod";
|
|
5117
|
+
var A11Y_RULE_IDS = [
|
|
5118
|
+
"missing-name",
|
|
5119
|
+
"image-no-name",
|
|
5120
|
+
"target-size",
|
|
5121
|
+
"target-size-platform",
|
|
5122
|
+
"duplicate-name",
|
|
5123
|
+
"role-in-name",
|
|
5124
|
+
"label-in-name",
|
|
5125
|
+
"hidden-focusable",
|
|
5126
|
+
"text-clipped",
|
|
5127
|
+
"native"
|
|
5128
|
+
];
|
|
5129
|
+
var ruleId = z7.enum(A11Y_RULE_IDS);
|
|
5130
|
+
var a11yAuditConfigSchema = z7.object({
|
|
5131
|
+
ignore: z7.array(z7.object({ rule: ruleId, identifier: z7.string().min(1).optional() }).strict()).optional(),
|
|
5132
|
+
minSeverity: z7.enum(["error", "warning"]).optional(),
|
|
5133
|
+
rules: z7.array(ruleId).min(1).optional()
|
|
5134
|
+
}).strict();
|
|
5135
|
+
function parseA11yAuditEnv(raw) {
|
|
5136
|
+
let json;
|
|
5137
|
+
try {
|
|
5138
|
+
json = JSON.parse(raw);
|
|
5139
|
+
} catch {
|
|
5140
|
+
return err("must be JSON");
|
|
5141
|
+
}
|
|
5142
|
+
const parsed = a11yAuditConfigSchema.safeParse(json);
|
|
5143
|
+
if (parsed.success)
|
|
5144
|
+
return ok(parsed.data);
|
|
5145
|
+
return err(parsed.error.issues.map((i) => `${i.path.join(".") || "a11y"}: ${i.message}`).join("; "));
|
|
5146
|
+
}
|
|
5147
|
+
|
|
5148
|
+
// ../../packages/core/src/conditions/conditions.config.ts
|
|
5149
|
+
import { z as z8 } from "zod";
|
|
5150
|
+
|
|
5151
|
+
// ../../packages/core/src/duration.ts
|
|
5152
|
+
var UNIT_MS = { ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
|
|
5153
|
+
function parseDuration(input) {
|
|
5154
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)?$/.exec(input.trim());
|
|
5155
|
+
if (!match?.[1])
|
|
5156
|
+
return { success: false, error: `invalid duration "${input}" (use e.g. 30s, 10m, 2h)` };
|
|
5157
|
+
return { success: true, data: Math.round(Number(match[1]) * (UNIT_MS[match[2] ?? "s"] ?? 1000)) };
|
|
5158
|
+
}
|
|
5159
|
+
|
|
5160
|
+
// ../../packages/core/src/conditions/conditions.types.ts
|
|
5161
|
+
var PERMISSION_NAMES = [
|
|
5162
|
+
"location",
|
|
5163
|
+
"locationAlways",
|
|
5164
|
+
"camera",
|
|
5165
|
+
"photos",
|
|
5166
|
+
"photosAdd",
|
|
5167
|
+
"contacts",
|
|
5168
|
+
"contactsLimited",
|
|
5169
|
+
"notifications",
|
|
5170
|
+
"microphone",
|
|
5171
|
+
"calendar",
|
|
5172
|
+
"reminders",
|
|
5173
|
+
"motion",
|
|
5174
|
+
"mediaLibrary",
|
|
5175
|
+
"siri"
|
|
5176
|
+
];
|
|
5177
|
+
var TEXT_SIZE_NAMES = [
|
|
5178
|
+
"extra-small",
|
|
5179
|
+
"small",
|
|
5180
|
+
"medium",
|
|
5181
|
+
"large",
|
|
5182
|
+
"extra-large",
|
|
5183
|
+
"extra-extra-large",
|
|
5184
|
+
"extra-extra-extra-large",
|
|
5185
|
+
"accessibility-medium",
|
|
5186
|
+
"accessibility-large",
|
|
5187
|
+
"accessibility-extra-large",
|
|
5188
|
+
"accessibility-extra-extra-large",
|
|
5189
|
+
"accessibility-extra-extra-extra-large"
|
|
5190
|
+
];
|
|
5191
|
+
var CAMERA_FACINGS = ["back", "front"];
|
|
5192
|
+
var CONDITION_AREAS = [
|
|
5193
|
+
"network",
|
|
5194
|
+
"location",
|
|
5195
|
+
"clock",
|
|
5196
|
+
"permission",
|
|
5197
|
+
"locale",
|
|
5198
|
+
"a11y",
|
|
5199
|
+
"battery",
|
|
5200
|
+
"camera"
|
|
5201
|
+
];
|
|
5202
|
+
|
|
5203
|
+
// ../../packages/core/src/conditions/network.presets.ts
|
|
5204
|
+
var NETWORK_PRESET_NAMES = ["edge", "3g", "4g", "lte", "wifi", "slow", "lossy"];
|
|
5205
|
+
var NETWORK_PRESETS = {
|
|
5206
|
+
edge: {
|
|
5207
|
+
emu: { speed: "200:240", delay: "175:225" },
|
|
5208
|
+
shaping: { latencyMs: 400, jitterMs: 25, downKbps: 240, upKbps: 200, lossPct: 0 }
|
|
5209
|
+
},
|
|
5210
|
+
"3g": {
|
|
5211
|
+
emu: { speed: "330:780", delay: "40:60" },
|
|
5212
|
+
shaping: { latencyMs: 100, jitterMs: 10, downKbps: 780, upKbps: 330, lossPct: 0 }
|
|
5213
|
+
},
|
|
5214
|
+
"4g": {
|
|
5215
|
+
emu: { speed: "9000:9000", delay: "70:100" },
|
|
5216
|
+
shaping: { latencyMs: 170, jitterMs: 15, downKbps: 9000, upKbps: 9000, lossPct: 0 }
|
|
5217
|
+
},
|
|
5218
|
+
lte: {
|
|
5219
|
+
emu: { speed: "10000:50000", delay: "20:30" },
|
|
5220
|
+
shaping: { latencyMs: 50, jitterMs: 5, downKbps: 50000, upKbps: 1e4, lossPct: 0 }
|
|
5221
|
+
},
|
|
5222
|
+
wifi: {
|
|
5223
|
+
emu: { speed: "33000:40000", delay: "0:1" },
|
|
5224
|
+
shaping: { latencyMs: 2, jitterMs: 0, downKbps: 40000, upKbps: 33000, lossPct: 0 }
|
|
5225
|
+
},
|
|
5226
|
+
slow: {
|
|
5227
|
+
emu: { speed: "400:400", delay: "900:1100" },
|
|
5228
|
+
shaping: { latencyMs: 2000, jitterMs: 100, downKbps: 400, upKbps: 400, lossPct: 0 }
|
|
5229
|
+
},
|
|
5230
|
+
lossy: {
|
|
5231
|
+
emu: { speed: "1000:1000", delay: "200:300" },
|
|
5232
|
+
shaping: { latencyMs: 500, jitterMs: 50, downKbps: 1000, upKbps: 1000, lossPct: 10 }
|
|
5233
|
+
}
|
|
5234
|
+
};
|
|
5235
|
+
var UNSHAPED = { latencyMs: 0, jitterMs: 0, downKbps: 0, upKbps: 0, lossPct: 0 };
|
|
5236
|
+
var UNSHAPED_EMU = { speed: "full", delay: "none" };
|
|
5237
|
+
var nonNegative = (value) => value === undefined || !Number.isFinite(value) ? 0 : Math.max(0, value);
|
|
5238
|
+
function customEmu(shaping) {
|
|
5239
|
+
const { downKbps, upKbps, latencyMs, jitterMs } = shaping;
|
|
5240
|
+
const speed = downKbps === 0 && upKbps === 0 ? UNSHAPED_EMU.speed : `${upKbps || downKbps}:${downKbps || upKbps}`;
|
|
5241
|
+
const half = latencyMs / 2;
|
|
5242
|
+
const min = Math.max(0, Math.round(half - jitterMs));
|
|
5243
|
+
const max = Math.round(half + jitterMs);
|
|
5244
|
+
const delay = max === 0 ? UNSHAPED_EMU.delay : `${min}:${max}`;
|
|
5245
|
+
return { speed, delay };
|
|
5246
|
+
}
|
|
5247
|
+
function resolveNetwork(condition) {
|
|
5248
|
+
switch (condition.kind) {
|
|
5249
|
+
case "offline":
|
|
5250
|
+
return { online: false };
|
|
5251
|
+
case "online":
|
|
5252
|
+
return { online: true, shaping: UNSHAPED, emu: UNSHAPED_EMU };
|
|
5253
|
+
case "preset":
|
|
5254
|
+
return { online: true, ...NETWORK_PRESETS[condition.preset] };
|
|
5255
|
+
case "flap":
|
|
5256
|
+
return resolveNetwork(condition.base ?? { kind: "online" });
|
|
5257
|
+
case "custom": {
|
|
5258
|
+
const shaping = {
|
|
5259
|
+
latencyMs: nonNegative(condition.latencyMs),
|
|
5260
|
+
jitterMs: nonNegative(condition.jitterMs),
|
|
5261
|
+
downKbps: nonNegative(condition.downKbps),
|
|
5262
|
+
upKbps: nonNegative(condition.upKbps),
|
|
5263
|
+
lossPct: Math.min(100, nonNegative(condition.lossPct))
|
|
5264
|
+
};
|
|
5265
|
+
return { online: true, shaping, emu: customEmu(shaping) };
|
|
5266
|
+
}
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
|
|
5270
|
+
// ../../packages/core/src/conditions/text-size.ts
|
|
5271
|
+
var TEXT_SIZE_SCALE = {
|
|
5272
|
+
"extra-small": 0.82,
|
|
5273
|
+
small: 0.88,
|
|
5274
|
+
medium: 0.94,
|
|
5275
|
+
large: 1,
|
|
5276
|
+
"extra-large": 1.12,
|
|
5277
|
+
"extra-extra-large": 1.24,
|
|
5278
|
+
"extra-extra-extra-large": 1.35,
|
|
5279
|
+
"accessibility-medium": 1.6,
|
|
5280
|
+
"accessibility-large": 1.9,
|
|
5281
|
+
"accessibility-extra-large": 2.35,
|
|
5282
|
+
"accessibility-extra-extra-large": 2.75,
|
|
5283
|
+
"accessibility-extra-extra-extra-large": 3.1
|
|
5284
|
+
};
|
|
5285
|
+
var ANDROID_MAX_FONT_SCALE = 2;
|
|
5286
|
+
var ANDROID_MIN_FONT_SCALE = 0.5;
|
|
5287
|
+
var isName = (v) => TEXT_SIZE_NAMES.includes(v);
|
|
5288
|
+
function resolveTextSize(size) {
|
|
5289
|
+
if (typeof size === "number") {
|
|
5290
|
+
if (!Number.isFinite(size) || size <= 0)
|
|
5291
|
+
return err(`text size must be a positive scale, got ${String(size)}`);
|
|
5292
|
+
let best = TEXT_SIZE_NAMES[0];
|
|
5293
|
+
for (const name of TEXT_SIZE_NAMES) {
|
|
5294
|
+
if (Math.abs(TEXT_SIZE_SCALE[name] - size) < Math.abs(TEXT_SIZE_SCALE[best] - size))
|
|
5295
|
+
best = name;
|
|
5296
|
+
}
|
|
5297
|
+
return ok({ category: best, scale: size });
|
|
5298
|
+
}
|
|
5299
|
+
if (!isName(size))
|
|
5300
|
+
return err(`unknown text size "${String(size)}" (use ${TEXT_SIZE_NAMES.join(", ")} or a scale like 1.3)`);
|
|
5301
|
+
return ok({ category: size, scale: TEXT_SIZE_SCALE[size] });
|
|
5302
|
+
}
|
|
5303
|
+
var androidFontScale = (scale) => Math.min(ANDROID_MAX_FONT_SCALE, Math.max(ANDROID_MIN_FONT_SCALE, Math.round(scale * 100) / 100));
|
|
5304
|
+
function appliedA11y(op, applied) {
|
|
5305
|
+
return Object.fromEntries(Object.entries(op).filter(([key]) => applied.includes(key)));
|
|
5306
|
+
}
|
|
5307
|
+
|
|
5308
|
+
// ../../packages/core/src/conditions/conditions.config.ts
|
|
5309
|
+
var NETWORK_WORDS = ["online", "offline", ...NETWORK_PRESET_NAMES];
|
|
5310
|
+
var nonNegative2 = z8.number().nonnegative().finite();
|
|
5311
|
+
var customNetworkSchema = z8.object({
|
|
5312
|
+
latencyMs: nonNegative2.optional(),
|
|
5313
|
+
jitterMs: nonNegative2.optional(),
|
|
5314
|
+
downKbps: nonNegative2.optional(),
|
|
5315
|
+
upKbps: nonNegative2.optional(),
|
|
5316
|
+
lossPct: z8.number().min(0).max(100).optional()
|
|
5317
|
+
}).strict();
|
|
5318
|
+
var isoDate = z8.string().refine((v) => !Number.isNaN(Date.parse(v)), { message: "invalid date (use ISO 8601)" });
|
|
5319
|
+
var offsetDuration = z8.string().refine((v) => parseDuration(v).success, { message: "invalid duration (use e.g. 2d)" });
|
|
5320
|
+
var textSize = z8.union([z8.enum(TEXT_SIZE_NAMES), z8.number().positive().finite()]);
|
|
5321
|
+
var a11yConfigSchema = z8.object({
|
|
5322
|
+
textSize: textSize.optional(),
|
|
5323
|
+
boldText: z8.boolean().optional(),
|
|
5324
|
+
contrast: z8.enum(["increased", "normal"]).optional(),
|
|
5325
|
+
reduceMotion: z8.boolean().optional()
|
|
5326
|
+
}).strict();
|
|
5327
|
+
var conditionsConfigSchema = z8.object({
|
|
5328
|
+
network: z8.union([z8.enum(NETWORK_WORDS), customNetworkSchema]).optional(),
|
|
5329
|
+
location: z8.union([
|
|
5330
|
+
z8.enum(["city-run", "city-bike", "freeway"]),
|
|
5331
|
+
z8.object({
|
|
5332
|
+
lat: z8.number().min(-90).max(90),
|
|
5333
|
+
lon: z8.number().min(-180).max(180),
|
|
5334
|
+
altM: z8.number().finite().optional()
|
|
5335
|
+
}).strict()
|
|
5336
|
+
]).optional(),
|
|
5337
|
+
clock: z8.union([
|
|
5338
|
+
isoDate,
|
|
5339
|
+
z8.object({ at: isoDate, frozen: z8.boolean().optional() }).strict(),
|
|
5340
|
+
z8.object({ offset: offsetDuration }).strict()
|
|
5341
|
+
]).optional(),
|
|
5342
|
+
permissions: z8.partialRecord(z8.enum(PERMISSION_NAMES), z8.enum(["granted", "denied", "unset"])).optional(),
|
|
5343
|
+
locale: z8.string().min(1).optional(),
|
|
5344
|
+
timezone: z8.string().min(1).optional(),
|
|
5345
|
+
a11y: a11yConfigSchema.optional()
|
|
5346
|
+
}).strict();
|
|
5347
|
+
var cameraFeedInput = z8.union([
|
|
5348
|
+
z8.string().min(1),
|
|
5349
|
+
z8.object({ path: z8.string().min(1), loop: z8.boolean().optional() }).strict()
|
|
5350
|
+
]);
|
|
5351
|
+
var cameraConfigSchema = z8.object({ back: cameraFeedInput.optional(), front: cameraFeedInput.optional() }).strict().refine((v) => v.back !== undefined || v.front !== undefined, { message: "camera needs a back or a front asset" });
|
|
5352
|
+
var cameraFeedsSchema = z8.object({
|
|
5353
|
+
back: z8.object({ path: z8.string().min(1), loop: z8.boolean() }).strict().optional(),
|
|
5354
|
+
front: z8.object({ path: z8.string().min(1), loop: z8.boolean() }).strict().optional()
|
|
5355
|
+
}).strict();
|
|
5356
|
+
function parseCameraFeedsEnv(raw) {
|
|
5357
|
+
let json;
|
|
5358
|
+
try {
|
|
5359
|
+
json = JSON.parse(raw);
|
|
5360
|
+
} catch {
|
|
5361
|
+
return err("must be JSON");
|
|
5362
|
+
}
|
|
5363
|
+
const parsed = cameraFeedsSchema.safeParse(json);
|
|
5364
|
+
return parsed.success ? ok(parsed.data) : err(parsed.error.issues.map((i) => i.message).join("; "));
|
|
5365
|
+
}
|
|
5366
|
+
var isWord = (v) => NETWORK_WORDS.includes(v);
|
|
5367
|
+
function toNetworkCondition(input) {
|
|
5368
|
+
if (typeof input === "string") {
|
|
5369
|
+
if (!isWord(input))
|
|
5370
|
+
return err(`unknown network "${input}" (use ${NETWORK_WORDS.join(", ")})`);
|
|
5371
|
+
if (input === "online" || input === "offline")
|
|
5372
|
+
return ok({ kind: input });
|
|
5373
|
+
return ok({ kind: "preset", preset: input });
|
|
5374
|
+
}
|
|
5375
|
+
if ("kind" in input)
|
|
5376
|
+
return ok(input);
|
|
5377
|
+
return ok({ kind: "custom", ...input });
|
|
5378
|
+
}
|
|
5379
|
+
function toLocation(input) {
|
|
5380
|
+
if (typeof input === "string")
|
|
5381
|
+
return { kind: "scenario", name: input };
|
|
5382
|
+
return { kind: "point", lat: input.lat, lon: input.lon, ...input.altM === undefined ? {} : { altM: input.altM } };
|
|
5383
|
+
}
|
|
5384
|
+
function toClock(input) {
|
|
5385
|
+
if (typeof input === "string")
|
|
5386
|
+
return { kind: "fixed", at: new Date(input).toISOString() };
|
|
5387
|
+
if ("offset" in input) {
|
|
5388
|
+
const ms = parseDuration(input.offset);
|
|
5389
|
+
return { kind: "offset", ms: ms.success ? ms.data : 0 };
|
|
5390
|
+
}
|
|
5391
|
+
const at = new Date(input.at).toISOString();
|
|
5392
|
+
return input.frozen === true ? { kind: "frozen", at } : { kind: "fixed", at };
|
|
5393
|
+
}
|
|
5394
|
+
function normalizeConditions(config) {
|
|
5395
|
+
const conditions = {};
|
|
5396
|
+
if (config.network !== undefined) {
|
|
5397
|
+
const network = toNetworkCondition(config.network);
|
|
5398
|
+
if (!network.success)
|
|
5399
|
+
return network;
|
|
5400
|
+
conditions.network = network.data;
|
|
5401
|
+
}
|
|
5402
|
+
if (config.location !== undefined)
|
|
5403
|
+
conditions.location = toLocation(config.location);
|
|
5404
|
+
if (config.clock !== undefined)
|
|
5405
|
+
conditions.clock = toClock(config.clock);
|
|
5406
|
+
if (config.permissions !== undefined)
|
|
5407
|
+
conditions.permissions = config.permissions;
|
|
5408
|
+
if (config.locale !== undefined)
|
|
5409
|
+
conditions.locale = config.locale;
|
|
5410
|
+
if (config.timezone !== undefined)
|
|
5411
|
+
conditions.timezone = config.timezone;
|
|
5412
|
+
if (config.a11y !== undefined) {
|
|
5413
|
+
const a11y = { ...config.a11y };
|
|
5414
|
+
if (config.a11y.textSize !== undefined) {
|
|
5415
|
+
const size = resolveTextSize(config.a11y.textSize);
|
|
5416
|
+
if (!size.success)
|
|
5417
|
+
return size;
|
|
5418
|
+
a11y.fontScale = size.data.scale;
|
|
5419
|
+
}
|
|
5420
|
+
conditions.a11y = a11y;
|
|
5421
|
+
}
|
|
5422
|
+
return ok({ conditions });
|
|
5423
|
+
}
|
|
5424
|
+
function parseConditionsEnv(raw) {
|
|
5425
|
+
let json;
|
|
5426
|
+
try {
|
|
5427
|
+
json = JSON.parse(raw);
|
|
5428
|
+
} catch {
|
|
5429
|
+
return err("must be JSON");
|
|
5430
|
+
}
|
|
5431
|
+
const parsed = conditionsConfigSchema.safeParse(json);
|
|
5432
|
+
return parsed.success ? ok(parsed.data) : err(parsed.error.issues.map((i) => i.message).join("; "));
|
|
5433
|
+
}
|
|
5434
|
+
|
|
5435
|
+
// ../../packages/e2e/src/env.ts
|
|
5436
|
+
import { z as z9 } from "zod";
|
|
5047
5437
|
var ENV_NAMES = {
|
|
5048
5438
|
udid: "WARDEN_UDID",
|
|
5049
5439
|
deviceName: "WARDEN_DEVICE_NAME",
|
|
@@ -5052,6 +5442,7 @@ var ENV_NAMES = {
|
|
|
5052
5442
|
appPath: "WARDEN_APP_PATH",
|
|
5053
5443
|
metroUrl: "WARDEN_METRO_URL",
|
|
5054
5444
|
launchArgs: "WARDEN_LAUNCH_ARGS",
|
|
5445
|
+
devClientScheme: "WARDEN_DEV_CLIENT_SCHEME",
|
|
5055
5446
|
out: "WARDEN_E2E_OUT",
|
|
5056
5447
|
timeout: "WARDEN_E2E_TIMEOUT",
|
|
5057
5448
|
actionTimeout: "WARDEN_E2E_ACTION_TIMEOUT",
|
|
@@ -5060,57 +5451,98 @@ var ENV_NAMES = {
|
|
|
5060
5451
|
helperDir: "WARDEN_HELPER_DIR",
|
|
5061
5452
|
simd: "WARDEN_SIMD",
|
|
5062
5453
|
simdSocket: "WARDEN_SIMD_SOCKET",
|
|
5454
|
+
iosRunnerSocket: "WARDEN_IOS_RUNNER_SOCKET",
|
|
5063
5455
|
logTimestamps: "WARDEN_LOG_TIMESTAMPS",
|
|
5456
|
+
deviceKind: "WARDEN_DEVICE_KIND",
|
|
5457
|
+
iosRunnerRestart: "WARDEN_IOS_RUNNER_RESTART",
|
|
5064
5458
|
afterReboot: "WARDEN_AFTER_REBOOT",
|
|
5065
5459
|
version: "WARDEN_VERSION",
|
|
5066
5460
|
bin: "WARDEN_BIN",
|
|
5067
5461
|
perf: "WARDEN_PERF",
|
|
5068
|
-
perfBudgets: "WARDEN_PERF_BUDGETS"
|
|
5462
|
+
perfBudgets: "WARDEN_PERF_BUDGETS",
|
|
5463
|
+
conditions: "WARDEN_CONDITIONS",
|
|
5464
|
+
leaseId: "WARDEN_LEASE_ID",
|
|
5465
|
+
netproxyCommand: "WARDEN_NETPROXY_COMMAND",
|
|
5466
|
+
biometrics: "WARDEN_BIOMETRICS",
|
|
5467
|
+
a11yAudit: "WARDEN_A11Y_AUDIT",
|
|
5468
|
+
a11y: "WARDEN_A11Y",
|
|
5469
|
+
cameraFeeds: "WARDEN_CAMERA_FEEDS"
|
|
5069
5470
|
};
|
|
5070
5471
|
var DEFAULT_TIMEOUT_MS4 = 120000;
|
|
5071
5472
|
var DEFAULT_ACTION_TIMEOUT_MS = 30000;
|
|
5072
5473
|
var DEFAULT_ASSERTION_TIMEOUT_MS = 5000;
|
|
5073
|
-
var toggle =
|
|
5074
|
-
var millis =
|
|
5075
|
-
var launchArgs =
|
|
5474
|
+
var toggle = z9.enum(["1", "true", "0", "false"]).transform((v) => v === "1" || v === "true");
|
|
5475
|
+
var millis = z9.string().regex(/^\d+$/, "must be a whole number of milliseconds").transform(Number).pipe(z9.number().int().positive());
|
|
5476
|
+
var launchArgs = z9.string().transform((raw, ctx) => {
|
|
5076
5477
|
try {
|
|
5077
|
-
const parsed =
|
|
5478
|
+
const parsed = z9.array(z9.string()).safeParse(JSON.parse(raw));
|
|
5078
5479
|
if (parsed.success)
|
|
5079
5480
|
return parsed.data;
|
|
5080
5481
|
} catch {}
|
|
5081
5482
|
ctx.addIssue({ code: "custom", message: "must be a JSON array of strings" });
|
|
5082
|
-
return
|
|
5483
|
+
return z9.NEVER;
|
|
5484
|
+
});
|
|
5485
|
+
var conditions = z9.string().transform((raw, ctx) => {
|
|
5486
|
+
const parsed = parseConditionsEnv(raw);
|
|
5487
|
+
if (parsed.success)
|
|
5488
|
+
return parsed.data;
|
|
5489
|
+
ctx.addIssue({ code: "custom", message: parsed.error });
|
|
5490
|
+
return z9.NEVER;
|
|
5491
|
+
});
|
|
5492
|
+
var a11yAudit = z9.string().transform((raw, ctx) => {
|
|
5493
|
+
const parsed = parseA11yAuditEnv(raw);
|
|
5494
|
+
if (parsed.success)
|
|
5495
|
+
return parsed.data;
|
|
5496
|
+
ctx.addIssue({ code: "custom", message: parsed.error });
|
|
5497
|
+
return z9.NEVER;
|
|
5498
|
+
});
|
|
5499
|
+
var cameraFeeds = z9.string().transform((raw, ctx) => {
|
|
5500
|
+
const parsed = parseCameraFeedsEnv(raw);
|
|
5501
|
+
if (parsed.success)
|
|
5502
|
+
return parsed.data;
|
|
5503
|
+
ctx.addIssue({ code: "custom", message: parsed.error });
|
|
5504
|
+
return z9.NEVER;
|
|
5083
5505
|
});
|
|
5084
5506
|
var REQUIRED = "required when WARDEN_UDID is set";
|
|
5085
|
-
var schema =
|
|
5086
|
-
udid:
|
|
5087
|
-
deviceName:
|
|
5088
|
-
platform:
|
|
5089
|
-
bundleId:
|
|
5090
|
-
appPath:
|
|
5091
|
-
metroUrl:
|
|
5507
|
+
var schema = z9.object({
|
|
5508
|
+
udid: z9.string().min(1),
|
|
5509
|
+
deviceName: z9.string().optional(),
|
|
5510
|
+
platform: z9.enum(["ios", "android"]).default("ios"),
|
|
5511
|
+
bundleId: z9.string({ error: REQUIRED }).min(1, REQUIRED),
|
|
5512
|
+
appPath: z9.string().optional(),
|
|
5513
|
+
metroUrl: z9.string().optional(),
|
|
5514
|
+
devClientScheme: z9.string().min(1).optional(),
|
|
5092
5515
|
launchArgs: launchArgs.default([]),
|
|
5093
|
-
out:
|
|
5516
|
+
out: z9.string().optional(),
|
|
5094
5517
|
timeout: millis.default(DEFAULT_TIMEOUT_MS4),
|
|
5095
5518
|
actionTimeout: millis.default(DEFAULT_ACTION_TIMEOUT_MS),
|
|
5096
5519
|
assertionTimeout: millis.default(DEFAULT_ASSERTION_TIMEOUT_MS),
|
|
5097
|
-
specsDir:
|
|
5098
|
-
helperDir:
|
|
5099
|
-
simd:
|
|
5100
|
-
simdSocket:
|
|
5520
|
+
specsDir: z9.string().optional(),
|
|
5521
|
+
helperDir: z9.string().optional(),
|
|
5522
|
+
simd: z9.string().optional(),
|
|
5523
|
+
simdSocket: z9.string().optional(),
|
|
5524
|
+
iosRunnerSocket: z9.string().optional(),
|
|
5101
5525
|
logTimestamps: toggle.default(false),
|
|
5526
|
+
deviceKind: z9.enum(["physical"]).optional(),
|
|
5527
|
+
iosRunnerRestart: launchArgs.optional(),
|
|
5102
5528
|
afterReboot: launchArgs.optional(),
|
|
5103
|
-
version:
|
|
5104
|
-
bin:
|
|
5105
|
-
perf:
|
|
5106
|
-
perfBudgets:
|
|
5529
|
+
version: z9.string().optional(),
|
|
5530
|
+
bin: z9.string().optional(),
|
|
5531
|
+
perf: z9.string().optional(),
|
|
5532
|
+
perfBudgets: z9.string().optional(),
|
|
5533
|
+
conditions: conditions.optional(),
|
|
5534
|
+
leaseId: z9.string().min(1).optional(),
|
|
5535
|
+
netproxyCommand: launchArgs.pipe(z9.array(z9.string()).min(1)).optional(),
|
|
5536
|
+
biometrics: toggle.optional(),
|
|
5537
|
+
a11yAudit: a11yAudit.optional(),
|
|
5538
|
+
cameraFeeds: cameraFeeds.optional()
|
|
5107
5539
|
});
|
|
5108
5540
|
function defined2(values) {
|
|
5109
5541
|
return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
|
|
5110
5542
|
}
|
|
5111
5543
|
function parseArgv(raw) {
|
|
5112
5544
|
try {
|
|
5113
|
-
const parsed =
|
|
5545
|
+
const parsed = z9.array(z9.string()).min(1).safeParse(JSON.parse(raw));
|
|
5114
5546
|
if (parsed.success)
|
|
5115
5547
|
return parsed.data;
|
|
5116
5548
|
} catch {}
|
|
@@ -5131,13 +5563,23 @@ function toEnv(v, now) {
|
|
|
5131
5563
|
deviceName: v.deviceName,
|
|
5132
5564
|
appPath: v.appPath,
|
|
5133
5565
|
metroUrl: v.metroUrl,
|
|
5566
|
+
devClientScheme: v.devClientScheme,
|
|
5134
5567
|
specsDir: v.specsDir,
|
|
5135
5568
|
helperDir: v.helperDir,
|
|
5136
5569
|
simd: v.simd,
|
|
5137
5570
|
simdSocket: v.simdSocket,
|
|
5571
|
+
iosRunnerSocket: v.iosRunnerSocket,
|
|
5138
5572
|
wardenVersion: v.version,
|
|
5139
5573
|
perfBudgets: v.perfBudgets
|
|
5140
5574
|
}),
|
|
5575
|
+
...v.deviceKind === undefined ? {} : { deviceKind: v.deviceKind },
|
|
5576
|
+
...v.iosRunnerRestart === undefined ? {} : { iosRunnerRestart: v.iosRunnerRestart },
|
|
5577
|
+
...v.conditions === undefined ? {} : { conditions: v.conditions },
|
|
5578
|
+
...v.leaseId === undefined ? {} : { leaseId: v.leaseId },
|
|
5579
|
+
...v.netproxyCommand === undefined ? {} : { netproxyCommand: v.netproxyCommand },
|
|
5580
|
+
...v.biometrics === true ? { biometrics: true } : {},
|
|
5581
|
+
...v.a11yAudit === undefined ? {} : { a11yAudit: v.a11yAudit },
|
|
5582
|
+
...v.cameraFeeds === undefined ? {} : { cameraFeeds: v.cameraFeeds },
|
|
5141
5583
|
...v.afterReboot === undefined ? {} : { afterReboot: v.afterReboot },
|
|
5142
5584
|
...v.bin === undefined ? {} : { wardenBin: parseArgv(v.bin) },
|
|
5143
5585
|
...v.perf === undefined ? {} : {
|
|
@@ -5170,25 +5612,25 @@ function readEnv(env, opts = {}) {
|
|
|
5170
5612
|
// ../../packages/e2e/src/runner/results.ts
|
|
5171
5613
|
import { appendFileSync, mkdirSync } from "fs";
|
|
5172
5614
|
import { dirname as dirname2 } from "path";
|
|
5173
|
-
import { z as
|
|
5174
|
-
var testStatusSchema =
|
|
5175
|
-
var testResultSchema =
|
|
5176
|
-
file:
|
|
5177
|
-
title:
|
|
5615
|
+
import { z as z10 } from "zod";
|
|
5616
|
+
var testStatusSchema = z10.enum(["passed", "failed", "skipped", "timed-out"]);
|
|
5617
|
+
var testResultSchema = z10.object({
|
|
5618
|
+
file: z10.string(),
|
|
5619
|
+
title: z10.array(z10.string()),
|
|
5178
5620
|
status: testStatusSchema,
|
|
5179
|
-
durationMs:
|
|
5180
|
-
startedAt:
|
|
5181
|
-
only:
|
|
5182
|
-
error:
|
|
5183
|
-
message:
|
|
5184
|
-
code:
|
|
5185
|
-
stack:
|
|
5621
|
+
durationMs: z10.number(),
|
|
5622
|
+
startedAt: z10.number(),
|
|
5623
|
+
only: z10.boolean().optional(),
|
|
5624
|
+
error: z10.object({
|
|
5625
|
+
message: z10.string(),
|
|
5626
|
+
code: z10.string().optional(),
|
|
5627
|
+
stack: z10.string().optional()
|
|
5186
5628
|
}).optional(),
|
|
5187
|
-
artifacts:
|
|
5188
|
-
device:
|
|
5189
|
-
udid:
|
|
5190
|
-
platform:
|
|
5191
|
-
name:
|
|
5629
|
+
artifacts: z10.object({ dir: z10.string(), files: z10.array(z10.string()) }).optional(),
|
|
5630
|
+
device: z10.object({
|
|
5631
|
+
udid: z10.string(),
|
|
5632
|
+
platform: z10.enum(["ios", "android"]),
|
|
5633
|
+
name: z10.string().optional()
|
|
5192
5634
|
})
|
|
5193
5635
|
});
|
|
5194
5636
|
function openResults(path) {
|
|
@@ -5260,6 +5702,8 @@ async function runStep(attempt, kind, label, fn, now = Date.now) {
|
|
|
5260
5702
|
}
|
|
5261
5703
|
|
|
5262
5704
|
// ../../packages/e2e/src/session.ts
|
|
5705
|
+
var ENROLL_TRIES = 5;
|
|
5706
|
+
var ENROLL_RETRY_MS = 500;
|
|
5263
5707
|
function devtoolsTarget(env) {
|
|
5264
5708
|
return { appId: env.bundleId, ...env.deviceName === undefined ? {} : { deviceName: env.deviceName } };
|
|
5265
5709
|
}
|
|
@@ -5366,10 +5810,27 @@ function createSession(opts = {}) {
|
|
|
5366
5810
|
await sleep(WARM_INTERVAL_MS);
|
|
5367
5811
|
}
|
|
5368
5812
|
}
|
|
5813
|
+
async function recoverRunner(deps, name) {
|
|
5814
|
+
if (hidRecovered) {
|
|
5815
|
+
note(`iPhone ${name}: the runner connection was lost again after a restart; check the phone and retry`);
|
|
5816
|
+
return false;
|
|
5817
|
+
}
|
|
5818
|
+
hidRecovered = true;
|
|
5819
|
+
await resetDevice();
|
|
5820
|
+
const restarted = await restartRunnerHost(deps, resolved.iosRunnerRestart, resolved.udid);
|
|
5821
|
+
if (!restarted.success) {
|
|
5822
|
+
note(`iPhone ${name}: runner connection lost, restart failed: ${restarted.error}`);
|
|
5823
|
+
return false;
|
|
5824
|
+
}
|
|
5825
|
+
note(`iPhone ${name}: runner connection lost, runner host restarted; running the test again`);
|
|
5826
|
+
return true;
|
|
5827
|
+
}
|
|
5369
5828
|
async function recoverHid() {
|
|
5370
5829
|
if (hid === undefined || closing !== undefined)
|
|
5371
5830
|
return false;
|
|
5372
5831
|
const name = resolved.deviceName ?? resolved.udid;
|
|
5832
|
+
if (resolved.deviceKind === "physical")
|
|
5833
|
+
return recoverRunner(hid, name);
|
|
5373
5834
|
if (hidRecovered) {
|
|
5374
5835
|
note(`sim ${name}: HID connection lost again after a reboot; \`warden release <id> --shutdown\` and retry`);
|
|
5375
5836
|
return false;
|
|
@@ -5391,6 +5852,24 @@ function createSession(opts = {}) {
|
|
|
5391
5852
|
note(`sim ${name}: HID connection lost (Device Hub quit?), rebooted; running the test again`);
|
|
5392
5853
|
return true;
|
|
5393
5854
|
}
|
|
5855
|
+
async function reenroll(driver) {
|
|
5856
|
+
if (resolved.biometrics !== true || resolved.platform !== "ios" || resolved.deviceKind === "physical")
|
|
5857
|
+
return driver;
|
|
5858
|
+
const sleep = hid?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
5859
|
+
for (let attempt = 1;attempt <= ENROLL_TRIES; attempt += 1) {
|
|
5860
|
+
const state = await driver.biometrics("enroll");
|
|
5861
|
+
if (!state.success) {
|
|
5862
|
+
note(`biometrics enroll failed: ${state.error.code}: ${state.error.message}`);
|
|
5863
|
+
return driver;
|
|
5864
|
+
}
|
|
5865
|
+
if (state.data.enrolled)
|
|
5866
|
+
return driver;
|
|
5867
|
+
if (attempt < ENROLL_TRIES)
|
|
5868
|
+
await sleep(ENROLL_RETRY_MS);
|
|
5869
|
+
}
|
|
5870
|
+
note(`biometrics enroll did not take after ${ENROLL_TRIES} attempts`);
|
|
5871
|
+
return driver;
|
|
5872
|
+
}
|
|
5394
5873
|
const session = {
|
|
5395
5874
|
env: resolved,
|
|
5396
5875
|
out,
|
|
@@ -5423,7 +5902,7 @@ function createSession(opts = {}) {
|
|
|
5423
5902
|
throw new E2eError2("DRIVER", `${code}: ${message}`);
|
|
5424
5903
|
}
|
|
5425
5904
|
current = cacheObservations(traceDriver(result.data, { trace: () => attempt?.trace }));
|
|
5426
|
-
return current;
|
|
5905
|
+
return reenroll(current);
|
|
5427
5906
|
}, (cause) => {
|
|
5428
5907
|
opened = undefined;
|
|
5429
5908
|
throw new E2eError2("DRIVER", cause instanceof Error ? cause.message : String(cause));
|
|
@@ -5579,133 +6058,2823 @@ function getSession(opts = {}) {
|
|
|
5579
6058
|
return session;
|
|
5580
6059
|
}
|
|
5581
6060
|
|
|
5582
|
-
// ../../packages/e2e/src/
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
6061
|
+
// ../../packages/e2e/src/a11y/speech.ts
|
|
6062
|
+
var ACTIONABLE_ROLES = new Set([
|
|
6063
|
+
"button",
|
|
6064
|
+
"switch",
|
|
6065
|
+
"checkbox",
|
|
6066
|
+
"slider",
|
|
6067
|
+
"link",
|
|
6068
|
+
"tab",
|
|
6069
|
+
"textfield",
|
|
6070
|
+
"securefield",
|
|
6071
|
+
"searchfield"
|
|
6072
|
+
]);
|
|
6073
|
+
var INPUT_ROLES = new Set(["textfield", "securefield", "searchfield"]);
|
|
6074
|
+
var ON_VALUES = new Set(["1", "on", "true", "checked"]);
|
|
6075
|
+
var OFF_VALUES = new Set(["0", "off", "false", "unchecked"]);
|
|
6076
|
+
function clean(s) {
|
|
6077
|
+
const t = s?.trim();
|
|
6078
|
+
return t ? t : undefined;
|
|
6079
|
+
}
|
|
6080
|
+
function has(el, trait) {
|
|
6081
|
+
return el.traits.includes(trait);
|
|
6082
|
+
}
|
|
6083
|
+
function isActionable(el, platform) {
|
|
6084
|
+
if (ACTIONABLE_ROLES.has(el.role))
|
|
6085
|
+
return true;
|
|
6086
|
+
if (platform === "ios")
|
|
6087
|
+
return has(el, "button") || has(el, "link") || has(el, "adjustable");
|
|
6088
|
+
return has(el, "clickable") || has(el, "long-clickable") || has(el, "checkable");
|
|
6089
|
+
}
|
|
6090
|
+
function isFocusTarget(el, platform) {
|
|
6091
|
+
return el.a11y?.focusable === true || isActionable(el, platform);
|
|
6092
|
+
}
|
|
6093
|
+
function isInput(el) {
|
|
6094
|
+
return INPUT_ROLES.has(el.role);
|
|
6095
|
+
}
|
|
6096
|
+
function ownName(el, platform) {
|
|
6097
|
+
const label = clean(el.label);
|
|
6098
|
+
if (label)
|
|
6099
|
+
return label;
|
|
6100
|
+
const text = clean(el.text);
|
|
6101
|
+
if (text)
|
|
6102
|
+
return text;
|
|
6103
|
+
if (isInput(el)) {
|
|
6104
|
+
return platform === "android" ? clean(el.value) ?? clean(el.placeholder) : undefined;
|
|
6105
|
+
}
|
|
6106
|
+
if (el.role === "switch" || el.role === "checkbox")
|
|
6107
|
+
return;
|
|
6108
|
+
return platform === "android" ? clean(el.value) : undefined;
|
|
5604
6109
|
}
|
|
5605
|
-
function
|
|
5606
|
-
const
|
|
5607
|
-
|
|
5608
|
-
for (const metric of BUDGET_KEYS) {
|
|
5609
|
-
const budget = budgets[metric];
|
|
5610
|
-
if (budget === undefined)
|
|
6110
|
+
function descendantTexts(el, platform, out) {
|
|
6111
|
+
for (const child of el.children) {
|
|
6112
|
+
if (child.hidden)
|
|
5611
6113
|
continue;
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
6114
|
+
if (child.a11y?.important === false && child.children.length === 0)
|
|
6115
|
+
continue;
|
|
6116
|
+
const name = clean(child.label) ?? clean(child.text) ?? (child.role === "text" ? clean(child.value) : undefined);
|
|
6117
|
+
if (name)
|
|
6118
|
+
out.push(name);
|
|
6119
|
+
else
|
|
6120
|
+
descendantTexts(child, platform, out);
|
|
5615
6121
|
}
|
|
5616
|
-
return breaches;
|
|
5617
6122
|
}
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
const lines = breaches.map((b) => ` ${b.metric}: ${round(b.value)} > budget ${b.budget}`);
|
|
5621
|
-
return [
|
|
5622
|
-
`perf_budget: ${breaches.length} metric${breaches.length === 1 ? "" : "s"} over budget`,
|
|
5623
|
-
...lines,
|
|
5624
|
-
...reportPath === undefined ? [] : [` report: ${reportPath}`]
|
|
5625
|
-
].join(`
|
|
5626
|
-
`);
|
|
6123
|
+
function hasActionableDescendant(el, platform) {
|
|
6124
|
+
return el.children.some((c) => !c.hidden && (isActionable(c, platform) || hasActionableDescendant(c, platform)));
|
|
5627
6125
|
}
|
|
5628
|
-
function
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
return
|
|
6126
|
+
function accessibleName(el, platform) {
|
|
6127
|
+
const own = ownName(el, platform);
|
|
6128
|
+
if (own)
|
|
6129
|
+
return own;
|
|
6130
|
+
if (!isFocusTarget(el, platform) && el.role !== "text")
|
|
6131
|
+
return;
|
|
6132
|
+
if (el.children.length === 0 || hasActionableDescendant(el, platform))
|
|
6133
|
+
return;
|
|
6134
|
+
const parts = [];
|
|
6135
|
+
descendantTexts(el, platform, parts);
|
|
6136
|
+
return parts.length ? parts.join(platform === "ios" ? " " : ", ") : undefined;
|
|
6137
|
+
}
|
|
6138
|
+
function switchState(el) {
|
|
6139
|
+
const v = clean(el.value)?.toLowerCase();
|
|
6140
|
+
if (v && ON_VALUES.has(v))
|
|
6141
|
+
return "on";
|
|
6142
|
+
if (v && OFF_VALUES.has(v))
|
|
6143
|
+
return "off";
|
|
6144
|
+
if (el.checked !== undefined)
|
|
6145
|
+
return el.checked ? "on" : "off";
|
|
6146
|
+
if (has(el, "checked"))
|
|
6147
|
+
return "on";
|
|
6148
|
+
return;
|
|
6149
|
+
}
|
|
6150
|
+
function iosRoleWord(el) {
|
|
6151
|
+
if (has(el, "header"))
|
|
6152
|
+
return "heading";
|
|
6153
|
+
if (has(el, "adjustable") || el.role === "slider")
|
|
6154
|
+
return "adjustable";
|
|
6155
|
+
if (has(el, "link") || el.role === "link")
|
|
6156
|
+
return "link";
|
|
6157
|
+
switch (el.role) {
|
|
6158
|
+
case "switch":
|
|
6159
|
+
return "switch button";
|
|
6160
|
+
case "checkbox":
|
|
6161
|
+
return "checkbox";
|
|
6162
|
+
case "tab":
|
|
6163
|
+
return "tab";
|
|
6164
|
+
case "textfield":
|
|
6165
|
+
return "text field";
|
|
6166
|
+
case "securefield":
|
|
6167
|
+
return "secure text field";
|
|
6168
|
+
case "searchfield":
|
|
6169
|
+
return "search field";
|
|
6170
|
+
case "image":
|
|
6171
|
+
return "image";
|
|
6172
|
+
case "button":
|
|
6173
|
+
return "button";
|
|
6174
|
+
default:
|
|
5636
6175
|
}
|
|
5637
|
-
if (
|
|
5638
|
-
return
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
6176
|
+
if (has(el, "button"))
|
|
6177
|
+
return "button";
|
|
6178
|
+
if (has(el, "image"))
|
|
6179
|
+
return "image";
|
|
6180
|
+
return;
|
|
6181
|
+
}
|
|
6182
|
+
function androidRoleWord(el) {
|
|
6183
|
+
if (has(el, "heading") || has(el, "header"))
|
|
6184
|
+
return "heading";
|
|
6185
|
+
if (has(el, "link") || el.role === "link")
|
|
6186
|
+
return "link";
|
|
6187
|
+
switch (el.role) {
|
|
6188
|
+
case "switch":
|
|
6189
|
+
return "switch";
|
|
6190
|
+
case "checkbox":
|
|
6191
|
+
return "checkbox";
|
|
6192
|
+
case "textfield":
|
|
6193
|
+
case "securefield":
|
|
6194
|
+
case "searchfield":
|
|
6195
|
+
return "edit box";
|
|
6196
|
+
case "image":
|
|
6197
|
+
return "image";
|
|
6198
|
+
case "button":
|
|
6199
|
+
return "button";
|
|
6200
|
+
case "tab":
|
|
6201
|
+
return "tab";
|
|
6202
|
+
case "slider":
|
|
6203
|
+
return "seek control";
|
|
6204
|
+
default:
|
|
5644
6205
|
}
|
|
5645
|
-
|
|
6206
|
+
if (has(el, "checkable"))
|
|
6207
|
+
return "checkbox";
|
|
6208
|
+
return;
|
|
6209
|
+
}
|
|
6210
|
+
function expandedState(el) {
|
|
6211
|
+
if (has(el, "expanded"))
|
|
6212
|
+
return "expanded";
|
|
6213
|
+
if (has(el, "collapsed"))
|
|
6214
|
+
return "collapsed";
|
|
6215
|
+
return;
|
|
6216
|
+
}
|
|
6217
|
+
function join2(parts) {
|
|
6218
|
+
return parts.filter((p) => !!p).join(", ");
|
|
6219
|
+
}
|
|
6220
|
+
function announceIos(el, opts) {
|
|
6221
|
+
const label = accessibleName(el, "ios");
|
|
6222
|
+
const toggle = el.role === "switch" || el.role === "checkbox" ? switchState(el) : undefined;
|
|
6223
|
+
const rawValue = clean(el.value);
|
|
6224
|
+
let value;
|
|
6225
|
+
if (el.role === "securefield")
|
|
6226
|
+
value = undefined;
|
|
6227
|
+
else if (isInput(el))
|
|
6228
|
+
value = rawValue ?? clean(el.placeholder);
|
|
6229
|
+
else if (!toggle && el.role !== "text" && rawValue && rawValue !== label)
|
|
6230
|
+
value = rawValue;
|
|
6231
|
+
const role = iosRoleWord(el);
|
|
6232
|
+
const state = [];
|
|
6233
|
+
if (toggle)
|
|
6234
|
+
state.push(toggle);
|
|
6235
|
+
if (el.selected || has(el, "selected"))
|
|
6236
|
+
state.push("selected");
|
|
6237
|
+
const expanded = expandedState(el);
|
|
6238
|
+
if (expanded)
|
|
6239
|
+
state.push(expanded);
|
|
6240
|
+
if (!el.enabled || has(el, "not-enabled"))
|
|
6241
|
+
state.push("dimmed");
|
|
6242
|
+
const hint = opts.hints === false ? undefined : clean(el.a11y?.hint);
|
|
6243
|
+
return build(el, "ios", { label, value, role, state, hint }, join2([label, value, role, ...state, hint]));
|
|
6244
|
+
}
|
|
6245
|
+
function announceAndroid(el, opts) {
|
|
6246
|
+
const name = accessibleName(el, "android");
|
|
6247
|
+
const state = [];
|
|
6248
|
+
const toggle = switchState(el);
|
|
6249
|
+
if (el.role === "switch") {
|
|
6250
|
+
if (toggle)
|
|
6251
|
+
state.push(toggle);
|
|
6252
|
+
} else if (el.role === "checkbox" || has(el, "checkable")) {
|
|
6253
|
+
const checked = el.checked ?? has(el, "checked");
|
|
6254
|
+
state.push(checked ? "checked" : "not checked");
|
|
6255
|
+
}
|
|
6256
|
+
if (el.selected || has(el, "selected"))
|
|
6257
|
+
state.push("selected");
|
|
6258
|
+
const expanded = expandedState(el);
|
|
6259
|
+
if (expanded)
|
|
6260
|
+
state.push(expanded);
|
|
6261
|
+
const role = androidRoleWord(el);
|
|
6262
|
+
const disabled = !el.enabled;
|
|
6263
|
+
const hint = opts.hints === false ? undefined : clean(el.a11y?.hint);
|
|
6264
|
+
const text = join2([...state, name, role, disabled ? "disabled" : undefined, hint]);
|
|
6265
|
+
return build(el, "android", { label: name, role, state: disabled ? [...state, "disabled"] : state, hint }, text);
|
|
6266
|
+
}
|
|
6267
|
+
function build(el, platform, parts, text) {
|
|
6268
|
+
const { label, value, role, state, hint } = parts;
|
|
6269
|
+
return {
|
|
6270
|
+
id: el.id,
|
|
6271
|
+
...el.identifier ? { identifier: el.identifier } : {},
|
|
6272
|
+
text,
|
|
6273
|
+
role: el.role,
|
|
6274
|
+
platform,
|
|
6275
|
+
source: "simulated",
|
|
6276
|
+
parts: {
|
|
6277
|
+
...label ? { label } : {},
|
|
6278
|
+
...value ? { value } : {},
|
|
6279
|
+
...role ? { role } : {},
|
|
6280
|
+
state,
|
|
6281
|
+
...hint ? { hint } : {}
|
|
6282
|
+
}
|
|
6283
|
+
};
|
|
6284
|
+
}
|
|
6285
|
+
function announce(el, platform, opts = {}) {
|
|
6286
|
+
return platform === "ios" ? announceIos(el, opts) : announceAndroid(el, opts);
|
|
5646
6287
|
}
|
|
5647
6288
|
|
|
5648
|
-
// ../../packages/e2e/src/
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
6289
|
+
// ../../packages/e2e/src/a11y/order.ts
|
|
6290
|
+
function onScreen(el, screen) {
|
|
6291
|
+
const f = el.frame;
|
|
6292
|
+
if (f.width <= 0 || f.height <= 0)
|
|
6293
|
+
return false;
|
|
6294
|
+
return f.x < screen.width && f.y < screen.height && f.x + f.width > 0 && f.y + f.height > 0;
|
|
6295
|
+
}
|
|
6296
|
+
function verticalOverlap(a, b) {
|
|
6297
|
+
const top = Math.max(a.frame.y, b.frame.y);
|
|
6298
|
+
const bottom = Math.min(a.frame.y + a.frame.height, b.frame.y + b.frame.height);
|
|
6299
|
+
return bottom - top;
|
|
6300
|
+
}
|
|
6301
|
+
function rowMajor(items) {
|
|
6302
|
+
const byTop = [...items].sort((a, b) => a.frame.y - b.frame.y || a.frame.x - b.frame.x);
|
|
6303
|
+
const rows = [];
|
|
6304
|
+
for (const el of byTop) {
|
|
6305
|
+
const row = rows.find((r) => {
|
|
6306
|
+
const anchor = r[0];
|
|
6307
|
+
return anchor !== undefined && verticalOverlap(anchor, el) > 0.5 * Math.min(anchor.frame.height, el.frame.height);
|
|
6308
|
+
});
|
|
6309
|
+
if (row)
|
|
6310
|
+
row.push(el);
|
|
6311
|
+
else
|
|
6312
|
+
rows.push([el]);
|
|
6313
|
+
}
|
|
6314
|
+
return rows.flatMap((r) => r.sort((a, b) => a.frame.x - b.frame.x));
|
|
6315
|
+
}
|
|
6316
|
+
function orderSiblings(children, platform) {
|
|
6317
|
+
if (platform === "android")
|
|
6318
|
+
return children;
|
|
6319
|
+
const explicit = children.filter((c) => c.a11y?.order !== undefined);
|
|
6320
|
+
if (explicit.length > 0) {
|
|
6321
|
+
const rest = children.filter((c) => c.a11y?.order === undefined);
|
|
6322
|
+
return [...explicit.sort((a, b) => (a.a11y?.order ?? 0) - (b.a11y?.order ?? 0)), ...rowMajor(rest)];
|
|
6323
|
+
}
|
|
6324
|
+
return rowMajor(children);
|
|
6325
|
+
}
|
|
6326
|
+
function hasActionableDescendant2(el, platform) {
|
|
6327
|
+
return el.children.some((c) => !c.hidden && (isActionable(c, platform) || hasActionableDescendant2(c, platform)));
|
|
6328
|
+
}
|
|
6329
|
+
function hasSpokenDescendant(el, platform) {
|
|
6330
|
+
return el.children.some((c) => !c.hidden && (isActionable(c, platform) || accessibleName(c, platform) !== undefined || hasSpokenDescendant(c, platform)));
|
|
6331
|
+
}
|
|
6332
|
+
var CONTAINERS = new Set([
|
|
6333
|
+
"application",
|
|
6334
|
+
"window",
|
|
6335
|
+
"toolbar",
|
|
6336
|
+
"navbar",
|
|
6337
|
+
"tabbar",
|
|
6338
|
+
"list",
|
|
6339
|
+
"scrollview"
|
|
6340
|
+
]);
|
|
6341
|
+
function swallows(el, platform, actionable) {
|
|
6342
|
+
if (actionable)
|
|
6343
|
+
return !hasActionableDescendant2(el, platform);
|
|
6344
|
+
return el.a11y?.focusable === true || !hasSpokenDescendant(el, platform);
|
|
6345
|
+
}
|
|
6346
|
+
function walk(el, platform, screen, opts, out) {
|
|
6347
|
+
if (el.hidden)
|
|
5658
6348
|
return;
|
|
6349
|
+
const visible = el.a11y?.important !== false && onScreen(el, screen);
|
|
6350
|
+
if (visible) {
|
|
6351
|
+
const actionable = isActionable(el, platform);
|
|
6352
|
+
const container = CONTAINERS.has(el.role) && !actionable;
|
|
6353
|
+
const named = accessibleName(el, platform) !== undefined;
|
|
6354
|
+
if (!container && (actionable || named)) {
|
|
6355
|
+
if (swallows(el, platform, actionable)) {
|
|
6356
|
+
out.push({ el, announcement: announce(el, platform, opts) });
|
|
6357
|
+
return;
|
|
6358
|
+
}
|
|
6359
|
+
if (named)
|
|
6360
|
+
out.push({ el, announcement: announce(el, platform, opts) });
|
|
6361
|
+
}
|
|
5659
6362
|
}
|
|
6363
|
+
for (const child of orderSiblings(el.children, platform))
|
|
6364
|
+
walk(child, platform, screen, opts, out);
|
|
5660
6365
|
}
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
...o.frames ? ["--frames"] : [],
|
|
5667
|
-
...o.logs ? ["--logs"] : [],
|
|
5668
|
-
...o.all ? ["--all"] : [],
|
|
5669
|
-
...o.relaunch ? ["--relaunch"] : [],
|
|
5670
|
-
...o.reset ? ["--reset"] : [],
|
|
5671
|
-
...o.maxDurationMs === undefined ? [] : ["--max-duration", `${Math.ceil(o.maxDurationMs / 1000)}s`]
|
|
5672
|
-
];
|
|
5673
|
-
var isRecord = (v) => typeof v === "object" && v !== null;
|
|
5674
|
-
function saveReport(deps, result, report, sessionId) {
|
|
5675
|
-
const sessionReport = typeof result.reportPath === "string" ? result.reportPath : undefined;
|
|
5676
|
-
const attempt = deps.attemptDir();
|
|
5677
|
-
if (attempt === undefined)
|
|
5678
|
-
return sessionReport;
|
|
5679
|
-
const out = join2(attempt, PERF_DIR);
|
|
5680
|
-
mkdirSync3(out, { recursive: true });
|
|
5681
|
-
const sessionDir = typeof result.dir === "string" ? result.dir : undefined;
|
|
5682
|
-
const write = (name, from, fallback) => {
|
|
5683
|
-
if (from !== undefined && existsSync3(from))
|
|
5684
|
-
copyFileSync(from, join2(out, name));
|
|
5685
|
-
else if (fallback !== undefined)
|
|
5686
|
-
writeFileSync(join2(out, name), fallback);
|
|
5687
|
-
else
|
|
5688
|
-
return;
|
|
5689
|
-
deps.addFile?.(`${PERF_DIR}/${name}`);
|
|
5690
|
-
};
|
|
5691
|
-
write("report.json", sessionReport, `${JSON.stringify(report, null, 2)}
|
|
5692
|
-
`);
|
|
5693
|
-
write("report.md", sessionDir === undefined ? undefined : join2(sessionDir, "report.md"));
|
|
5694
|
-
write("session.json", undefined, `${JSON.stringify({ sessionId, dir: sessionDir }, null, 2)}
|
|
5695
|
-
`);
|
|
5696
|
-
return join2(out, "report.json");
|
|
6366
|
+
function readingOrder(snapshot, platform, opts = {}) {
|
|
6367
|
+
const out = [];
|
|
6368
|
+
for (const el of orderSiblings(snapshot.elements, platform))
|
|
6369
|
+
walk(el, platform, snapshot.screen, opts, out);
|
|
6370
|
+
return out.map((e) => e.announcement);
|
|
5697
6371
|
}
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
6372
|
+
|
|
6373
|
+
// ../../packages/e2e/src/a11y/rules/rule.context.ts
|
|
6374
|
+
function unitsPerPoint(snapshot) {
|
|
6375
|
+
return snapshot.platform === "android" ? snapshot.screen.scale || 1 : 1;
|
|
6376
|
+
}
|
|
6377
|
+
function isAncestor(a, b, parents) {
|
|
6378
|
+
for (let p = parents.get(b);p; p = parents.get(p))
|
|
6379
|
+
if (p === a)
|
|
6380
|
+
return true;
|
|
6381
|
+
return false;
|
|
6382
|
+
}
|
|
6383
|
+
function related(a, b, parents) {
|
|
6384
|
+
return a === b || isAncestor(a, b, parents) || isAncestor(b, a, parents);
|
|
6385
|
+
}
|
|
6386
|
+
function issue(rule, severity, el, message, wcag) {
|
|
6387
|
+
return {
|
|
6388
|
+
rule,
|
|
6389
|
+
severity,
|
|
6390
|
+
source: "warden",
|
|
6391
|
+
elementId: el.id,
|
|
6392
|
+
message,
|
|
6393
|
+
...wcag === undefined ? {} : { wcag },
|
|
6394
|
+
...el.identifier === undefined ? {} : { identifier: el.identifier }
|
|
6395
|
+
};
|
|
6396
|
+
}
|
|
6397
|
+
function describe(el) {
|
|
6398
|
+
const what = el.identifier ?? el.label ?? el.text ?? el.rawType;
|
|
6399
|
+
return `${el.role} "${what}"`;
|
|
6400
|
+
}
|
|
6401
|
+
|
|
6402
|
+
// ../../packages/e2e/src/a11y/rules/duplicate-name.ts
|
|
6403
|
+
var key = (s) => s.trim().toLowerCase().replace(/\s+/g, " ");
|
|
6404
|
+
var sameFrame2 = (a, b) => a.frame.x === b.frame.x && a.frame.y === b.frame.y && a.frame.width === b.frame.width && a.frame.height === b.frame.height;
|
|
6405
|
+
var duplicateName = (_snapshot, ctx) => {
|
|
6406
|
+
const named = ctx.reachable.flatMap((el) => {
|
|
6407
|
+
if (!isActionable(el, ctx.platform))
|
|
6408
|
+
return [];
|
|
6409
|
+
const name = accessibleName(el, ctx.platform);
|
|
6410
|
+
return name === undefined ? [] : [{ el, name: key(name), raw: name }];
|
|
6411
|
+
});
|
|
6412
|
+
const out = [];
|
|
6413
|
+
const kept = [];
|
|
6414
|
+
for (const n of named) {
|
|
6415
|
+
const first = kept.find((k) => k.name === n.name && !sameFrame2(k.el, n.el) && !related(k.el, n.el, ctx.parents) && !(k.el.identifier !== undefined && k.el.identifier === n.el.identifier));
|
|
6416
|
+
if (first) {
|
|
6417
|
+
out.push(issue("duplicate-name", "warning", n.el, `${describe(n.el)} is announced "${n.raw}", the same as ${describe(first.el)}`, "2.4.6"));
|
|
6418
|
+
} else
|
|
6419
|
+
kept.push(n);
|
|
6420
|
+
}
|
|
6421
|
+
return out;
|
|
6422
|
+
};
|
|
6423
|
+
|
|
6424
|
+
// ../../packages/e2e/src/a11y/rules/hidden-focusable.ts
|
|
6425
|
+
function offScreen(el, screen) {
|
|
6426
|
+
const f = el.frame;
|
|
6427
|
+
return f.x >= screen.width || f.y >= screen.height || f.x + f.width <= 0 || f.y + f.height <= 0;
|
|
6428
|
+
}
|
|
6429
|
+
var hiddenFocusable = (snapshot) => snapshot.flat.filter((el) => el.a11y?.focusable === true && el.a11y.important !== false).flatMap((el) => {
|
|
6430
|
+
const why = el.hidden ? "hidden" : el.frame.width <= 0 || el.frame.height <= 0 ? "zero-size" : offScreen(el, snapshot.screen) ? "off screen" : undefined;
|
|
6431
|
+
return why ? [issue("hidden-focusable", "warning", el, `${describe(el)} is ${why} but still focusable`)] : [];
|
|
6432
|
+
});
|
|
6433
|
+
|
|
6434
|
+
// ../../packages/e2e/src/a11y/rules/image-no-name.ts
|
|
6435
|
+
var imageNoName = (_snapshot, ctx) => ctx.reachable.filter((el) => el.role === "image" && accessibleName(el, ctx.platform) === undefined).map((el) => issue("image-no-name", "error", el, `${describe(el)} has no name (add a label, or mark it decorative)`, "1.1.1"));
|
|
6436
|
+
|
|
6437
|
+
// ../../packages/e2e/src/a11y/rules/label-in-name.ts
|
|
6438
|
+
var norm = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
|
|
6439
|
+
function visibleText(el, platform) {
|
|
6440
|
+
const own = platform === "android" ? el.text?.trim() : undefined;
|
|
6441
|
+
if (own)
|
|
6442
|
+
return own;
|
|
6443
|
+
const parts = [];
|
|
6444
|
+
const walk = (e) => {
|
|
6445
|
+
for (const c of e.children) {
|
|
6446
|
+
if (c.hidden)
|
|
6447
|
+
continue;
|
|
6448
|
+
if (c.role === "text") {
|
|
6449
|
+
const t = (c.text ?? c.label ?? c.value)?.trim();
|
|
6450
|
+
if (t)
|
|
6451
|
+
parts.push(t);
|
|
6452
|
+
} else
|
|
6453
|
+
walk(c);
|
|
6454
|
+
}
|
|
6455
|
+
};
|
|
6456
|
+
walk(el);
|
|
6457
|
+
return parts.join(" ");
|
|
6458
|
+
}
|
|
6459
|
+
var labelInName = (_snapshot, ctx) => ctx.reachable.flatMap((el) => {
|
|
6460
|
+
if (!isActionable(el, ctx.platform) || !el.label?.trim())
|
|
6461
|
+
return [];
|
|
6462
|
+
const visible = visibleText(el, ctx.platform);
|
|
6463
|
+
const name = accessibleName(el, ctx.platform);
|
|
6464
|
+
if (!visible || !name || norm(name).includes(norm(visible)))
|
|
6465
|
+
return [];
|
|
6466
|
+
return [issue("label-in-name", "error", el, `${describe(el)} shows "${visible}" but is named "${name}"`, "2.5.3")];
|
|
6467
|
+
});
|
|
6468
|
+
|
|
6469
|
+
// ../../packages/e2e/src/a11y/rules/missing-name.ts
|
|
6470
|
+
function isScrollContainer(el, platform) {
|
|
6471
|
+
const scrolls = el.role === "scrollview" || el.role === "list" || el.traits.includes("scrollable");
|
|
6472
|
+
return scrolls && !isActionable(el, platform);
|
|
6473
|
+
}
|
|
6474
|
+
var missingName = (_snapshot, ctx) => ctx.reachable.filter((el) => isFocusTarget(el, ctx.platform) && !isScrollContainer(el, ctx.platform) && accessibleName(el, ctx.platform) === undefined).map((el) => issue("missing-name", "error", el, `${describe(el)} has no accessible name`, "4.1.2"));
|
|
6475
|
+
|
|
6476
|
+
// ../../packages/e2e/src/a11y/rules/role-in-name.ts
|
|
6477
|
+
var WORDS = {
|
|
6478
|
+
button: /\b(button|btn)\b/i,
|
|
6479
|
+
image: /\b(image|icon)\b/i
|
|
6480
|
+
};
|
|
6481
|
+
var roleInName = (_snapshot, ctx) => ctx.reachable.flatMap((el) => {
|
|
6482
|
+
const pattern = WORDS[el.role];
|
|
6483
|
+
const name = pattern ? accessibleName(el, ctx.platform) : undefined;
|
|
6484
|
+
const hit = name ? pattern?.exec(name)?.[0] : undefined;
|
|
6485
|
+
return hit ? [
|
|
6486
|
+
issue("role-in-name", "warning", el, `${describe(el)} name "${name}" contains its role word "${hit}", announced twice`)
|
|
6487
|
+
] : [];
|
|
6488
|
+
});
|
|
6489
|
+
|
|
6490
|
+
// ../../packages/e2e/src/a11y/rules/target-size.ts
|
|
6491
|
+
var WCAG_MIN = 24;
|
|
6492
|
+
var IOS_MIN = 44;
|
|
6493
|
+
var ANDROID_MIN = 48;
|
|
6494
|
+
var RADIUS = WCAG_MIN / 2;
|
|
6495
|
+
function targets(snapshot, ctx) {
|
|
6496
|
+
const k = unitsPerPoint(snapshot);
|
|
6497
|
+
return ctx.reachable.filter((el) => el.enabled && isActionable(el, ctx.platform) && el.frame.width > 0 && el.frame.height > 0).map((el) => ({ el, x: el.frame.x / k, y: el.frame.y / k, w: el.frame.width / k, h: el.frame.height / k }));
|
|
6498
|
+
}
|
|
6499
|
+
function centre(b) {
|
|
6500
|
+
return { x: b.x + b.w / 2, y: b.y + b.h / 2 };
|
|
6501
|
+
}
|
|
6502
|
+
function distanceToBox(p, b) {
|
|
6503
|
+
const dx = Math.max(b.x - p.x, 0, p.x - (b.x + b.w));
|
|
6504
|
+
const dy = Math.max(b.y - p.y, 0, p.y - (b.y + b.h));
|
|
6505
|
+
return Math.hypot(dx, dy);
|
|
6506
|
+
}
|
|
6507
|
+
var undersized = (b) => b.w < WCAG_MIN || b.h < WCAG_MIN;
|
|
6508
|
+
function wcagFailures(snapshot, ctx) {
|
|
6509
|
+
const all = targets(snapshot, ctx);
|
|
6510
|
+
return all.filter((b) => {
|
|
6511
|
+
if (!undersized(b))
|
|
6512
|
+
return false;
|
|
6513
|
+
const c = centre(b);
|
|
6514
|
+
return all.some((o) => {
|
|
6515
|
+
if (related(b.el, o.el, ctx.parents))
|
|
6516
|
+
return false;
|
|
6517
|
+
if (undersized(o))
|
|
6518
|
+
return Math.hypot(centre(o).x - c.x, centre(o).y - c.y) < RADIUS * 2;
|
|
6519
|
+
return distanceToBox(c, o) < RADIUS;
|
|
6520
|
+
});
|
|
6521
|
+
});
|
|
6522
|
+
}
|
|
6523
|
+
var fmt = (n) => String(Math.round(n * 10) / 10);
|
|
6524
|
+
var targetSize = (snapshot, ctx) => {
|
|
6525
|
+
const unit = ctx.platform === "ios" ? "pt" : "dp";
|
|
6526
|
+
return wcagFailures(snapshot, ctx).map((b) => issue("target-size", "error", b.el, `${describe(b.el)} is ${fmt(b.w)}x${fmt(b.h)}${unit}, under ${WCAG_MIN}x${WCAG_MIN}${unit} with another target within ${WCAG_MIN}${unit}`, "2.5.8"));
|
|
6527
|
+
};
|
|
6528
|
+
var targetSizePlatform = (snapshot, ctx) => {
|
|
6529
|
+
const min = ctx.platform === "ios" ? IOS_MIN : ANDROID_MIN;
|
|
6530
|
+
const unit = ctx.platform === "ios" ? "pt" : "dp";
|
|
6531
|
+
const failing = new Set(wcagFailures(snapshot, ctx).map((b) => b.el));
|
|
6532
|
+
return targets(snapshot, ctx).filter((b) => (b.w < min || b.h < min) && !failing.has(b.el)).map((b) => issue("target-size-platform", "warning", b.el, `${describe(b.el)} is ${fmt(b.w)}x${fmt(b.h)}${unit}, under the platform minimum ${min}${unit}`));
|
|
6533
|
+
};
|
|
6534
|
+
|
|
6535
|
+
// ../../packages/e2e/src/a11y/rules/text-clipped.ts
|
|
6536
|
+
var CLIPPING_MIN_SCALE = 1.6;
|
|
6537
|
+
var ELLIPSIS = /(\u2026|\.\.\.)$/;
|
|
6538
|
+
var TOLERANCE = 1;
|
|
6539
|
+
function overflows(el, parent) {
|
|
6540
|
+
const f = el.frame;
|
|
6541
|
+
const p = parent.frame;
|
|
6542
|
+
return f.x < p.x - TOLERANCE || f.y < p.y - TOLERANCE || f.x + f.width > p.x + p.width + TOLERANCE || f.y + f.height > p.y + p.height + TOLERANCE;
|
|
6543
|
+
}
|
|
6544
|
+
var textClipped = (snapshot, ctx) => {
|
|
6545
|
+
if (ctx.textScale === undefined || ctx.textScale < CLIPPING_MIN_SCALE)
|
|
6546
|
+
return [];
|
|
6547
|
+
return ctx.reachable.flatMap((el) => {
|
|
6548
|
+
if (el.role !== "text")
|
|
6549
|
+
return [];
|
|
6550
|
+
const content = (el.text ?? el.label ?? el.value)?.trim();
|
|
6551
|
+
if (content && ELLIPSIS.test(content))
|
|
6552
|
+
return [issue("text-clipped", "warning", el, `${describe(el)} is truncated ("${content}")`, "1.4.4")];
|
|
6553
|
+
const parent = ctx.parents.get(el);
|
|
6554
|
+
if (!parent || parent.role === "scrollview" || parent.role === "list" || parent.traits.includes("scrollable"))
|
|
6555
|
+
return [];
|
|
6556
|
+
if (parent.frame.width >= snapshot.screen.width && parent.frame.height >= snapshot.screen.height)
|
|
6557
|
+
return [];
|
|
6558
|
+
return overflows(el, parent) ? [issue("text-clipped", "warning", el, `${describe(el)} overflows its container`, "1.4.4")] : [];
|
|
6559
|
+
});
|
|
6560
|
+
};
|
|
6561
|
+
|
|
6562
|
+
// ../../packages/e2e/src/a11y/audit.ts
|
|
6563
|
+
var WARDEN_RULES = [
|
|
6564
|
+
{ id: "missing-name", run: missingName },
|
|
6565
|
+
{ id: "image-no-name", run: imageNoName },
|
|
6566
|
+
{ id: "label-in-name", run: labelInName },
|
|
6567
|
+
{ id: "target-size", run: targetSize },
|
|
6568
|
+
{ id: "target-size-platform", run: targetSizePlatform },
|
|
6569
|
+
{ id: "duplicate-name", run: duplicateName },
|
|
6570
|
+
{ id: "role-in-name", run: roleInName },
|
|
6571
|
+
{ id: "hidden-focusable", run: hiddenFocusable },
|
|
6572
|
+
{ id: "text-clipped", run: textClipped }
|
|
6573
|
+
];
|
|
6574
|
+
function reachableNodes(snapshot) {
|
|
6575
|
+
const reachable = [];
|
|
6576
|
+
const parents = new Map;
|
|
6577
|
+
const walk = (el, parent) => {
|
|
6578
|
+
if (parent)
|
|
6579
|
+
parents.set(el, parent);
|
|
6580
|
+
if (el.hidden)
|
|
6581
|
+
return;
|
|
6582
|
+
if (el.a11y?.important !== false)
|
|
6583
|
+
reachable.push(el);
|
|
6584
|
+
for (const c of el.children)
|
|
6585
|
+
walk(c, el);
|
|
6586
|
+
};
|
|
6587
|
+
for (const el of snapshot.elements)
|
|
6588
|
+
walk(el, undefined);
|
|
6589
|
+
return { reachable, parents };
|
|
6590
|
+
}
|
|
6591
|
+
var SEVERITY_RANK = { warning: 0, error: 1 };
|
|
6592
|
+
var ruleRank = (rule) => {
|
|
6593
|
+
const i = WARDEN_RULES.findIndex((r) => r.id === rule);
|
|
6594
|
+
return i === -1 ? WARDEN_RULES.length : i;
|
|
6595
|
+
};
|
|
6596
|
+
function positions(snapshot, platform) {
|
|
6597
|
+
const listed = new Map;
|
|
6598
|
+
readingOrder(snapshot, platform).forEach((a, i) => {
|
|
6599
|
+
listed.set(a.id, i);
|
|
6600
|
+
});
|
|
6601
|
+
const pos = new Map;
|
|
6602
|
+
let last = -1;
|
|
6603
|
+
snapshot.flat.forEach((el, i) => {
|
|
6604
|
+
const at = listed.get(el.id);
|
|
6605
|
+
if (at !== undefined) {
|
|
6606
|
+
pos.set(el.id, at);
|
|
6607
|
+
last = at;
|
|
6608
|
+
} else
|
|
6609
|
+
pos.set(el.id, last + 0.5 + i / 1e6);
|
|
6610
|
+
});
|
|
6611
|
+
return pos;
|
|
6612
|
+
}
|
|
6613
|
+
function runAudit(snapshot, platform, opts = {}) {
|
|
6614
|
+
const { reachable, parents } = reachableNodes(snapshot);
|
|
6615
|
+
const resolved = opts.textSize === undefined ? undefined : resolveTextSize(opts.textSize);
|
|
6616
|
+
const ctx = {
|
|
6617
|
+
platform,
|
|
6618
|
+
reachable,
|
|
6619
|
+
parents,
|
|
6620
|
+
...resolved?.success ? { textScale: resolved.data.scale } : {}
|
|
6621
|
+
};
|
|
6622
|
+
const wanted = opts.rules === undefined ? undefined : new Set(opts.rules);
|
|
6623
|
+
const found = WARDEN_RULES.filter((r) => wanted === undefined || wanted.has(r.id)).flatMap((r) => r.run(snapshot, ctx));
|
|
6624
|
+
const ignore = opts.ignore ?? [];
|
|
6625
|
+
const kept = found.filter((i) => !ignore.some((g) => g.rule === i.rule && (g.identifier === undefined || g.identifier === i.identifier)));
|
|
6626
|
+
const seen = new Set;
|
|
6627
|
+
const unique = kept.filter((i) => {
|
|
6628
|
+
const k = `${i.rule}\x00${i.elementId ?? ""}\x00${i.message}`;
|
|
6629
|
+
if (seen.has(k))
|
|
6630
|
+
return false;
|
|
6631
|
+
seen.add(k);
|
|
6632
|
+
return true;
|
|
6633
|
+
});
|
|
6634
|
+
const pos = positions(snapshot, platform);
|
|
6635
|
+
const at = (i) => i.elementId === undefined ? Number.MAX_SAFE_INTEGER : pos.get(i.elementId) ?? 0;
|
|
6636
|
+
const issues = unique.sort((a, b) => at(a) - at(b) || ruleRank(a.rule) - ruleRank(b.rule));
|
|
6637
|
+
return { issues, checked: reachable.length, sources: ["warden"], skipped: [] };
|
|
6638
|
+
}
|
|
6639
|
+
function failing(result, minSeverity = "error") {
|
|
6640
|
+
return result.issues.filter((i) => SEVERITY_RANK[i.severity] >= SEVERITY_RANK[minSeverity]);
|
|
6641
|
+
}
|
|
6642
|
+
function elementKey(i) {
|
|
6643
|
+
return i.identifier ?? i.elementId;
|
|
6644
|
+
}
|
|
6645
|
+
function mergeIssues(base, extra, skipped = []) {
|
|
6646
|
+
const issues = [...base.issues];
|
|
6647
|
+
const added = [];
|
|
6648
|
+
for (const e of extra) {
|
|
6649
|
+
const key = elementKey(e);
|
|
6650
|
+
const idx = issues.findIndex((i) => key !== undefined && elementKey(i) === key && (e.wcag !== undefined ? i.wcag === e.wcag : i.rule === e.rule && i.message === e.message));
|
|
6651
|
+
const hit = idx === -1 ? undefined : issues[idx];
|
|
6652
|
+
if (hit === undefined) {
|
|
6653
|
+
added.push(e);
|
|
6654
|
+
} else if (hit.source !== e.source && !hit.message.includes(`also reported by ${e.source}`)) {
|
|
6655
|
+
issues[idx] = { ...hit, message: `${hit.message} (also reported by ${e.source})` };
|
|
6656
|
+
}
|
|
6657
|
+
}
|
|
6658
|
+
const sources = [...new Set([...base.sources, ...extra.map((e) => e.source)])];
|
|
6659
|
+
return {
|
|
6660
|
+
issues: [...issues, ...added],
|
|
6661
|
+
checked: base.checked,
|
|
6662
|
+
sources,
|
|
6663
|
+
skipped: [...base.skipped, ...skipped]
|
|
6664
|
+
};
|
|
6665
|
+
}
|
|
6666
|
+
function formatIssues(result, minSeverity = "warning") {
|
|
6667
|
+
const shown = failing(result, minSeverity);
|
|
6668
|
+
const head = `a11y audit: ${shown.length} issue${shown.length === 1 ? "" : "s"} in ${result.checked} elements (${result.sources.join(", ")})`;
|
|
6669
|
+
const skipped = result.skipped.map((s) => `skipped ${s.source}: ${s.reason}`);
|
|
6670
|
+
if (shown.length === 0)
|
|
6671
|
+
return [head, ...skipped].join(`
|
|
6672
|
+
`);
|
|
6673
|
+
const rows = shown.map((i) => [i.severity, i.rule, i.wcag ?? "-", i.identifier ?? i.elementId ?? "-", i.message]);
|
|
6674
|
+
const widths = [0, 1, 2, 3].map((c) => Math.max(...rows.map((r) => r[c]?.length ?? 0)));
|
|
6675
|
+
const lines = rows.map((r) => `${r.map((cell, c) => c < 4 ? cell.padEnd(widths[c] ?? 0) : cell).join(" ")}`);
|
|
6676
|
+
return [head, ...lines, ...skipped].join(`
|
|
6677
|
+
`);
|
|
6678
|
+
}
|
|
6679
|
+
|
|
6680
|
+
// ../../packages/e2e/src/driver/unwrap.ts
|
|
6681
|
+
var TIMEOUTS = new Set(["E_TIMEOUT", "E_APP_NOT_RESPONDING"]);
|
|
6682
|
+
var INFRASTRUCTURE = new Set([
|
|
6683
|
+
"E_HELPER_MISSING",
|
|
6684
|
+
"E_HELPER_CRASHED",
|
|
6685
|
+
"E_NOT_BOOTED",
|
|
6686
|
+
"E_UNKNOWN_DEVICE",
|
|
6687
|
+
"E_AX_UNAVAILABLE",
|
|
6688
|
+
"E_APP_UNAVAILABLE",
|
|
6689
|
+
"E_INTERNAL",
|
|
6690
|
+
"E_DEVICE_LOCKED"
|
|
6691
|
+
]);
|
|
6692
|
+
function codeFor(code) {
|
|
6693
|
+
if (TIMEOUTS.has(code))
|
|
6694
|
+
return "TIMEOUT";
|
|
6695
|
+
if (INFRASTRUCTURE.has(code))
|
|
6696
|
+
return "DRIVER";
|
|
6697
|
+
return "ACTION_FAILED";
|
|
6698
|
+
}
|
|
6699
|
+
function toE2eError(error, action) {
|
|
6700
|
+
return new E2eError2(codeFor(error.code), `${action} failed: ${error.code}: ${error.message}`);
|
|
6701
|
+
}
|
|
6702
|
+
function aborted2(action, signal) {
|
|
6703
|
+
const reason = signal.reason;
|
|
6704
|
+
const detail = reason instanceof Error ? `: ${reason.message}` : "";
|
|
6705
|
+
return new E2eError2("TIMEOUT", `${action} aborted (test deadline reached or cancelled)${detail}`);
|
|
6706
|
+
}
|
|
6707
|
+
async function settled(call, action, signal) {
|
|
6708
|
+
if (signal === undefined)
|
|
6709
|
+
return call();
|
|
6710
|
+
if (signal.aborted)
|
|
6711
|
+
throw aborted2(action, signal);
|
|
6712
|
+
let onAbort;
|
|
6713
|
+
const abort = new Promise((_, reject) => {
|
|
6714
|
+
onAbort = () => reject(aborted2(action, signal));
|
|
6715
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
6716
|
+
});
|
|
6717
|
+
try {
|
|
6718
|
+
return await Promise.race([call(), abort]);
|
|
6719
|
+
} finally {
|
|
6720
|
+
if (onAbort !== undefined)
|
|
6721
|
+
signal.removeEventListener("abort", onAbort);
|
|
6722
|
+
}
|
|
6723
|
+
}
|
|
6724
|
+
async function unwrap(call, action, signal) {
|
|
6725
|
+
const result = await settled(call, action, signal);
|
|
6726
|
+
if (!result.success)
|
|
6727
|
+
throw toE2eError(result.error, action);
|
|
6728
|
+
return result.data;
|
|
6729
|
+
}
|
|
6730
|
+
|
|
6731
|
+
// ../../packages/e2e/src/driver/a11y-events.ts
|
|
6732
|
+
function canReadA11yEvents(driver) {
|
|
6733
|
+
return typeof driver.a11yEvents === "function";
|
|
6734
|
+
}
|
|
6735
|
+
var KINDS = new Set([
|
|
6736
|
+
"announcement",
|
|
6737
|
+
"live-region",
|
|
6738
|
+
"screen-changed",
|
|
6739
|
+
"layout-changed"
|
|
6740
|
+
]);
|
|
6741
|
+
function isRecord(value) {
|
|
6742
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6743
|
+
}
|
|
6744
|
+
var SYSTEM_ANNOUNCEMENT = /^AXAnnouncementType/;
|
|
6745
|
+
function parseA11yEvent(value, source) {
|
|
6746
|
+
if (!isRecord(value) || typeof value.kind !== "string" || !KINDS.has(value.kind))
|
|
6747
|
+
return;
|
|
6748
|
+
if (typeof value.at !== "number" || !Number.isFinite(value.at))
|
|
6749
|
+
return;
|
|
6750
|
+
if (value.text !== undefined && typeof value.text !== "string")
|
|
6751
|
+
return;
|
|
6752
|
+
return {
|
|
6753
|
+
kind: value.kind,
|
|
6754
|
+
...typeof value.text === "string" && value.text !== "" ? { text: value.text } : {},
|
|
6755
|
+
at: value.at,
|
|
6756
|
+
source
|
|
6757
|
+
};
|
|
6758
|
+
}
|
|
6759
|
+
function parseUiadA11yEvents(data) {
|
|
6760
|
+
if (!isRecord(data) || !Array.isArray(data.events) || typeof data.cursor !== "number") {
|
|
6761
|
+
return { success: false, error: "warden-uiad answered a11yEvents with an unexpected shape" };
|
|
6762
|
+
}
|
|
6763
|
+
const events = [];
|
|
6764
|
+
for (const [index, raw] of data.events.entries()) {
|
|
6765
|
+
const event = parseA11yEvent(raw, "uiad");
|
|
6766
|
+
if (!event)
|
|
6767
|
+
return { success: false, error: `warden-uiad sent a malformed a11y event at ${index}` };
|
|
6768
|
+
events.push(event);
|
|
6769
|
+
}
|
|
6770
|
+
return { success: true, data: { events, cursor: data.cursor, ...data.lost === true ? { lost: true } : {} } };
|
|
6771
|
+
}
|
|
6772
|
+
function parseInjectEvents(text, since = 0) {
|
|
6773
|
+
const bytes = Buffer.from(text, "utf8");
|
|
6774
|
+
const start = since > bytes.length || since < 0 ? 0 : since;
|
|
6775
|
+
const end = bytes.lastIndexOf(10) + 1;
|
|
6776
|
+
if (end <= start)
|
|
6777
|
+
return { events: [], cursor: start };
|
|
6778
|
+
const events = [];
|
|
6779
|
+
for (const line of bytes.subarray(start, end).toString("utf8").split(`
|
|
6780
|
+
`)) {
|
|
6781
|
+
if (line.trim() === "")
|
|
6782
|
+
continue;
|
|
6783
|
+
try {
|
|
6784
|
+
const event = parseA11yEvent(JSON.parse(line), "inject");
|
|
6785
|
+
if (event && !(event.text !== undefined && SYSTEM_ANNOUNCEMENT.test(event.text)))
|
|
6786
|
+
events.push(event);
|
|
6787
|
+
} catch {}
|
|
6788
|
+
}
|
|
6789
|
+
return { events, cursor: end };
|
|
6790
|
+
}
|
|
6791
|
+
|
|
6792
|
+
// ../../packages/e2e/src/a11y/announcements.ts
|
|
6793
|
+
var ANNOUNCEMENT_TIMEOUT_MS = 5000;
|
|
6794
|
+
var ANNOUNCEMENT_POLL_MS = 100;
|
|
6795
|
+
var ANNOUNCING = new Set(["announcement", "live-region"]);
|
|
6796
|
+
function matches(event, match) {
|
|
6797
|
+
if (!ANNOUNCING.has(event.kind) || event.text === undefined)
|
|
6798
|
+
return false;
|
|
6799
|
+
if (typeof match === "string")
|
|
6800
|
+
return event.text.includes(match);
|
|
6801
|
+
match.lastIndex = 0;
|
|
6802
|
+
return match.test(event.text);
|
|
6803
|
+
}
|
|
6804
|
+
function withDefaults(opts) {
|
|
6805
|
+
return {
|
|
6806
|
+
now: opts.now ?? Date.now,
|
|
6807
|
+
sleep: opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
6808
|
+
timeoutMs: opts.timeoutMs ?? ANNOUNCEMENT_TIMEOUT_MS,
|
|
6809
|
+
pollMs: opts.pollMs ?? ANNOUNCEMENT_POLL_MS
|
|
6810
|
+
};
|
|
6811
|
+
}
|
|
6812
|
+
async function waitForAnnouncement(driver, match, opts = {}) {
|
|
6813
|
+
if (!canReadA11yEvents(driver)) {
|
|
6814
|
+
return {
|
|
6815
|
+
success: false,
|
|
6816
|
+
error: driverError("E_UNSUPPORTED", "this driver cannot read announcements (no a11yEvents capability)")
|
|
6817
|
+
};
|
|
6818
|
+
}
|
|
6819
|
+
const { now, sleep, timeoutMs, pollMs } = withDefaults(opts);
|
|
6820
|
+
const deadline = now() + timeoutMs;
|
|
6821
|
+
let cursor = opts.since;
|
|
6822
|
+
const seen = [];
|
|
6823
|
+
for (;; ) {
|
|
6824
|
+
const read = await driver.a11yEvents(cursor);
|
|
6825
|
+
if (!read.success)
|
|
6826
|
+
return read;
|
|
6827
|
+
cursor = read.data.cursor;
|
|
6828
|
+
const hit = read.data.events.find((event) => matches(event, match));
|
|
6829
|
+
if (hit)
|
|
6830
|
+
return { success: true, data: { event: hit, cursor: read.data.cursor } };
|
|
6831
|
+
for (const event of read.data.events)
|
|
6832
|
+
if (event.text !== undefined)
|
|
6833
|
+
seen.push(event.text);
|
|
6834
|
+
if (now() >= deadline) {
|
|
6835
|
+
return {
|
|
6836
|
+
success: false,
|
|
6837
|
+
error: driverError("E_TIMEOUT", `no announcement matched ${String(match)} within ${timeoutMs} ms`, {
|
|
6838
|
+
detail: { seen: seen.slice(-10), cursor }
|
|
6839
|
+
})
|
|
6840
|
+
};
|
|
6841
|
+
}
|
|
6842
|
+
await sleep(pollMs);
|
|
6843
|
+
}
|
|
6844
|
+
}
|
|
6845
|
+
|
|
6846
|
+
// ../../packages/e2e/src/driver/a11y-read.ts
|
|
6847
|
+
function canReadA11y(driver) {
|
|
6848
|
+
return typeof driver.readA11y === "function";
|
|
6849
|
+
}
|
|
6850
|
+
|
|
6851
|
+
// ../../packages/e2e/src/driver/native-audit.ts
|
|
6852
|
+
function canAuditNatively(driver) {
|
|
6853
|
+
return typeof driver.auditNatively === "function";
|
|
6854
|
+
}
|
|
6855
|
+
|
|
6856
|
+
// ../../packages/e2e/src/driver/screen-reader.ts
|
|
6857
|
+
function canDriveScreenReader(driver) {
|
|
6858
|
+
return typeof driver.screenReaderNext === "function";
|
|
6859
|
+
}
|
|
6860
|
+
|
|
6861
|
+
// ../../packages/e2e/src/device-a11y.ts
|
|
6862
|
+
var TALKBACK_HINT = "turn TalkBack on with device.a11y({ screenReader: true }) on an Android emulator";
|
|
6863
|
+
function unsupported(action, why) {
|
|
6864
|
+
return toE2eError(driverError("E_UNSUPPORTED", why), action);
|
|
6865
|
+
}
|
|
6866
|
+
async function withA11yRead(driver, snapshot) {
|
|
6867
|
+
if (!canReadA11y(driver))
|
|
6868
|
+
return { success: true, data: snapshot };
|
|
6869
|
+
const read = await driver.readA11y(snapshot);
|
|
6870
|
+
if (read.success)
|
|
6871
|
+
return read;
|
|
6872
|
+
if (read.error.code === "E_UNSUPPORTED")
|
|
6873
|
+
return { success: true, data: snapshot };
|
|
6874
|
+
return read;
|
|
6875
|
+
}
|
|
6876
|
+
var A11Y_EXCERPT_LINES = 40;
|
|
6877
|
+
function renderA11yScreen(snapshot, maxLines = A11Y_EXCERPT_LINES) {
|
|
6878
|
+
const rendered = renderSnapshot(snapshot, { interactiveOnly: true, refs: new Map, a11y: true, maxLines });
|
|
6879
|
+
return [renderHeader({ snapshot, rendered, interactiveOnly: true }), ...rendered.lines].join(`
|
|
6880
|
+
`);
|
|
6881
|
+
}
|
|
6882
|
+
function ignored(issue, ignore) {
|
|
6883
|
+
return ignore.some((g) => g.rule === issue.rule && (g.identifier === undefined || g.identifier === issue.identifier));
|
|
6884
|
+
}
|
|
6885
|
+
async function auditSnapshot(driver, snapshot, options = {}) {
|
|
6886
|
+
const { platform } = driver;
|
|
6887
|
+
const { rules, minSeverity } = options;
|
|
6888
|
+
const ignore = options.ignore ?? [];
|
|
6889
|
+
const own = runAudit(snapshot, platform, {
|
|
6890
|
+
...rules === undefined ? {} : { rules },
|
|
6891
|
+
ignore,
|
|
6892
|
+
...minSeverity === undefined ? {} : { minSeverity },
|
|
6893
|
+
...options.textSize === undefined ? {} : { textSize: options.textSize }
|
|
6894
|
+
});
|
|
6895
|
+
if (options.native === false || rules !== undefined && !rules.includes("native"))
|
|
6896
|
+
return own;
|
|
6897
|
+
const source = platform === "ios" ? "xcuitest" : "atf";
|
|
6898
|
+
if (!canAuditNatively(driver)) {
|
|
6899
|
+
return mergeIssues(own, [], [{ source, reason: "this driver has no native accessibility audit" }]);
|
|
6900
|
+
}
|
|
6901
|
+
const native = await driver.auditNatively(options.bundleId === undefined ? {} : { bundleId: options.bundleId });
|
|
6902
|
+
return mergeIssues(own, native.issues.filter((issue) => !ignored(issue, ignore)), native.skipped === undefined ? [] : [native.skipped]);
|
|
6903
|
+
}
|
|
6904
|
+
function createDeviceA11y(deps) {
|
|
6905
|
+
const { driver, env } = deps;
|
|
6906
|
+
const signal = () => deps.signal();
|
|
6907
|
+
const { platform } = driver;
|
|
6908
|
+
const readSnapshot = async () => {
|
|
6909
|
+
const snapshot = await unwrap(() => driver.snapshot(), "snapshot", signal());
|
|
6910
|
+
const merged = await withA11yRead(driver, snapshot);
|
|
6911
|
+
if (!merged.success)
|
|
6912
|
+
throw toE2eError(merged.error, "read accessibility facts");
|
|
6913
|
+
return merged.data;
|
|
6914
|
+
};
|
|
6915
|
+
const audit = async (options = {}) => {
|
|
6916
|
+
const config = env.a11yAudit ?? {};
|
|
6917
|
+
const rules = options.rules ?? config.rules;
|
|
6918
|
+
const ignore = options.ignore ?? config.ignore ?? [];
|
|
6919
|
+
const minSeverity = options.minSeverity ?? config.minSeverity;
|
|
6920
|
+
return auditSnapshot(driver, await readSnapshot(), {
|
|
6921
|
+
...rules === undefined ? {} : { rules },
|
|
6922
|
+
ignore,
|
|
6923
|
+
...minSeverity === undefined ? {} : { minSeverity },
|
|
6924
|
+
...options.textSize === undefined ? {} : { textSize: options.textSize },
|
|
6925
|
+
...options.native === undefined ? {} : { native: options.native },
|
|
6926
|
+
...env.bundleId === undefined ? {} : { bundleId: env.bundleId }
|
|
6927
|
+
});
|
|
6928
|
+
};
|
|
6929
|
+
const announcements = async (options = {}) => {
|
|
6930
|
+
if (!canReadA11yEvents(driver)) {
|
|
6931
|
+
throw unsupported("device.announcements", "this driver cannot read announcements (no a11yEvents capability)");
|
|
6932
|
+
}
|
|
6933
|
+
const { since } = options;
|
|
6934
|
+
const read = await unwrap(() => driver.a11yEvents(since), "read announcements", signal());
|
|
6935
|
+
const announced = read.events.filter((e) => (e.kind === "announcement" || e.kind === "live-region") && e.text !== undefined);
|
|
6936
|
+
return {
|
|
6937
|
+
announcements: announced.map((e) => ({ kind: e.kind, text: e.text, at: e.at, source: e.source })),
|
|
6938
|
+
cursor: read.cursor,
|
|
6939
|
+
...read.lost === true ? { lost: true } : {}
|
|
6940
|
+
};
|
|
6941
|
+
};
|
|
6942
|
+
const reader = (action) => {
|
|
6943
|
+
if (!canDriveScreenReader(driver))
|
|
6944
|
+
throw unsupported(action, TALKBACK_HINT);
|
|
6945
|
+
return driver;
|
|
6946
|
+
};
|
|
6947
|
+
const spoken = (el) => el === undefined ? undefined : announce(el, platform);
|
|
6948
|
+
const screenReader = {
|
|
6949
|
+
async order() {
|
|
6950
|
+
return readingOrder(await readSnapshot(), platform);
|
|
6951
|
+
},
|
|
6952
|
+
async state() {
|
|
6953
|
+
const d = reader("device.screenReader.state");
|
|
6954
|
+
return unwrap(() => d.screenReaderState(), "screen reader state", signal());
|
|
6955
|
+
},
|
|
6956
|
+
async next() {
|
|
6957
|
+
const d = reader("device.screenReader.next");
|
|
6958
|
+
return spoken(await unwrap(() => d.screenReaderNext(), "screen reader next", signal()));
|
|
6959
|
+
},
|
|
6960
|
+
async prev() {
|
|
6961
|
+
const d = reader("device.screenReader.prev");
|
|
6962
|
+
return spoken(await unwrap(() => d.screenReaderPrev(), "screen reader prev", signal()));
|
|
6963
|
+
},
|
|
6964
|
+
async activate() {
|
|
6965
|
+
const d = reader("device.screenReader.activate");
|
|
6966
|
+
await unwrap(() => d.screenReaderActivate(), "screen reader activate", signal());
|
|
6967
|
+
},
|
|
6968
|
+
async focused() {
|
|
6969
|
+
const d = reader("device.screenReader.focused");
|
|
6970
|
+
return spoken(await unwrap(() => d.screenReaderFocused(), "screen reader focused", signal()));
|
|
6971
|
+
},
|
|
6972
|
+
async spoken(options = {}) {
|
|
6973
|
+
const d = reader("device.screenReader.spoken");
|
|
6974
|
+
const read = await unwrap(() => d.screenReaderSpoken(options.since), "screen reader spoken", signal());
|
|
6975
|
+
return { lines: read.lines, cursor: read.cursor };
|
|
6976
|
+
}
|
|
6977
|
+
};
|
|
6978
|
+
const target = {
|
|
6979
|
+
audit: (options) => audit(options),
|
|
6980
|
+
waitForAnnouncement(match, options) {
|
|
6981
|
+
return waitForAnnouncement(driver, match, {
|
|
6982
|
+
timeoutMs: options.timeoutMs,
|
|
6983
|
+
...options.since === undefined ? {} : { since: options.since }
|
|
6984
|
+
});
|
|
6985
|
+
},
|
|
6986
|
+
async a11yScreen() {
|
|
6987
|
+
const taken = await settled(() => driver.snapshot(), "snapshot", signal());
|
|
6988
|
+
if (!taken.success)
|
|
6989
|
+
return;
|
|
6990
|
+
const merged = await withA11yRead(driver, taken.data);
|
|
6991
|
+
return renderA11yScreen(merged.success ? merged.data : taken.data);
|
|
6992
|
+
},
|
|
6993
|
+
assertionTimeout: () => env.assertionTimeoutMs,
|
|
6994
|
+
step: (label, fn) => deps.step === undefined ? fn() : deps.step(label, fn)
|
|
6995
|
+
};
|
|
6996
|
+
return { a11yAudit: audit, screenReader, announcements, target };
|
|
6997
|
+
}
|
|
6998
|
+
|
|
6999
|
+
// ../../packages/e2e/src/driver/ios/device/capabilities.ts
|
|
7000
|
+
var IOS_DEVICE_CAPABILITIES = {
|
|
7001
|
+
pinch: false,
|
|
7002
|
+
maxPointers: 0,
|
|
7003
|
+
unicodeText: true,
|
|
7004
|
+
nativeSettle: false,
|
|
7005
|
+
alerts: true,
|
|
7006
|
+
reachability: true,
|
|
7007
|
+
cheapHitTest: false
|
|
7008
|
+
};
|
|
7009
|
+
|
|
7010
|
+
// ../../packages/e2e/src/conditions/device-conditions.ts
|
|
7011
|
+
import { dirname as dirname5, resolve as resolve3 } from "path";
|
|
7012
|
+
|
|
7013
|
+
// ../../packages/core/src/conditions/camera-media.ts
|
|
7014
|
+
import { createHash } from "crypto";
|
|
7015
|
+
import { closeSync, copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, openSync, readFileSync, readSync, rmSync } from "fs";
|
|
7016
|
+
import { join as join3 } from "path";
|
|
7017
|
+
var SUPPORTED = "png, jpg, heic, mp4, mov";
|
|
7018
|
+
var FTYP_BRANDS = {
|
|
7019
|
+
heic: { kind: "image", ext: "heic" },
|
|
7020
|
+
heix: { kind: "image", ext: "heic" },
|
|
7021
|
+
mif1: { kind: "image", ext: "heic" },
|
|
7022
|
+
"qt ": { kind: "video", ext: "mov" },
|
|
7023
|
+
isom: { kind: "video", ext: "mp4" },
|
|
7024
|
+
iso2: { kind: "video", ext: "mp4" },
|
|
7025
|
+
mp41: { kind: "video", ext: "mp4" },
|
|
7026
|
+
mp42: { kind: "video", ext: "mp4" },
|
|
7027
|
+
avc1: { kind: "video", ext: "mp4" },
|
|
7028
|
+
M4V: { kind: "video", ext: "mp4" },
|
|
7029
|
+
"M4V ": { kind: "video", ext: "mp4" }
|
|
7030
|
+
};
|
|
7031
|
+
function head2(path, n) {
|
|
7032
|
+
const fd = openSync(path, "r");
|
|
7033
|
+
try {
|
|
7034
|
+
const buf = Buffer.alloc(n);
|
|
7035
|
+
const read = readSync(fd, buf, 0, n, 0);
|
|
7036
|
+
return buf.subarray(0, read);
|
|
7037
|
+
} finally {
|
|
7038
|
+
closeSync(fd);
|
|
7039
|
+
}
|
|
7040
|
+
}
|
|
7041
|
+
function sniffCameraMedia(path) {
|
|
7042
|
+
if (!existsSync3(path)) {
|
|
7043
|
+
return { success: false, error: { code: "E_CAMERA_NOT_FOUND", message: `camera media not found: ${path}` } };
|
|
7044
|
+
}
|
|
7045
|
+
const b = head2(path, 12);
|
|
7046
|
+
if (b.length >= 8 && b.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
|
|
7047
|
+
return ok({ kind: "image", ext: "png" });
|
|
7048
|
+
}
|
|
7049
|
+
if (b.length >= 3 && b[0] === 255 && b[1] === 216 && b[2] === 255)
|
|
7050
|
+
return ok({ kind: "image", ext: "jpg" });
|
|
7051
|
+
if (b.length >= 12 && b.subarray(4, 8).toString("latin1") === "ftyp") {
|
|
7052
|
+
const brand = FTYP_BRANDS[b.subarray(8, 12).toString("latin1")];
|
|
7053
|
+
if (brand)
|
|
7054
|
+
return ok(brand);
|
|
7055
|
+
}
|
|
7056
|
+
return {
|
|
7057
|
+
success: false,
|
|
7058
|
+
error: { code: "E_CAMERA_MEDIA", message: `unsupported camera media ${path}: use ${SUPPORTED}` }
|
|
7059
|
+
};
|
|
7060
|
+
}
|
|
7061
|
+
function cameraFeedDir(wardenHome, udid) {
|
|
7062
|
+
return join3(wardenHome, "camera", udid);
|
|
7063
|
+
}
|
|
7064
|
+
function stageCameraFeed(input) {
|
|
7065
|
+
const sniffed = sniffCameraMedia(input.source);
|
|
7066
|
+
if (!sniffed.success)
|
|
7067
|
+
return sniffed;
|
|
7068
|
+
const hash = createHash("sha256").update(readFileSync(input.source)).digest("hex").slice(0, 12);
|
|
7069
|
+
const dir = cameraFeedDir(input.wardenHome, input.udid);
|
|
7070
|
+
mkdirSync3(dir, { recursive: true });
|
|
7071
|
+
const path = join3(dir, `${input.facing}-${hash}.${sniffed.data.ext}`);
|
|
7072
|
+
if (!existsSync3(path))
|
|
7073
|
+
copyFileSync(input.source, path);
|
|
7074
|
+
return ok(sniffed.data.kind === "image" ? { kind: "image", path } : { kind: "video", path, loop: input.loop });
|
|
7075
|
+
}
|
|
7076
|
+
function clearCameraFeeds(wardenHome, udid) {
|
|
7077
|
+
rmSync(cameraFeedDir(wardenHome, udid), { recursive: true, force: true });
|
|
7078
|
+
}
|
|
7079
|
+
|
|
7080
|
+
// ../../packages/core/src/store.ts
|
|
7081
|
+
import { Database } from "bun:sqlite";
|
|
7082
|
+
import { randomUUID } from "crypto";
|
|
7083
|
+
import { mkdirSync as mkdirSync4 } from "fs";
|
|
7084
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
7085
|
+
|
|
7086
|
+
// ../../packages/core/src/lease-events.ts
|
|
7087
|
+
var LEASE_EVENT_TYPES = [
|
|
7088
|
+
"requested",
|
|
7089
|
+
"queued",
|
|
7090
|
+
"granted",
|
|
7091
|
+
"heartbeat_lost",
|
|
7092
|
+
"released",
|
|
7093
|
+
"expired",
|
|
7094
|
+
"evicted",
|
|
7095
|
+
"quarantined",
|
|
7096
|
+
"unquarantined",
|
|
7097
|
+
"shutdown",
|
|
7098
|
+
"device_unhealthy",
|
|
7099
|
+
"device_recovered"
|
|
7100
|
+
];
|
|
7101
|
+
function insertLeaseEvent(db, event) {
|
|
7102
|
+
db.query(`INSERT INTO lease_events (at, type, lease_id, resource_key, owner_key, label, claim_id, data)
|
|
7103
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(event.at, event.type, event.leaseId ?? null, event.resourceKey ?? null, event.ownerKey ?? null, event.label ?? null, event.claimId ?? null, JSON.stringify(event.data));
|
|
7104
|
+
}
|
|
7105
|
+
function isEventType(value) {
|
|
7106
|
+
return LEASE_EVENT_TYPES.includes(value);
|
|
7107
|
+
}
|
|
7108
|
+
function toEvent(row) {
|
|
7109
|
+
if (!isEventType(row.type))
|
|
7110
|
+
return;
|
|
7111
|
+
const base = {
|
|
7112
|
+
id: row.id,
|
|
7113
|
+
at: row.at,
|
|
7114
|
+
type: row.type,
|
|
7115
|
+
data: JSON.parse(row.data),
|
|
7116
|
+
...row.lease_id !== null ? { leaseId: row.lease_id } : {},
|
|
7117
|
+
...row.resource_key !== null ? { resourceKey: row.resource_key } : {},
|
|
7118
|
+
...row.owner_key !== null ? { ownerKey: row.owner_key } : {},
|
|
7119
|
+
...row.label !== null ? { label: row.label } : {},
|
|
7120
|
+
...row.claim_id !== null ? { claimId: row.claim_id } : {}
|
|
7121
|
+
};
|
|
7122
|
+
return base;
|
|
7123
|
+
}
|
|
7124
|
+
function queryLeaseEvents(db, filter = {}) {
|
|
7125
|
+
const where = [];
|
|
7126
|
+
const params = [];
|
|
7127
|
+
if (filter.since !== undefined) {
|
|
7128
|
+
where.push("at >= ?");
|
|
7129
|
+
params.push(filter.since);
|
|
7130
|
+
}
|
|
7131
|
+
if (filter.leaseId !== undefined) {
|
|
7132
|
+
where.push("lease_id = ?");
|
|
7133
|
+
params.push(filter.leaseId);
|
|
7134
|
+
}
|
|
7135
|
+
if (filter.owner !== undefined) {
|
|
7136
|
+
where.push("instr(owner_key, ?) > 0");
|
|
7137
|
+
params.push(filter.owner);
|
|
7138
|
+
}
|
|
7139
|
+
if (filter.afterId !== undefined) {
|
|
7140
|
+
where.push("id > ?");
|
|
7141
|
+
params.push(filter.afterId);
|
|
7142
|
+
}
|
|
7143
|
+
const sql = `SELECT * FROM lease_events${where.length > 0 ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY id`;
|
|
7144
|
+
return db.query(sql).all(...params).flatMap((row) => toEvent(row) ?? []);
|
|
7145
|
+
}
|
|
7146
|
+
function pruneLeaseEvents(db, before) {
|
|
7147
|
+
return db.query("DELETE FROM lease_events WHERE at < ?").run(before).changes;
|
|
7148
|
+
}
|
|
7149
|
+
var INTERNAL_LEASE_LABELS = ["capacity-evict", "gc-shutdown", "reclaiming"];
|
|
7150
|
+
|
|
7151
|
+
// ../../packages/core/src/liveness.ts
|
|
7152
|
+
var LOST_HEARTBEATS = 3;
|
|
7153
|
+
function isLeaseAlive(lease, now, pidAlive) {
|
|
7154
|
+
const supervised = lease.pid !== undefined && lease.heartbeatMs !== undefined;
|
|
7155
|
+
const fresh = supervised ? Math.min(lease.ttlMs, LOST_HEARTBEATS * (lease.heartbeatMs ?? 0)) : lease.ttlMs;
|
|
7156
|
+
if (now - lease.heartbeatAt <= fresh)
|
|
7157
|
+
return true;
|
|
7158
|
+
return lease.pid !== undefined && pidAlive(lease.pid);
|
|
7159
|
+
}
|
|
7160
|
+
function processAlive(pid) {
|
|
7161
|
+
try {
|
|
7162
|
+
process.kill(pid, 0);
|
|
7163
|
+
return true;
|
|
7164
|
+
} catch (error) {
|
|
7165
|
+
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
7166
|
+
}
|
|
7167
|
+
}
|
|
7168
|
+
|
|
7169
|
+
// ../../packages/core/src/queue.ts
|
|
7170
|
+
var AGING_MS = 10 * 60000;
|
|
7171
|
+
var DEVICE_WAIT_REASONS = ["device-limit", "requester-limit", "waiting-for-device"];
|
|
7172
|
+
var WAIT_REASONS = new Set([
|
|
7173
|
+
"behind-earlier",
|
|
7174
|
+
"ram-budget",
|
|
7175
|
+
"cpu-budget",
|
|
7176
|
+
"memory-pressure",
|
|
7177
|
+
"host-load",
|
|
7178
|
+
...DEVICE_WAIT_REASONS
|
|
7179
|
+
]);
|
|
7180
|
+
var isWaitReason = (s) => WAIT_REASONS.has(s);
|
|
7181
|
+
|
|
7182
|
+
// ../../packages/core/src/types.ts
|
|
7183
|
+
function resourceKey(resource) {
|
|
7184
|
+
switch (resource.kind) {
|
|
7185
|
+
case "device":
|
|
7186
|
+
return `device:${resource.platform}:${resource.id}`;
|
|
7187
|
+
case "port":
|
|
7188
|
+
return `port:${resource.port}`;
|
|
7189
|
+
case "build":
|
|
7190
|
+
return `build:${resource.key}`;
|
|
7191
|
+
}
|
|
7192
|
+
}
|
|
7193
|
+
function describeOwner(owner) {
|
|
7194
|
+
switch (owner.kind) {
|
|
7195
|
+
case "agent":
|
|
7196
|
+
return `agent ${owner.sessionId}`;
|
|
7197
|
+
case "user":
|
|
7198
|
+
return `user pid ${owner.pid}`;
|
|
7199
|
+
case "ci":
|
|
7200
|
+
return `ci ${owner.runId}`;
|
|
7201
|
+
}
|
|
7202
|
+
}
|
|
7203
|
+
var PHYSICAL_IOS_ID = /^(?:[0-9A-F]{8}-[0-9A-F]{16}|[0-9a-f]{40})$/i;
|
|
7204
|
+
function isPhysicalIosId(id) {
|
|
7205
|
+
return PHYSICAL_IOS_ID.test(id);
|
|
7206
|
+
}
|
|
7207
|
+
|
|
7208
|
+
// ../../packages/core/src/store.ts
|
|
7209
|
+
function wardenHome(env = process.env) {
|
|
7210
|
+
if (env.WARDEN_HOME)
|
|
7211
|
+
return env.WARDEN_HOME;
|
|
7212
|
+
return join4(env.HOME ?? "~", ".warden");
|
|
7213
|
+
}
|
|
7214
|
+
function defaultDbPath(env = process.env) {
|
|
7215
|
+
return join4(wardenHome(env), "warden.db");
|
|
7216
|
+
}
|
|
7217
|
+
var MIGRATIONS = [
|
|
7218
|
+
`CREATE TABLE leases (
|
|
7219
|
+
id TEXT PRIMARY KEY,
|
|
7220
|
+
resource_key TEXT NOT NULL UNIQUE,
|
|
7221
|
+
resource TEXT NOT NULL,
|
|
7222
|
+
owner_key TEXT NOT NULL,
|
|
7223
|
+
owner TEXT NOT NULL,
|
|
7224
|
+
label TEXT,
|
|
7225
|
+
acquired_at INTEGER NOT NULL,
|
|
7226
|
+
heartbeat_at INTEGER NOT NULL,
|
|
7227
|
+
ttl_ms INTEGER NOT NULL,
|
|
7228
|
+
pid INTEGER
|
|
7229
|
+
);
|
|
7230
|
+
CREATE INDEX leases_owner ON leases(owner_key);
|
|
7231
|
+
CREATE TABLE devices (
|
|
7232
|
+
platform TEXT NOT NULL,
|
|
7233
|
+
id TEXT NOT NULL,
|
|
7234
|
+
name TEXT NOT NULL,
|
|
7235
|
+
profile TEXT,
|
|
7236
|
+
runtime TEXT,
|
|
7237
|
+
created_at INTEGER NOT NULL,
|
|
7238
|
+
last_used_at INTEGER NOT NULL,
|
|
7239
|
+
PRIMARY KEY (platform, id)
|
|
7240
|
+
);`,
|
|
7241
|
+
`CREATE TABLE builds (
|
|
7242
|
+
project_key TEXT NOT NULL,
|
|
7243
|
+
platform TEXT NOT NULL,
|
|
7244
|
+
profile TEXT NOT NULL,
|
|
7245
|
+
hash TEXT NOT NULL,
|
|
7246
|
+
path TEXT NOT NULL,
|
|
7247
|
+
source TEXT NOT NULL,
|
|
7248
|
+
size INTEGER NOT NULL,
|
|
7249
|
+
created_at INTEGER NOT NULL,
|
|
7250
|
+
last_used_at INTEGER NOT NULL,
|
|
7251
|
+
PRIMARY KEY (project_key, platform, hash)
|
|
7252
|
+
);
|
|
7253
|
+
CREATE TABLE installs (
|
|
7254
|
+
platform TEXT NOT NULL,
|
|
7255
|
+
device_id TEXT NOT NULL,
|
|
7256
|
+
bundle_id TEXT NOT NULL,
|
|
7257
|
+
hash TEXT NOT NULL,
|
|
7258
|
+
installed_at INTEGER NOT NULL,
|
|
7259
|
+
PRIMARY KEY (platform, device_id, bundle_id)
|
|
7260
|
+
);`,
|
|
7261
|
+
`ALTER TABLE leases ADD COLUMN booted_by_owner INTEGER;
|
|
7262
|
+
CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);`,
|
|
7263
|
+
`ALTER TABLE devices ADD COLUMN quarantined_at INTEGER;
|
|
7264
|
+
ALTER TABLE devices ADD COLUMN quarantine_reason TEXT;
|
|
7265
|
+
ALTER TABLE devices ADD COLUMN slimmed_at INTEGER;`,
|
|
7266
|
+
`ALTER TABLE leases ADD COLUMN heartbeat_ms INTEGER;`,
|
|
7267
|
+
`CREATE TABLE fingerprints (
|
|
7268
|
+
root TEXT NOT NULL,
|
|
7269
|
+
platform TEXT NOT NULL,
|
|
7270
|
+
variant TEXT NOT NULL,
|
|
7271
|
+
command TEXT NOT NULL,
|
|
7272
|
+
signature TEXT NOT NULL,
|
|
7273
|
+
native TEXT NOT NULL,
|
|
7274
|
+
computed_at INTEGER NOT NULL,
|
|
7275
|
+
PRIMARY KEY (root, platform, variant, command)
|
|
7276
|
+
);`,
|
|
7277
|
+
`ALTER TABLE installs ADD COLUMN container TEXT;
|
|
7278
|
+
ALTER TABLE installs ADD COLUMN exe_stamp TEXT;`,
|
|
7279
|
+
`CREATE TABLE boot_queue (
|
|
7280
|
+
id TEXT PRIMARY KEY,
|
|
7281
|
+
platform TEXT NOT NULL,
|
|
7282
|
+
profile TEXT NOT NULL,
|
|
7283
|
+
units REAL NOT NULL,
|
|
7284
|
+
priority INTEGER NOT NULL,
|
|
7285
|
+
enqueued_at INTEGER NOT NULL,
|
|
7286
|
+
heartbeat_at INTEGER NOT NULL,
|
|
7287
|
+
owner_key TEXT NOT NULL,
|
|
7288
|
+
owner TEXT NOT NULL,
|
|
7289
|
+
label TEXT,
|
|
7290
|
+
pid INTEGER
|
|
7291
|
+
);
|
|
7292
|
+
CREATE INDEX boot_queue_order ON boot_queue(priority, enqueued_at);`,
|
|
7293
|
+
`ALTER TABLE leases ADD COLUMN shared INTEGER;`,
|
|
7294
|
+
`ALTER TABLE builds ADD COLUMN kind TEXT NOT NULL DEFAULT 'legacy';
|
|
7295
|
+
ALTER TABLE builds ADD COLUMN base_hash TEXT;
|
|
7296
|
+
ALTER TABLE builds ADD COLUMN js_key TEXT;
|
|
7297
|
+
ALTER TABLE builds ADD COLUMN worktree TEXT;
|
|
7298
|
+
ALTER TABLE builds ADD COLUMN branch TEXT;`,
|
|
7299
|
+
`CREATE TABLE lease_conditions (
|
|
7300
|
+
lease_id TEXT NOT NULL REFERENCES leases(id) ON DELETE CASCADE,
|
|
7301
|
+
area TEXT NOT NULL,
|
|
7302
|
+
json TEXT NOT NULL,
|
|
7303
|
+
owned TEXT NOT NULL DEFAULT '[]',
|
|
7304
|
+
set_at INTEGER NOT NULL,
|
|
7305
|
+
PRIMARY KEY (lease_id, area)
|
|
7306
|
+
);`,
|
|
7307
|
+
`CREATE TABLE orphan_conditions (
|
|
7308
|
+
platform TEXT NOT NULL,
|
|
7309
|
+
device_id TEXT NOT NULL,
|
|
7310
|
+
area TEXT NOT NULL,
|
|
7311
|
+
lease_id TEXT NOT NULL,
|
|
7312
|
+
json TEXT NOT NULL,
|
|
7313
|
+
owned TEXT NOT NULL DEFAULT '[]',
|
|
7314
|
+
set_at INTEGER NOT NULL,
|
|
7315
|
+
orphaned_at INTEGER NOT NULL,
|
|
7316
|
+
PRIMARY KEY (platform, device_id, area)
|
|
7317
|
+
);
|
|
7318
|
+
CREATE TRIGGER leases_orphan_conditions BEFORE DELETE ON leases
|
|
7319
|
+
WHEN json_extract(OLD.resource, '$.kind') = 'device' AND json_extract(OLD.resource, '$.id') NOT LIKE 'pending:%'
|
|
7320
|
+
BEGIN
|
|
7321
|
+
INSERT OR REPLACE INTO orphan_conditions (platform, device_id, area, lease_id, json, owned, set_at, orphaned_at)
|
|
7322
|
+
SELECT json_extract(OLD.resource, '$.platform'), json_extract(OLD.resource, '$.id'), c.area, OLD.id, c.json, c.owned, c.set_at,
|
|
7323
|
+
CAST(strftime('%s', 'now') AS INTEGER) * 1000
|
|
7324
|
+
FROM lease_conditions c WHERE c.lease_id = OLD.id;
|
|
7325
|
+
END;`,
|
|
7326
|
+
`ALTER TABLE devices ADD COLUMN persistent_at INTEGER;`,
|
|
7327
|
+
`ALTER TABLE boot_queue ADD COLUMN reason TEXT;`,
|
|
7328
|
+
`ALTER TABLE leases ADD COLUMN claim_id TEXT;
|
|
7329
|
+
ALTER TABLE boot_queue ADD COLUMN claim_id TEXT;`,
|
|
7330
|
+
`CREATE TABLE lease_events (
|
|
7331
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
7332
|
+
at INTEGER NOT NULL,
|
|
7333
|
+
type TEXT NOT NULL,
|
|
7334
|
+
lease_id TEXT,
|
|
7335
|
+
resource_key TEXT,
|
|
7336
|
+
owner_key TEXT,
|
|
7337
|
+
label TEXT,
|
|
7338
|
+
claim_id TEXT,
|
|
7339
|
+
data TEXT NOT NULL DEFAULT '{}'
|
|
7340
|
+
);
|
|
7341
|
+
CREATE INDEX lease_events_at ON lease_events(at);
|
|
7342
|
+
CREATE INDEX lease_events_lease ON lease_events(lease_id);
|
|
7343
|
+
CREATE INDEX lease_events_owner ON lease_events(owner_key, at);
|
|
7344
|
+
CREATE INDEX lease_events_claim ON lease_events(claim_id);`,
|
|
7345
|
+
`CREATE TABLE claim_keys (
|
|
7346
|
+
owner_key TEXT NOT NULL,
|
|
7347
|
+
key TEXT NOT NULL,
|
|
7348
|
+
claim_id TEXT NOT NULL,
|
|
7349
|
+
platform TEXT NOT NULL,
|
|
7350
|
+
profile TEXT NOT NULL,
|
|
7351
|
+
count INTEGER NOT NULL,
|
|
7352
|
+
ready INTEGER NOT NULL DEFAULT 0,
|
|
7353
|
+
created_at INTEGER NOT NULL,
|
|
7354
|
+
PRIMARY KEY (owner_key, key)
|
|
7355
|
+
);
|
|
7356
|
+
CREATE INDEX claim_keys_claim ON claim_keys(claim_id);`
|
|
7357
|
+
];
|
|
7358
|
+
var QUEUE_TTL_MS = 60000;
|
|
7359
|
+
function ownerKey(owner) {
|
|
7360
|
+
switch (owner.kind) {
|
|
7361
|
+
case "agent":
|
|
7362
|
+
return `agent:${owner.sessionId}`;
|
|
7363
|
+
case "user":
|
|
7364
|
+
return `user:${owner.pid}`;
|
|
7365
|
+
case "ci":
|
|
7366
|
+
return `ci:${owner.runId}`;
|
|
7367
|
+
}
|
|
7368
|
+
}
|
|
7369
|
+
function toQueueEntry(row) {
|
|
7370
|
+
const entry = {
|
|
7371
|
+
id: row.id,
|
|
7372
|
+
platform: row.platform,
|
|
7373
|
+
profile: row.profile,
|
|
7374
|
+
units: row.units,
|
|
7375
|
+
priority: row.priority,
|
|
7376
|
+
enqueuedAt: row.enqueued_at,
|
|
7377
|
+
heartbeatAt: row.heartbeat_at,
|
|
7378
|
+
owner: JSON.parse(row.owner)
|
|
7379
|
+
};
|
|
7380
|
+
if (row.label !== null)
|
|
7381
|
+
entry.label = row.label;
|
|
7382
|
+
if (row.pid !== null)
|
|
7383
|
+
entry.pid = row.pid;
|
|
7384
|
+
if (row.reason !== null && isWaitReason(row.reason))
|
|
7385
|
+
entry.reason = row.reason;
|
|
7386
|
+
if (row.claim_id !== null)
|
|
7387
|
+
entry.claimId = row.claim_id;
|
|
7388
|
+
return entry;
|
|
7389
|
+
}
|
|
7390
|
+
function toLease(row) {
|
|
7391
|
+
const lease = {
|
|
7392
|
+
id: row.id,
|
|
7393
|
+
resource: JSON.parse(row.resource),
|
|
7394
|
+
owner: JSON.parse(row.owner),
|
|
7395
|
+
acquiredAt: row.acquired_at,
|
|
7396
|
+
heartbeatAt: row.heartbeat_at,
|
|
7397
|
+
ttlMs: row.ttl_ms
|
|
7398
|
+
};
|
|
7399
|
+
if (row.label !== null)
|
|
7400
|
+
lease.label = row.label;
|
|
7401
|
+
if (row.pid !== null)
|
|
7402
|
+
lease.pid = row.pid;
|
|
7403
|
+
if (row.booted_by_owner)
|
|
7404
|
+
lease.bootedByOwner = true;
|
|
7405
|
+
if (row.heartbeat_ms !== null)
|
|
7406
|
+
lease.heartbeatMs = row.heartbeat_ms;
|
|
7407
|
+
if (row.shared)
|
|
7408
|
+
lease.shared = true;
|
|
7409
|
+
if (row.claim_id !== null)
|
|
7410
|
+
lease.claimId = row.claim_id;
|
|
7411
|
+
return lease;
|
|
7412
|
+
}
|
|
7413
|
+
function toDevice(row) {
|
|
7414
|
+
const device = {
|
|
7415
|
+
platform: row.platform,
|
|
7416
|
+
id: row.id,
|
|
7417
|
+
name: row.name,
|
|
7418
|
+
createdAt: row.created_at,
|
|
7419
|
+
lastUsedAt: row.last_used_at
|
|
7420
|
+
};
|
|
7421
|
+
if (row.profile !== null)
|
|
7422
|
+
device.profile = row.profile;
|
|
7423
|
+
if (row.runtime !== null)
|
|
7424
|
+
device.runtime = row.runtime;
|
|
7425
|
+
if (row.quarantined_at !== null)
|
|
7426
|
+
device.quarantinedAt = row.quarantined_at;
|
|
7427
|
+
if (row.quarantine_reason !== null)
|
|
7428
|
+
device.quarantineReason = row.quarantine_reason;
|
|
7429
|
+
if (row.slimmed_at !== null)
|
|
7430
|
+
device.slimmedAt = row.slimmed_at;
|
|
7431
|
+
if (row.persistent_at !== null)
|
|
7432
|
+
device.persistentAt = row.persistent_at;
|
|
7433
|
+
return device;
|
|
7434
|
+
}
|
|
7435
|
+
function newLeaseId() {
|
|
7436
|
+
return `l_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
|
|
7437
|
+
}
|
|
7438
|
+
|
|
7439
|
+
class Store {
|
|
7440
|
+
db;
|
|
7441
|
+
constructor(db) {
|
|
7442
|
+
this.db = db;
|
|
7443
|
+
}
|
|
7444
|
+
close() {
|
|
7445
|
+
this.db.close();
|
|
7446
|
+
}
|
|
7447
|
+
transaction(fn) {
|
|
7448
|
+
if (this.db.inTransaction)
|
|
7449
|
+
return fn();
|
|
7450
|
+
return this.db.transaction(fn).immediate();
|
|
7451
|
+
}
|
|
7452
|
+
insertLease(input, now) {
|
|
7453
|
+
const lease = {
|
|
7454
|
+
id: newLeaseId(),
|
|
7455
|
+
resource: input.resource,
|
|
7456
|
+
owner: input.owner,
|
|
7457
|
+
acquiredAt: now,
|
|
7458
|
+
heartbeatAt: now,
|
|
7459
|
+
ttlMs: input.ttlMs
|
|
7460
|
+
};
|
|
7461
|
+
if (input.label !== undefined)
|
|
7462
|
+
lease.label = input.label;
|
|
7463
|
+
if (input.pid !== undefined)
|
|
7464
|
+
lease.pid = input.pid;
|
|
7465
|
+
if (input.bootedByOwner)
|
|
7466
|
+
lease.bootedByOwner = true;
|
|
7467
|
+
if (input.shared)
|
|
7468
|
+
lease.shared = true;
|
|
7469
|
+
if (input.claimId !== undefined)
|
|
7470
|
+
lease.claimId = input.claimId;
|
|
7471
|
+
this.db.query(`INSERT INTO leases (id, resource_key, resource, owner_key, owner, label, acquired_at, heartbeat_at, ttl_ms, pid, booted_by_owner, shared, claim_id)
|
|
7472
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(lease.id, resourceKey(lease.resource), JSON.stringify(lease.resource), ownerKey(lease.owner), JSON.stringify(lease.owner), lease.label ?? null, now, now, lease.ttlMs, lease.pid ?? null, lease.bootedByOwner ? 1 : null, lease.shared ? 1 : null, lease.claimId ?? null);
|
|
7473
|
+
return lease;
|
|
7474
|
+
}
|
|
7475
|
+
markShared(ids) {
|
|
7476
|
+
if (ids.length === 0)
|
|
7477
|
+
return;
|
|
7478
|
+
const stmt = this.db.query("UPDATE leases SET shared = 1 WHERE id = ?");
|
|
7479
|
+
this.transaction(() => {
|
|
7480
|
+
for (const id of ids)
|
|
7481
|
+
stmt.run(id);
|
|
7482
|
+
});
|
|
7483
|
+
}
|
|
7484
|
+
markBootedByOwner(id) {
|
|
7485
|
+
this.db.query("UPDATE leases SET booted_by_owner = 1 WHERE id = ?").run(id);
|
|
7486
|
+
}
|
|
7487
|
+
setLeasePid(id, pid) {
|
|
7488
|
+
this.db.query("UPDATE leases SET pid = ? WHERE id = ?").run(pid, id);
|
|
7489
|
+
}
|
|
7490
|
+
getMeta(key) {
|
|
7491
|
+
return this.db.query("SELECT value FROM meta WHERE key = ?").get(key)?.value;
|
|
7492
|
+
}
|
|
7493
|
+
setMeta(key, value) {
|
|
7494
|
+
this.db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value").run(key, value);
|
|
7495
|
+
}
|
|
7496
|
+
deleteMeta(key) {
|
|
7497
|
+
this.db.query("DELETE FROM meta WHERE key = ?").run(key);
|
|
7498
|
+
}
|
|
7499
|
+
getLease(id) {
|
|
7500
|
+
const row = this.db.query("SELECT * FROM leases WHERE id = ?").get(id);
|
|
7501
|
+
return row ? toLease(row) : undefined;
|
|
7502
|
+
}
|
|
7503
|
+
listLeases() {
|
|
7504
|
+
return this.db.query("SELECT * FROM leases ORDER BY acquired_at, id").all().map(toLease);
|
|
7505
|
+
}
|
|
7506
|
+
listLeasesByOwner(owner) {
|
|
7507
|
+
return this.db.query("SELECT * FROM leases WHERE owner_key = ? ORDER BY acquired_at, id").all(ownerKey(owner)).map(toLease);
|
|
7508
|
+
}
|
|
7509
|
+
findLeaseByResource(resource) {
|
|
7510
|
+
const row = this.db.query("SELECT * FROM leases WHERE resource_key = ?").get(resourceKey(resource));
|
|
7511
|
+
return row ? toLease(row) : undefined;
|
|
7512
|
+
}
|
|
7513
|
+
deleteLeases(ids, end) {
|
|
7514
|
+
if (ids.length === 0)
|
|
7515
|
+
return 0;
|
|
7516
|
+
const stmt = this.db.query("DELETE FROM leases WHERE id = ?");
|
|
7517
|
+
return this.transaction(() => ids.reduce((n, id) => {
|
|
7518
|
+
const lease = end ? this.getLease(id) : undefined;
|
|
7519
|
+
const changes = stmt.run(id).changes;
|
|
7520
|
+
if (changes > 0 && lease && end)
|
|
7521
|
+
this.recordLeaseEnd(lease, end);
|
|
7522
|
+
return n + changes;
|
|
7523
|
+
}, 0));
|
|
7524
|
+
}
|
|
7525
|
+
recordLeaseEnd(lease, end) {
|
|
7526
|
+
if (lease.resource.kind !== "device" || lease.resource.id.startsWith("pending:"))
|
|
7527
|
+
return;
|
|
7528
|
+
if (lease.label !== undefined && INTERNAL_LEASE_LABELS.includes(lease.label))
|
|
7529
|
+
return;
|
|
7530
|
+
const granted = this.db.query("SELECT at FROM lease_events WHERE lease_id = ? AND type = 'granted' LIMIT 1").get(lease.id);
|
|
7531
|
+
if (!granted)
|
|
7532
|
+
return;
|
|
7533
|
+
this.recordEvent({
|
|
7534
|
+
at: end.now,
|
|
7535
|
+
type: end.type,
|
|
7536
|
+
data: { heldMs: Math.max(0, end.now - granted.at) },
|
|
7537
|
+
leaseId: lease.id,
|
|
7538
|
+
resourceKey: resourceKey(lease.resource),
|
|
7539
|
+
ownerKey: ownerKey(lease.owner),
|
|
7540
|
+
...lease.label !== undefined ? { label: lease.label } : {},
|
|
7541
|
+
...lease.claimId !== undefined ? { claimId: lease.claimId } : {}
|
|
7542
|
+
});
|
|
7543
|
+
}
|
|
7544
|
+
recordEvent(event) {
|
|
7545
|
+
insertLeaseEvent(this.db, event);
|
|
7546
|
+
}
|
|
7547
|
+
recordQueuedOnce(event) {
|
|
7548
|
+
return this.transaction(() => {
|
|
7549
|
+
const seen = this.db.query("SELECT 1 AS n FROM lease_events WHERE type = 'queued' AND claim_id IS ?1 AND json_extract(data, '$.queueId') = ?2 LIMIT 1").get(event.claimId ?? null, event.data.queueId);
|
|
7550
|
+
if (seen)
|
|
7551
|
+
return false;
|
|
7552
|
+
this.recordEvent(event);
|
|
7553
|
+
return true;
|
|
7554
|
+
});
|
|
7555
|
+
}
|
|
7556
|
+
recordHeartbeatLost(ids, now) {
|
|
7557
|
+
return this.transaction(() => {
|
|
7558
|
+
let logged = 0;
|
|
7559
|
+
for (const id of ids) {
|
|
7560
|
+
const event = this.getLease(id) ? undefined : this.heartbeatLostEvent(id, now);
|
|
7561
|
+
if (!event)
|
|
7562
|
+
continue;
|
|
7563
|
+
this.recordEvent(event);
|
|
7564
|
+
logged++;
|
|
7565
|
+
}
|
|
7566
|
+
return logged;
|
|
7567
|
+
});
|
|
7568
|
+
}
|
|
7569
|
+
heartbeatLostEvent(leaseId, at) {
|
|
7570
|
+
const granted = this.db.query("SELECT resource_key, owner_key, label, claim_id FROM lease_events WHERE lease_id = ? AND type = 'granted' LIMIT 1").get(leaseId);
|
|
7571
|
+
if (!granted)
|
|
7572
|
+
return;
|
|
7573
|
+
return {
|
|
7574
|
+
at,
|
|
7575
|
+
type: "heartbeat_lost",
|
|
7576
|
+
data: {},
|
|
7577
|
+
leaseId,
|
|
7578
|
+
...granted.resource_key ? { resourceKey: granted.resource_key } : {},
|
|
7579
|
+
...granted.owner_key ? { ownerKey: granted.owner_key } : {},
|
|
7580
|
+
...granted.label ? { label: granted.label } : {},
|
|
7581
|
+
...granted.claim_id ? { claimId: granted.claim_id } : {}
|
|
7582
|
+
};
|
|
7583
|
+
}
|
|
7584
|
+
listEvents(filter = {}) {
|
|
7585
|
+
return queryLeaseEvents(this.db, filter);
|
|
7586
|
+
}
|
|
7587
|
+
pruneEvents(before) {
|
|
7588
|
+
return pruneLeaseEvents(this.db, before);
|
|
7589
|
+
}
|
|
7590
|
+
heartbeat(ids, now, periodMs) {
|
|
7591
|
+
if (ids.length === 0)
|
|
7592
|
+
return 0;
|
|
7593
|
+
const stmt = this.db.query("UPDATE leases SET heartbeat_at = ?, heartbeat_ms = COALESCE(?, heartbeat_ms) WHERE id = ?");
|
|
7594
|
+
return this.transaction(() => ids.reduce((n, id) => n + stmt.run(now, periodMs ?? null, id).changes, 0));
|
|
7595
|
+
}
|
|
7596
|
+
reclaimStale(now, pidAlive) {
|
|
7597
|
+
return this.transaction(() => {
|
|
7598
|
+
const stale = this.listLeases().filter((l) => !isLeaseAlive(l, now, pidAlive)).map((l) => l.id);
|
|
7599
|
+
this.deleteLeases(stale, { type: "expired", now });
|
|
7600
|
+
return stale;
|
|
7601
|
+
});
|
|
7602
|
+
}
|
|
7603
|
+
enqueue(entry) {
|
|
7604
|
+
return this.transaction(() => {
|
|
7605
|
+
this.db.query(`INSERT INTO boot_queue (id, platform, profile, units, priority, enqueued_at, heartbeat_at, owner_key, owner, label, pid, reason, claim_id)
|
|
7606
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
7607
|
+
ON CONFLICT (id) DO UPDATE SET platform = excluded.platform, profile = excluded.profile, units = excluded.units,
|
|
7608
|
+
heartbeat_at = excluded.heartbeat_at, label = excluded.label, pid = excluded.pid, reason = excluded.reason, claim_id = excluded.claim_id`).run(entry.id, entry.platform, entry.profile, entry.units, entry.priority, entry.enqueuedAt, entry.heartbeatAt, ownerKey(entry.owner), JSON.stringify(entry.owner), entry.label ?? null, entry.pid ?? null, entry.reason ?? null, entry.claimId ?? null);
|
|
7609
|
+
const row = this.db.query("SELECT * FROM boot_queue WHERE id = ?").get(entry.id);
|
|
7610
|
+
if (!row)
|
|
7611
|
+
throw new Error(`boot_queue row ${entry.id} vanished`);
|
|
7612
|
+
return toQueueEntry(row);
|
|
7613
|
+
});
|
|
7614
|
+
}
|
|
7615
|
+
refreshQueue(id, now, reason) {
|
|
7616
|
+
return this.db.query("UPDATE boot_queue SET heartbeat_at = ?, reason = COALESCE(?, reason) WHERE id = ?").run(now, reason ?? null, id).changes > 0;
|
|
7617
|
+
}
|
|
7618
|
+
getClaimKey(ownerKey, key) {
|
|
7619
|
+
const row = this.db.query("SELECT * FROM claim_keys WHERE owner_key = ? AND key = ?").get(ownerKey, key);
|
|
7620
|
+
if (!row)
|
|
7621
|
+
return;
|
|
7622
|
+
return {
|
|
7623
|
+
ownerKey: row.owner_key,
|
|
7624
|
+
key: row.key,
|
|
7625
|
+
claimId: row.claim_id,
|
|
7626
|
+
platform: row.platform,
|
|
7627
|
+
profile: row.profile,
|
|
7628
|
+
count: row.count,
|
|
7629
|
+
ready: row.ready === 1
|
|
7630
|
+
};
|
|
7631
|
+
}
|
|
7632
|
+
setClaimKey(entry, now) {
|
|
7633
|
+
this.db.query("INSERT OR REPLACE INTO claim_keys (owner_key, key, claim_id, platform, profile, count, ready, created_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?)").run(entry.ownerKey, entry.key, entry.claimId, entry.platform, entry.profile, entry.count, now);
|
|
7634
|
+
}
|
|
7635
|
+
markClaimKeyReady(claimId) {
|
|
7636
|
+
this.db.query("UPDATE claim_keys SET ready = 1 WHERE claim_id = ?").run(claimId);
|
|
7637
|
+
}
|
|
7638
|
+
deleteClaimKey(ownerKey, key) {
|
|
7639
|
+
this.db.query("DELETE FROM claim_keys WHERE owner_key = ? AND key = ?").run(ownerKey, key);
|
|
7640
|
+
}
|
|
7641
|
+
deleteClaimKeysOf(claimId) {
|
|
7642
|
+
this.db.query("DELETE FROM claim_keys WHERE claim_id = ?").run(claimId);
|
|
7643
|
+
}
|
|
7644
|
+
dequeue(id) {
|
|
7645
|
+
return this.db.query("DELETE FROM boot_queue WHERE id = ?").run(id).changes > 0;
|
|
7646
|
+
}
|
|
7647
|
+
listQueue() {
|
|
7648
|
+
return this.db.query("SELECT * FROM boot_queue ORDER BY enqueued_at, id").all().map(toQueueEntry);
|
|
7649
|
+
}
|
|
7650
|
+
reclaimStaleQueue(now, pidAlive) {
|
|
7651
|
+
return this.transaction(() => {
|
|
7652
|
+
const stale = this.listQueue().filter((e) => now - e.heartbeatAt > QUEUE_TTL_MS || e.pid !== undefined && !pidAlive(e.pid)).map((e) => e.id);
|
|
7653
|
+
const stmt = this.db.query("DELETE FROM boot_queue WHERE id = ?");
|
|
7654
|
+
for (const id of stale)
|
|
7655
|
+
stmt.run(id);
|
|
7656
|
+
return stale;
|
|
7657
|
+
});
|
|
7658
|
+
}
|
|
7659
|
+
recordDevice(device, now) {
|
|
7660
|
+
this.db.query(`INSERT INTO devices (platform, id, name, profile, runtime, created_at, last_used_at) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
7661
|
+
ON CONFLICT (platform, id) DO UPDATE SET name = excluded.name, profile = excluded.profile, runtime = excluded.runtime, last_used_at = excluded.last_used_at`).run(device.platform, device.id, device.name, device.profile ?? null, device.runtime ?? null, now, now);
|
|
7662
|
+
}
|
|
7663
|
+
listDevices(platform) {
|
|
7664
|
+
const rows = platform === undefined ? this.db.query("SELECT * FROM devices ORDER BY platform, name").all() : this.db.query("SELECT * FROM devices WHERE platform = ? ORDER BY name").all(platform);
|
|
7665
|
+
return rows.map(toDevice);
|
|
7666
|
+
}
|
|
7667
|
+
touchDevice(platform, id, now) {
|
|
7668
|
+
this.db.query("UPDATE devices SET last_used_at = ? WHERE platform = ? AND id = ?").run(now, platform, id);
|
|
7669
|
+
}
|
|
7670
|
+
quarantineDevice(device, reason, now) {
|
|
7671
|
+
this.transaction(() => {
|
|
7672
|
+
this.db.query(`INSERT INTO devices (platform, id, name, profile, runtime, created_at, last_used_at, quarantined_at, quarantine_reason)
|
|
7673
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
7674
|
+
ON CONFLICT (platform, id) DO UPDATE SET quarantined_at = excluded.quarantined_at, quarantine_reason = excluded.quarantine_reason`).run(device.platform, device.id, device.name, device.profile ?? null, device.runtime ?? null, now, now, now, reason ?? null);
|
|
7675
|
+
this.recordEvent({
|
|
7676
|
+
at: now,
|
|
7677
|
+
type: "quarantined",
|
|
7678
|
+
data: {
|
|
7679
|
+
platform: device.platform,
|
|
7680
|
+
deviceId: device.id,
|
|
7681
|
+
deviceName: device.name,
|
|
7682
|
+
...reason !== undefined ? { reason } : {}
|
|
7683
|
+
},
|
|
7684
|
+
resourceKey: `device:${device.platform}:${device.id}`
|
|
7685
|
+
});
|
|
7686
|
+
});
|
|
7687
|
+
}
|
|
7688
|
+
unquarantineDevice(id, now) {
|
|
7689
|
+
return this.transaction(() => {
|
|
7690
|
+
const cleared = this.db.query("SELECT platform FROM devices WHERE id = ? AND quarantined_at IS NOT NULL").all(id);
|
|
7691
|
+
this.db.query("UPDATE devices SET quarantined_at = NULL, quarantine_reason = NULL WHERE id = ? AND quarantined_at IS NOT NULL").run(id);
|
|
7692
|
+
for (const { platform } of cleared) {
|
|
7693
|
+
this.recordEvent({
|
|
7694
|
+
at: now,
|
|
7695
|
+
type: "unquarantined",
|
|
7696
|
+
data: { platform, deviceId: id },
|
|
7697
|
+
resourceKey: `device:${platform}:${id}`
|
|
7698
|
+
});
|
|
7699
|
+
}
|
|
7700
|
+
return cleared.length;
|
|
7701
|
+
});
|
|
7702
|
+
}
|
|
7703
|
+
setSlimmed(platform, id, at) {
|
|
7704
|
+
this.db.query("UPDATE devices SET slimmed_at = ? WHERE platform = ? AND id = ?").run(at ?? null, platform, id);
|
|
7705
|
+
}
|
|
7706
|
+
markPersistent(platform, id, now) {
|
|
7707
|
+
this.db.query("UPDATE devices SET persistent_at = ? WHERE platform = ? AND id = ?").run(now, platform, id);
|
|
7708
|
+
}
|
|
7709
|
+
forgetDevice(platform, id) {
|
|
7710
|
+
this.db.query("DELETE FROM devices WHERE platform = ? AND id = ?").run(platform, id);
|
|
7711
|
+
}
|
|
7712
|
+
gcWork(now, idleMs, pidAlive) {
|
|
7713
|
+
const staleLeases = this.listLeases().filter((l) => !isLeaseAlive(l, now, pidAlive)).length;
|
|
7714
|
+
const lastGc = Number(this.getMeta("last_gc_at") ?? 0);
|
|
7715
|
+
const row = this.db.query(`SELECT COUNT(*) AS n FROM devices d
|
|
7716
|
+
WHERE d.last_used_at <= ?1 - ?2 AND d.last_used_at + ?2 > ?3
|
|
7717
|
+
AND NOT EXISTS (SELECT 1 FROM leases l WHERE l.resource_key = 'device:' || d.platform || ':' || d.id)`).get(now, idleMs, lastGc);
|
|
7718
|
+
return { staleLeases, idleDevices: row?.n ?? 0 };
|
|
7719
|
+
}
|
|
7720
|
+
}
|
|
7721
|
+
function migrate(db) {
|
|
7722
|
+
const row = db.query("PRAGMA user_version").get();
|
|
7723
|
+
const current = row?.user_version ?? 0;
|
|
7724
|
+
if (current >= MIGRATIONS.length)
|
|
7725
|
+
return;
|
|
7726
|
+
db.transaction(() => {
|
|
7727
|
+
const again = db.query("PRAGMA user_version").get()?.user_version ?? 0;
|
|
7728
|
+
for (let v = again;v < MIGRATIONS.length; v++) {
|
|
7729
|
+
const sql = MIGRATIONS[v];
|
|
7730
|
+
if (sql !== undefined)
|
|
7731
|
+
db.exec(sql);
|
|
7732
|
+
}
|
|
7733
|
+
db.exec(`PRAGMA user_version = ${MIGRATIONS.length}`);
|
|
7734
|
+
}).immediate();
|
|
7735
|
+
}
|
|
7736
|
+
function openStore(path = defaultDbPath()) {
|
|
7737
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
7738
|
+
const db = new Database(path, { create: true, strict: true });
|
|
7739
|
+
db.exec("PRAGMA busy_timeout = 15000");
|
|
7740
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
7741
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
7742
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
7743
|
+
migrate(db);
|
|
7744
|
+
return new Store(db);
|
|
7745
|
+
}
|
|
7746
|
+
|
|
7747
|
+
// ../../packages/e2e/src/runner/callsite.ts
|
|
7748
|
+
import { join as join5 } from "path";
|
|
7749
|
+
var OWN_FILES = new Set(["callsite.ts", "scope.ts", "test.ts", "attempt.ts"].map((name) => join5(import.meta.dir, name)).concat(join5(import.meta.dir, "..", "index.ts")));
|
|
7750
|
+
var FRAME = /(?:\(|\s)((?:file:\/\/)?\/[^\s()]+?):\d+(?::\d+)?\)?\s*$/;
|
|
7751
|
+
function callerFile(stack = new Error().stack, extraOwn = () => false) {
|
|
7752
|
+
for (const line of (stack ?? "").split(`
|
|
7753
|
+
`)) {
|
|
7754
|
+
const raw = FRAME.exec(line)?.[1];
|
|
7755
|
+
if (raw === undefined)
|
|
7756
|
+
continue;
|
|
7757
|
+
const path = raw.startsWith("file://") ? raw.slice("file://".length) : raw;
|
|
7758
|
+
if (OWN_FILES.has(path) || extraOwn(path) || path.includes("/node_modules/"))
|
|
7759
|
+
continue;
|
|
7760
|
+
return path;
|
|
7761
|
+
}
|
|
7762
|
+
return "unknown";
|
|
7763
|
+
}
|
|
7764
|
+
|
|
7765
|
+
// ../../packages/e2e/src/conditions/condition-events.ts
|
|
7766
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5 } from "fs";
|
|
7767
|
+
import { dirname as dirname4 } from "path";
|
|
7768
|
+
function openConditionSink(path) {
|
|
7769
|
+
let ready = false;
|
|
7770
|
+
return (event) => {
|
|
7771
|
+
if (!ready) {
|
|
7772
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
7773
|
+
ready = true;
|
|
7774
|
+
}
|
|
7775
|
+
appendFileSync2(path, `${JSON.stringify(event)}
|
|
7776
|
+
`);
|
|
7777
|
+
};
|
|
7778
|
+
}
|
|
7779
|
+
var KNOWN_AREAS = new Set([
|
|
7780
|
+
"network",
|
|
7781
|
+
"location",
|
|
7782
|
+
"clock",
|
|
7783
|
+
"faults",
|
|
7784
|
+
"permission",
|
|
7785
|
+
"locale",
|
|
7786
|
+
"a11y",
|
|
7787
|
+
"battery",
|
|
7788
|
+
"camera"
|
|
7789
|
+
]);
|
|
7790
|
+
|
|
7791
|
+
// ../../packages/e2e/src/conditions/geo.ts
|
|
7792
|
+
var EARTH_RADIUS_M = 6371008.8;
|
|
7793
|
+
var rad = (deg) => deg * Math.PI / 180;
|
|
7794
|
+
var deg = (r) => r * 180 / Math.PI;
|
|
7795
|
+
function haversineM(a, b) {
|
|
7796
|
+
const dLat = rad(b.lat - a.lat);
|
|
7797
|
+
const dLon = rad(b.lon - a.lon);
|
|
7798
|
+
const h = Math.sin(dLat / 2) ** 2 + Math.cos(rad(a.lat)) * Math.cos(rad(b.lat)) * Math.sin(dLon / 2) ** 2;
|
|
7799
|
+
return 2 * EARTH_RADIUS_M * Math.asin(Math.min(1, Math.sqrt(h)));
|
|
7800
|
+
}
|
|
7801
|
+
function interpolate(a, b, f) {
|
|
7802
|
+
if (f <= 0)
|
|
7803
|
+
return a;
|
|
7804
|
+
if (f >= 1)
|
|
7805
|
+
return b;
|
|
7806
|
+
const delta = haversineM(a, b) / EARTH_RADIUS_M;
|
|
7807
|
+
if (delta < 0.000000000001)
|
|
7808
|
+
return a;
|
|
7809
|
+
const sinDelta = Math.sin(delta);
|
|
7810
|
+
const wa = Math.sin((1 - f) * delta) / sinDelta;
|
|
7811
|
+
const wb = Math.sin(f * delta) / sinDelta;
|
|
7812
|
+
const lat1 = rad(a.lat);
|
|
7813
|
+
const lon1 = rad(a.lon);
|
|
7814
|
+
const lat2 = rad(b.lat);
|
|
7815
|
+
const lon2 = rad(b.lon);
|
|
7816
|
+
const x = wa * Math.cos(lat1) * Math.cos(lon1) + wb * Math.cos(lat2) * Math.cos(lon2);
|
|
7817
|
+
const y = wa * Math.cos(lat1) * Math.sin(lon1) + wb * Math.cos(lat2) * Math.sin(lon2);
|
|
7818
|
+
const z = wa * Math.sin(lat1) + wb * Math.sin(lat2);
|
|
7819
|
+
return { lat: deg(Math.atan2(z, Math.hypot(x, y))), lon: deg(Math.atan2(y, x)) };
|
|
7820
|
+
}
|
|
7821
|
+
function routeSteps(points, speedMps, intervalS) {
|
|
7822
|
+
const first = points[0];
|
|
7823
|
+
if (!first)
|
|
7824
|
+
return [];
|
|
7825
|
+
if (points.length === 1)
|
|
7826
|
+
return [first];
|
|
7827
|
+
const stride = Math.max(speedMps * intervalS, 0.01);
|
|
7828
|
+
const steps = [first];
|
|
7829
|
+
let carried = 0;
|
|
7830
|
+
for (let i = 1;i < points.length; i++) {
|
|
7831
|
+
const from = points[i - 1];
|
|
7832
|
+
const to = points[i];
|
|
7833
|
+
const leg = haversineM(from, to);
|
|
7834
|
+
let at = stride - carried;
|
|
7835
|
+
while (at < leg - 0.000001) {
|
|
7836
|
+
steps.push(interpolate(from, to, at / leg));
|
|
7837
|
+
at += stride;
|
|
7838
|
+
}
|
|
7839
|
+
carried = leg - (at - stride);
|
|
7840
|
+
}
|
|
7841
|
+
const last = points[points.length - 1];
|
|
7842
|
+
if (haversineM(steps[steps.length - 1], last) > 0.001)
|
|
7843
|
+
steps.push(last);
|
|
7844
|
+
else
|
|
7845
|
+
steps[steps.length - 1] = last;
|
|
7846
|
+
return steps;
|
|
7847
|
+
}
|
|
7848
|
+
function formatCoord(n) {
|
|
7849
|
+
const fixed = n.toFixed(7);
|
|
7850
|
+
return fixed.includes(".") ? fixed.replace(/\.?0+$/, "") : fixed;
|
|
7851
|
+
}
|
|
7852
|
+
|
|
7853
|
+
// ../../packages/e2e/src/conditions/route-ticker.ts
|
|
7854
|
+
var DEFAULT_SPEED_MPS = 20;
|
|
7855
|
+
var DEFAULT_INTERVAL_S = 1;
|
|
7856
|
+
var MAX_CONSECUTIVE_FAILURES = 3;
|
|
7857
|
+
var realTickerClock = {
|
|
7858
|
+
now: () => Date.now(),
|
|
7859
|
+
sleep: (ms, signal) => new Promise((resolve) => {
|
|
7860
|
+
if (signal.aborted)
|
|
7861
|
+
return resolve();
|
|
7862
|
+
const done = () => {
|
|
7863
|
+
clearTimeout(timer);
|
|
7864
|
+
signal.removeEventListener("abort", done);
|
|
7865
|
+
resolve();
|
|
7866
|
+
};
|
|
7867
|
+
const timer = setTimeout(done, ms);
|
|
7868
|
+
signal.addEventListener("abort", done);
|
|
7869
|
+
})
|
|
7870
|
+
};
|
|
7871
|
+
function startRouteTicker(route, deps) {
|
|
7872
|
+
const controller = new AbortController;
|
|
7873
|
+
const { signal } = controller;
|
|
7874
|
+
const startedAt = deps.now();
|
|
7875
|
+
const intervalMs = (route.intervalS ?? DEFAULT_INTERVAL_S) * 1000;
|
|
7876
|
+
const steps = routeSteps(route.points, route.speedMps ?? DEFAULT_SPEED_MPS, route.intervalS ?? DEFAULT_INTERVAL_S);
|
|
7877
|
+
function* playlist() {
|
|
7878
|
+
do
|
|
7879
|
+
yield* steps;
|
|
7880
|
+
while (route.loop && steps.length > 0);
|
|
7881
|
+
}
|
|
7882
|
+
const total = steps.length;
|
|
7883
|
+
const pace = async (done) => {
|
|
7884
|
+
if (!route.loop && done >= total)
|
|
7885
|
+
return false;
|
|
7886
|
+
await deps.sleep(Math.max(0, startedAt + done * intervalMs - deps.now()), signal);
|
|
7887
|
+
return true;
|
|
7888
|
+
};
|
|
7889
|
+
const run = async () => {
|
|
7890
|
+
const count = { ticks: 0, failures: 0, index: 0 };
|
|
7891
|
+
let lastError = "";
|
|
7892
|
+
const sendOne = async (step) => {
|
|
7893
|
+
const failure = await deps.fix(step).catch((e) => e instanceof Error ? e.message : String(e));
|
|
7894
|
+
if (!failure) {
|
|
7895
|
+
count.failures = 0;
|
|
7896
|
+
count.ticks++;
|
|
7897
|
+
return true;
|
|
7898
|
+
}
|
|
7899
|
+
lastError = failure;
|
|
7900
|
+
count.failures++;
|
|
7901
|
+
return count.failures < MAX_CONSECUTIVE_FAILURES;
|
|
7902
|
+
};
|
|
7903
|
+
for (const step of playlist()) {
|
|
7904
|
+
if (signal.aborted)
|
|
7905
|
+
return { ticks: count.ticks, stopped: "aborted" };
|
|
7906
|
+
if (!await sendOne(step))
|
|
7907
|
+
return { ticks: count.ticks, stopped: "failed", error: lastError };
|
|
7908
|
+
count.index++;
|
|
7909
|
+
if (!await pace(count.index))
|
|
7910
|
+
break;
|
|
7911
|
+
}
|
|
7912
|
+
return { ticks: count.ticks, stopped: signal.aborted ? "aborted" : "finished" };
|
|
7913
|
+
};
|
|
7914
|
+
return { stop: () => controller.abort(), done: run() };
|
|
7915
|
+
}
|
|
7916
|
+
|
|
7917
|
+
// ../../packages/e2e/src/conditions/scheduler.ts
|
|
7918
|
+
var MAX_CONSECUTIVE_FAILURES2 = 3;
|
|
7919
|
+
var ONLINE = { kind: "online" };
|
|
7920
|
+
var OFFLINE = { kind: "offline" };
|
|
7921
|
+
function seededRng(seed) {
|
|
7922
|
+
let a = seed >>> 0;
|
|
7923
|
+
return () => {
|
|
7924
|
+
a = a + 1831565813 >>> 0;
|
|
7925
|
+
let t = a;
|
|
7926
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
7927
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
7928
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
7929
|
+
};
|
|
7930
|
+
}
|
|
7931
|
+
var messageOf = (e) => e instanceof Error ? e.message : String(e);
|
|
7932
|
+
async function attempt(run, what) {
|
|
7933
|
+
if (!run)
|
|
7934
|
+
return `target cannot apply ${what}`;
|
|
7935
|
+
try {
|
|
7936
|
+
const res = await run();
|
|
7937
|
+
return res.success ? undefined : res.error.message;
|
|
7938
|
+
} catch (e) {
|
|
7939
|
+
return messageOf(e);
|
|
7940
|
+
}
|
|
7941
|
+
}
|
|
7942
|
+
function core() {
|
|
7943
|
+
const controller = new AbortController;
|
|
7944
|
+
return { controller, signal: controller.signal, status: { state: "running", transitions: 0 }, transitions: 0 };
|
|
7945
|
+
}
|
|
7946
|
+
function invalid(message) {
|
|
7947
|
+
return {
|
|
7948
|
+
stop: () => {
|
|
7949
|
+
return;
|
|
7950
|
+
},
|
|
7951
|
+
done: Promise.resolve({ transitions: 0, stopped: "failed", error: message }),
|
|
7952
|
+
current: () => ({ state: "failed", transitions: 0, error: message })
|
|
7953
|
+
};
|
|
7954
|
+
}
|
|
7955
|
+
function conclude(state, end, error) {
|
|
7956
|
+
const { transitions } = state;
|
|
7957
|
+
if (error !== undefined) {
|
|
7958
|
+
state.status = { state: "failed", transitions, error };
|
|
7959
|
+
return { transitions, stopped: "failed", error };
|
|
7960
|
+
}
|
|
7961
|
+
state.status = { state: end === "finished" ? "finished" : "stopped", transitions };
|
|
7962
|
+
return { transitions, stopped: end };
|
|
7963
|
+
}
|
|
7964
|
+
function applier(target, deps, state) {
|
|
7965
|
+
const net = target.network?.bind(target);
|
|
7966
|
+
const loc = target.location?.bind(target);
|
|
7967
|
+
const tally = { failures: 0, lastError: "" };
|
|
7968
|
+
const record = async (run, what) => {
|
|
7969
|
+
const failure = await attempt(run, what);
|
|
7970
|
+
if (failure) {
|
|
7971
|
+
tally.failures++;
|
|
7972
|
+
tally.lastError = failure;
|
|
7973
|
+
return false;
|
|
7974
|
+
}
|
|
7975
|
+
tally.failures = 0;
|
|
7976
|
+
state.transitions++;
|
|
7977
|
+
return true;
|
|
7978
|
+
};
|
|
7979
|
+
return {
|
|
7980
|
+
tally,
|
|
7981
|
+
async network(condition) {
|
|
7982
|
+
const landed = await record(net && (() => net(condition)), "network");
|
|
7983
|
+
if (landed)
|
|
7984
|
+
deps.sink?.({ type: "condition", area: "network", value: condition, at: deps.now() });
|
|
7985
|
+
return landed;
|
|
7986
|
+
},
|
|
7987
|
+
async location(op) {
|
|
7988
|
+
const landed = await record(loc && (() => loc(op)), "location");
|
|
7989
|
+
if (landed)
|
|
7990
|
+
deps.sink?.({ type: "condition", area: "location", value: op, at: deps.now() });
|
|
7991
|
+
return landed;
|
|
7992
|
+
}
|
|
7993
|
+
};
|
|
7994
|
+
}
|
|
7995
|
+
function startFlap(target, options, deps) {
|
|
7996
|
+
if (!(options.upMs > 0 && options.downMs > 0 && Number.isFinite(options.upMs + options.downMs))) {
|
|
7997
|
+
return invalid("flap needs upMs and downMs above 0");
|
|
7998
|
+
}
|
|
7999
|
+
if (options.base?.kind === "flap")
|
|
8000
|
+
return invalid("flap base cannot itself be a flap");
|
|
8001
|
+
const base = options.base ?? ONLINE;
|
|
8002
|
+
const rng = deps.rng ?? seededRng(options.seed ?? 1);
|
|
8003
|
+
const jitter = (options.jitterPct ?? 0) / 100;
|
|
8004
|
+
const state = core();
|
|
8005
|
+
const { signal } = state;
|
|
8006
|
+
const apply = applier(target, deps, state);
|
|
8007
|
+
const stretch = (ms) => Math.max(1, ms * (1 + (rng() * 2 - 1) * jitter));
|
|
8008
|
+
const runPhase = async (phase, skipApply, deadline) => {
|
|
8009
|
+
const landed = skipApply || await apply.network(phase === "up" ? base : OFFLINE);
|
|
8010
|
+
state.status = { state: "running", transitions: state.transitions, phase };
|
|
8011
|
+
const next = deadline + stretch(phase === "up" ? options.upMs : options.downMs);
|
|
8012
|
+
await deps.sleep(Math.max(0, next - deps.now()), signal);
|
|
8013
|
+
return [landed && phase === "up", next];
|
|
8014
|
+
};
|
|
8015
|
+
const run = async () => {
|
|
8016
|
+
let phase = "up";
|
|
8017
|
+
let atBase = false;
|
|
8018
|
+
let deadline = deps.now();
|
|
8019
|
+
let skipApply = options.applyInitial === false;
|
|
8020
|
+
while (!signal.aborted && apply.tally.failures < MAX_CONSECUTIVE_FAILURES2) {
|
|
8021
|
+
[atBase, deadline] = await runPhase(phase, skipApply, deadline);
|
|
8022
|
+
skipApply = false;
|
|
8023
|
+
phase = phase === "up" ? "down" : "up";
|
|
8024
|
+
}
|
|
8025
|
+
const { failures, lastError } = apply.tally;
|
|
8026
|
+
if (!atBase)
|
|
8027
|
+
await apply.network(base);
|
|
8028
|
+
return conclude(state, "aborted", failures >= MAX_CONSECUTIVE_FAILURES2 ? lastError : undefined);
|
|
8029
|
+
};
|
|
8030
|
+
return { stop: () => state.controller.abort(), done: run(), current: () => state.status };
|
|
8031
|
+
}
|
|
8032
|
+
function startTimeline(target, steps, deps) {
|
|
8033
|
+
if (steps.some((s) => !(s.atMs >= 0) || !Number.isFinite(s.atMs)))
|
|
8034
|
+
return invalid("timeline atMs must be 0 or more");
|
|
8035
|
+
const ordered = [...steps].sort((a, b) => a.atMs - b.atMs);
|
|
8036
|
+
const startedAt = deps.now();
|
|
8037
|
+
const state = core();
|
|
8038
|
+
const { signal } = state;
|
|
8039
|
+
const apply = applier(target, deps, state);
|
|
8040
|
+
const runStep = async (step) => {
|
|
8041
|
+
const errors = [];
|
|
8042
|
+
if (step.network && !await apply.network(step.network))
|
|
8043
|
+
errors.push(apply.tally.lastError);
|
|
8044
|
+
if (step.location && !await apply.location(step.location))
|
|
8045
|
+
errors.push(apply.tally.lastError);
|
|
8046
|
+
return errors.length > 0 ? errors.join("; ") : undefined;
|
|
8047
|
+
};
|
|
8048
|
+
const run = async () => {
|
|
8049
|
+
let stepFailures = 0;
|
|
8050
|
+
let lastError = "";
|
|
8051
|
+
for (const [index, step] of ordered.entries()) {
|
|
8052
|
+
await deps.sleep(Math.max(0, startedAt + step.atMs - deps.now()), signal);
|
|
8053
|
+
if (signal.aborted)
|
|
8054
|
+
break;
|
|
8055
|
+
state.status = { state: "running", transitions: state.transitions, step: index };
|
|
8056
|
+
const problem = await runStep(step);
|
|
8057
|
+
stepFailures = problem === undefined ? 0 : stepFailures + 1;
|
|
8058
|
+
lastError = problem ?? lastError;
|
|
8059
|
+
if (stepFailures >= MAX_CONSECUTIVE_FAILURES2)
|
|
8060
|
+
return conclude(state, "aborted", lastError);
|
|
8061
|
+
}
|
|
8062
|
+
return conclude(state, signal.aborted ? "aborted" : "finished");
|
|
8063
|
+
};
|
|
8064
|
+
return { stop: () => state.controller.abort(), done: run(), current: () => state.status };
|
|
8065
|
+
}
|
|
8066
|
+
|
|
8067
|
+
// ../../packages/e2e/src/conditions/device-conditions.ts
|
|
8068
|
+
var ONLINE2 = { kind: "online" };
|
|
8069
|
+
var REAL_CLOCK = { kind: "real" };
|
|
8070
|
+
function a11yOpOf(config) {
|
|
8071
|
+
const { fontScale: _scale, ...op } = config;
|
|
8072
|
+
return op;
|
|
8073
|
+
}
|
|
8074
|
+
var stderrLog = (line) => {
|
|
8075
|
+
process.stderr.write(`${line}
|
|
8076
|
+
`);
|
|
8077
|
+
};
|
|
8078
|
+
function invalid2(message) {
|
|
8079
|
+
return new E2eError2("INVALID_ARGUMENT", message);
|
|
8080
|
+
}
|
|
8081
|
+
function durationMs(value, what) {
|
|
8082
|
+
if (typeof value === "number") {
|
|
8083
|
+
if (!Number.isFinite(value) || value < 0)
|
|
8084
|
+
throw invalid2(`${what} must be 0 or more, got ${String(value)}`);
|
|
8085
|
+
return value;
|
|
8086
|
+
}
|
|
8087
|
+
const parsed = parseDuration(value);
|
|
8088
|
+
if (!parsed.success)
|
|
8089
|
+
throw invalid2(`${what}: ${parsed.error}`);
|
|
8090
|
+
return parsed.data;
|
|
8091
|
+
}
|
|
8092
|
+
function network(input) {
|
|
8093
|
+
const condition = toNetworkCondition(input);
|
|
8094
|
+
if (!condition.success)
|
|
8095
|
+
throw invalid2(condition.error);
|
|
8096
|
+
return condition.data;
|
|
8097
|
+
}
|
|
8098
|
+
function instant(at) {
|
|
8099
|
+
const date = at instanceof Date ? new Date(at.getTime()) : new Date(at);
|
|
8100
|
+
if (Number.isNaN(date.getTime()))
|
|
8101
|
+
throw invalid2(`invalid time ${JSON.stringify(at)} (use an ISO 8601 string, a Date or epoch ms)`);
|
|
8102
|
+
return date;
|
|
8103
|
+
}
|
|
8104
|
+
function validatePoint(point) {
|
|
8105
|
+
const ok = Number.isFinite(point.lat) && Number.isFinite(point.lon) && Math.abs(point.lat) <= 90 && Math.abs(point.lon) <= 180;
|
|
8106
|
+
if (!ok)
|
|
8107
|
+
throw invalid2(`invalid location ${point.lat},${point.lon} (lat within 90, lon within 180)`);
|
|
8108
|
+
}
|
|
8109
|
+
var SCENARIOS = ["city-run", "city-bike", "freeway"];
|
|
8110
|
+
function locationOf(input) {
|
|
8111
|
+
if ("kind" in input)
|
|
8112
|
+
return input;
|
|
8113
|
+
validatePoint(input);
|
|
8114
|
+
return { kind: "point", lat: input.lat, lon: input.lon };
|
|
8115
|
+
}
|
|
8116
|
+
var PACKAGE_SRC = `${resolve3(import.meta.dir, "..")}/`;
|
|
8117
|
+
function callingSpecDir() {
|
|
8118
|
+
const file = callerFile(new Error().stack, (path) => path.startsWith(PACKAGE_SRC) && !path.endsWith(".test.ts"));
|
|
8119
|
+
return file === "unknown" ? undefined : dirname5(file);
|
|
8120
|
+
}
|
|
8121
|
+
var FACINGS = {
|
|
8122
|
+
back: ["back"],
|
|
8123
|
+
front: ["front"],
|
|
8124
|
+
both: ["back", "front"]
|
|
8125
|
+
};
|
|
8126
|
+
function facingsOf(choice) {
|
|
8127
|
+
const facings = FACINGS[choice ?? "back"];
|
|
8128
|
+
if (facings === undefined)
|
|
8129
|
+
throw invalid2(`unknown camera facing "${String(choice)}" (use back, front or both)`);
|
|
8130
|
+
return facings;
|
|
8131
|
+
}
|
|
8132
|
+
var registry2 = new WeakMap;
|
|
8133
|
+
function registerConditions(device, internals) {
|
|
8134
|
+
registry2.set(device, internals);
|
|
8135
|
+
}
|
|
8136
|
+
function conditionsOf(device) {
|
|
8137
|
+
return registry2.get(device);
|
|
8138
|
+
}
|
|
8139
|
+
function conditionSinkFor(session) {
|
|
8140
|
+
return session.results === undefined ? undefined : openConditionSink(session.results.path);
|
|
8141
|
+
}
|
|
8142
|
+
function createDeviceConditions(deps) {
|
|
8143
|
+
const { driver, env } = deps;
|
|
8144
|
+
const ticker = deps.tickerClock ?? realTickerClock;
|
|
8145
|
+
const log = deps.log ?? stderrLog;
|
|
8146
|
+
const normalized = normalizeConditions(env.conditions ?? {});
|
|
8147
|
+
if (!normalized.success)
|
|
8148
|
+
throw new E2eError2("SESSION", `invalid conditions: ${normalized.error}`);
|
|
8149
|
+
const baseline = normalized.data.conditions;
|
|
8150
|
+
let currentNetwork = ONLINE2;
|
|
8151
|
+
let currentFaults = [];
|
|
8152
|
+
let currentClock = REAL_CLOCK;
|
|
8153
|
+
let clockSetAt = ticker.now();
|
|
8154
|
+
let networkDirty = false;
|
|
8155
|
+
let locationDirty = false;
|
|
8156
|
+
let clockDirty = false;
|
|
8157
|
+
let permissionsDirty = false;
|
|
8158
|
+
let localeDirty = false;
|
|
8159
|
+
let a11yDirty = false;
|
|
8160
|
+
let batteryDirty = false;
|
|
8161
|
+
let cameraDirty = false;
|
|
8162
|
+
let currentCamera = {};
|
|
8163
|
+
const permissionStates = new Map;
|
|
8164
|
+
let permissionBundle;
|
|
8165
|
+
let currentLocale = {};
|
|
8166
|
+
let currentA11y = {};
|
|
8167
|
+
let flap;
|
|
8168
|
+
const runs = new Set;
|
|
8169
|
+
const recorder = deps.recorder;
|
|
8170
|
+
const emit = (event) => {
|
|
8171
|
+
deps.sink?.({ ...event, type: "condition", at: ticker.now() });
|
|
8172
|
+
};
|
|
8173
|
+
async function call(what, run) {
|
|
8174
|
+
const result = await settled(run, what, deps.signal());
|
|
8175
|
+
if (result.success)
|
|
8176
|
+
return result.data;
|
|
8177
|
+
const error = toE2eError(result.error, what);
|
|
8178
|
+
if (result.error.code === "E_NETWORK_RELAUNCH") {
|
|
8179
|
+
throw new E2eError2(error.code, `${error.message}
|
|
8180
|
+
the app runs without the warden network dylib: declare \`conditions.network\` for the suite so it launches with it, or pass { relaunch: true }`);
|
|
8181
|
+
}
|
|
8182
|
+
throw error;
|
|
8183
|
+
}
|
|
8184
|
+
const target = (options) => ({
|
|
8185
|
+
network: async (condition) => {
|
|
8186
|
+
const result = await driver.network(condition, options?.relaunch === undefined ? undefined : { relaunch: options.relaunch });
|
|
8187
|
+
if (result.success)
|
|
8188
|
+
recorder?.record("network", condition);
|
|
8189
|
+
return result;
|
|
8190
|
+
},
|
|
8191
|
+
location: async (op) => {
|
|
8192
|
+
const result = await driver.location(op);
|
|
8193
|
+
if (result.success)
|
|
8194
|
+
recorder?.record("location", op);
|
|
8195
|
+
return result;
|
|
8196
|
+
}
|
|
8197
|
+
});
|
|
8198
|
+
function track(scheduler) {
|
|
8199
|
+
const run = {
|
|
8200
|
+
async stop() {
|
|
8201
|
+
scheduler.stop();
|
|
8202
|
+
const outcome = await scheduler.done;
|
|
8203
|
+
runs.delete(run);
|
|
8204
|
+
if (flap?.run === run)
|
|
8205
|
+
flap = undefined;
|
|
8206
|
+
return outcome;
|
|
8207
|
+
},
|
|
8208
|
+
done: scheduler.done,
|
|
8209
|
+
status: () => scheduler.current()
|
|
8210
|
+
};
|
|
8211
|
+
runs.add(run);
|
|
8212
|
+
return run;
|
|
8213
|
+
}
|
|
8214
|
+
async function stopFlap() {
|
|
8215
|
+
const running = flap;
|
|
8216
|
+
if (running === undefined)
|
|
8217
|
+
return;
|
|
8218
|
+
await running.run.stop();
|
|
8219
|
+
}
|
|
8220
|
+
async function applyNetwork(condition, options) {
|
|
8221
|
+
await stopFlap();
|
|
8222
|
+
networkDirty = true;
|
|
8223
|
+
const opts = options?.relaunch === undefined ? undefined : { relaunch: options.relaunch };
|
|
8224
|
+
const state = await call("network", () => driver.network(condition, opts));
|
|
8225
|
+
currentNetwork = condition;
|
|
8226
|
+
recorder?.record("network", condition);
|
|
8227
|
+
emit({ area: "network", value: condition });
|
|
8228
|
+
return state;
|
|
8229
|
+
}
|
|
8230
|
+
async function startFlapRun(options, netOptions) {
|
|
8231
|
+
const upMs = durationMs(options.up, "flap up");
|
|
8232
|
+
const downMs = durationMs(options.down, "flap down");
|
|
8233
|
+
if (!(upMs > 0 && downMs > 0))
|
|
8234
|
+
throw invalid2("flap needs up and down above 0");
|
|
8235
|
+
const base = options.base === undefined ? ONLINE2 : network(options.base);
|
|
8236
|
+
if (base.kind === "flap")
|
|
8237
|
+
throw invalid2("flap base cannot itself be a flap");
|
|
8238
|
+
await applyNetwork(base, netOptions);
|
|
8239
|
+
const scheduler = startFlap(target(netOptions), {
|
|
8240
|
+
upMs,
|
|
8241
|
+
downMs,
|
|
8242
|
+
base,
|
|
8243
|
+
applyInitial: false,
|
|
8244
|
+
...options.jitterPct === undefined ? {} : { jitterPct: options.jitterPct },
|
|
8245
|
+
...options.seed === undefined ? {} : { seed: options.seed }
|
|
8246
|
+
}, { ...ticker, ...deps.sink === undefined ? {} : { sink: deps.sink } });
|
|
8247
|
+
const run = track(scheduler);
|
|
8248
|
+
flap = { run, options };
|
|
8249
|
+
currentNetwork = {
|
|
8250
|
+
kind: "flap",
|
|
8251
|
+
upMs,
|
|
8252
|
+
downMs,
|
|
8253
|
+
base,
|
|
8254
|
+
...options.jitterPct === undefined ? {} : { jitterPct: options.jitterPct }
|
|
8255
|
+
};
|
|
8256
|
+
emit({ area: "network", value: currentNetwork });
|
|
8257
|
+
return run;
|
|
8258
|
+
}
|
|
8259
|
+
async function setNetwork(input, options) {
|
|
8260
|
+
const condition = network(input);
|
|
8261
|
+
if (condition.kind === "flap") {
|
|
8262
|
+
await startFlapRun({
|
|
8263
|
+
up: condition.upMs,
|
|
8264
|
+
down: condition.downMs,
|
|
8265
|
+
...condition.jitterPct === undefined ? {} : { jitterPct: condition.jitterPct },
|
|
8266
|
+
...condition.base === undefined ? {} : { base: condition.base }
|
|
8267
|
+
}, options);
|
|
8268
|
+
return { online: true, condition, needsProxy: [] };
|
|
8269
|
+
}
|
|
8270
|
+
return applyNetwork(condition, options);
|
|
8271
|
+
}
|
|
8272
|
+
async function restoreNetwork(previous, previousFlap, options) {
|
|
8273
|
+
if (previous.kind === "flap" && previousFlap !== undefined)
|
|
8274
|
+
await startFlapRun(previousFlap, options);
|
|
8275
|
+
else
|
|
8276
|
+
await applyNetwork(previous, options);
|
|
8277
|
+
}
|
|
8278
|
+
async function resetNetwork() {
|
|
8279
|
+
await stopFlap();
|
|
8280
|
+
if (currentFaults.length > 0)
|
|
8281
|
+
await setFaults([]);
|
|
8282
|
+
const goal = baseline.network ?? ONLINE2;
|
|
8283
|
+
await applyNetwork(goal);
|
|
8284
|
+
networkDirty = false;
|
|
8285
|
+
recorder?.settle("network", goal, goal.kind === "online");
|
|
8286
|
+
}
|
|
8287
|
+
async function setFaults(rules) {
|
|
8288
|
+
networkDirty = true;
|
|
8289
|
+
const result = await call("network faults", () => driver.networkFaults(rules));
|
|
8290
|
+
currentFaults = rules;
|
|
8291
|
+
recorder?.record("network", currentNetwork);
|
|
8292
|
+
emit({ area: "faults", value: { kind: "faults", rules } });
|
|
8293
|
+
return result;
|
|
8294
|
+
}
|
|
8295
|
+
const networkApi = {
|
|
8296
|
+
set: setNetwork,
|
|
8297
|
+
async during(condition, fn, options) {
|
|
8298
|
+
const previous = currentNetwork;
|
|
8299
|
+
const previousFlap = flap?.options;
|
|
8300
|
+
await setNetwork(condition, options);
|
|
8301
|
+
let result;
|
|
8302
|
+
try {
|
|
8303
|
+
result = await fn();
|
|
8304
|
+
} catch (error) {
|
|
8305
|
+
await restoreNetwork(previous, previousFlap, options).catch((restoreError) => log(`[warden-e2e] network.during: restoring the previous network failed: ${String(restoreError instanceof Error ? restoreError.message : restoreError)}`));
|
|
8306
|
+
throw error;
|
|
8307
|
+
}
|
|
8308
|
+
await restoreNetwork(previous, previousFlap, options);
|
|
8309
|
+
return result;
|
|
8310
|
+
},
|
|
8311
|
+
flap: (options) => startFlapRun(options),
|
|
8312
|
+
fault: (rules) => setFaults("match" in rules ? [rules] : rules),
|
|
8313
|
+
async status() {
|
|
8314
|
+
return { condition: currentNetwork, faults: currentFaults, flapping: flap !== undefined };
|
|
8315
|
+
},
|
|
8316
|
+
reset: resetNetwork
|
|
8317
|
+
};
|
|
8318
|
+
async function applyLocation(op) {
|
|
8319
|
+
locationDirty = true;
|
|
8320
|
+
await call("location", () => driver.location(op));
|
|
8321
|
+
recorder?.record("location", op);
|
|
8322
|
+
emit({ area: "location", value: op });
|
|
8323
|
+
}
|
|
8324
|
+
const locationApi = {
|
|
8325
|
+
async set(point) {
|
|
8326
|
+
validatePoint(point);
|
|
8327
|
+
await applyLocation({
|
|
8328
|
+
kind: "point",
|
|
8329
|
+
lat: point.lat,
|
|
8330
|
+
lon: point.lon,
|
|
8331
|
+
...point.altM === undefined ? {} : { altM: point.altM }
|
|
8332
|
+
});
|
|
8333
|
+
},
|
|
8334
|
+
async route(points, options = {}) {
|
|
8335
|
+
const pace = {
|
|
8336
|
+
...options.speedMps === undefined ? {} : { speedMps: options.speedMps }
|
|
8337
|
+
};
|
|
8338
|
+
if ("gpx" in points) {
|
|
8339
|
+
await applyLocation({ kind: "gpx", path: resolve3(points.gpx), ...pace });
|
|
8340
|
+
return;
|
|
8341
|
+
}
|
|
8342
|
+
if (points.length === 0)
|
|
8343
|
+
throw invalid2("a route needs at least one point");
|
|
8344
|
+
for (const point of points)
|
|
8345
|
+
validatePoint(point);
|
|
8346
|
+
await applyLocation({
|
|
8347
|
+
kind: "route",
|
|
8348
|
+
points,
|
|
8349
|
+
...pace,
|
|
8350
|
+
...options.intervalS === undefined ? {} : { intervalS: options.intervalS },
|
|
8351
|
+
...options.loop === undefined ? {} : { loop: options.loop }
|
|
8352
|
+
});
|
|
8353
|
+
},
|
|
8354
|
+
async scenario(name) {
|
|
8355
|
+
if (!SCENARIOS.includes(name))
|
|
8356
|
+
throw invalid2(`unknown scenario "${String(name)}" (use ${SCENARIOS.join(", ")})`);
|
|
8357
|
+
await applyLocation({ kind: "scenario", name });
|
|
8358
|
+
},
|
|
8359
|
+
clear: () => applyLocation({ kind: "clear" })
|
|
8360
|
+
};
|
|
8361
|
+
async function applyClock(condition) {
|
|
8362
|
+
const op = driver.clock;
|
|
8363
|
+
clockDirty = true;
|
|
8364
|
+
if (typeof op !== "function") {
|
|
8365
|
+
throw toE2eError({ code: "E_UNSUPPORTED", message: "this driver has no clock condition", retryable: false }, "clock");
|
|
8366
|
+
}
|
|
8367
|
+
await call("clock", () => op.call(driver, condition));
|
|
8368
|
+
currentClock = condition;
|
|
8369
|
+
clockSetAt = ticker.now();
|
|
8370
|
+
emit({ area: "clock", value: condition });
|
|
8371
|
+
}
|
|
8372
|
+
function clockFor(at, frozen) {
|
|
8373
|
+
const iso = instant(at).toISOString();
|
|
8374
|
+
return frozen === true ? { kind: "frozen", at: iso } : { kind: "fixed", at: iso };
|
|
8375
|
+
}
|
|
8376
|
+
function advanced(by) {
|
|
8377
|
+
switch (currentClock.kind) {
|
|
8378
|
+
case "real":
|
|
8379
|
+
return { kind: "offset", ms: by };
|
|
8380
|
+
case "offset":
|
|
8381
|
+
return { kind: "offset", ms: currentClock.ms + by };
|
|
8382
|
+
case "frozen":
|
|
8383
|
+
return { kind: "frozen", at: new Date(Date.parse(currentClock.at) + by).toISOString() };
|
|
8384
|
+
case "fixed": {
|
|
8385
|
+
const now = Date.parse(currentClock.at) + (ticker.now() - clockSetAt);
|
|
8386
|
+
return { kind: "fixed", at: new Date(now + by).toISOString() };
|
|
8387
|
+
}
|
|
8388
|
+
}
|
|
8389
|
+
}
|
|
8390
|
+
const clockApi = {
|
|
8391
|
+
async set(at, options) {
|
|
8392
|
+
await applyClock(clockFor(at, options?.frozen));
|
|
8393
|
+
},
|
|
8394
|
+
async advance(by) {
|
|
8395
|
+
await applyClock(advanced(durationMs(by, "advance")));
|
|
8396
|
+
},
|
|
8397
|
+
async during(at, fn, options) {
|
|
8398
|
+
const previous = currentClock;
|
|
8399
|
+
await applyClock(clockFor(at, options?.frozen));
|
|
8400
|
+
let result;
|
|
8401
|
+
try {
|
|
8402
|
+
result = await fn();
|
|
8403
|
+
} catch (error) {
|
|
8404
|
+
await applyClock(previous).catch((restoreError) => log(`[warden-e2e] clock.during: restoring the previous clock failed: ${String(restoreError instanceof Error ? restoreError.message : restoreError)}`));
|
|
8405
|
+
throw error;
|
|
8406
|
+
}
|
|
8407
|
+
await applyClock(previous);
|
|
8408
|
+
return result;
|
|
8409
|
+
},
|
|
8410
|
+
async reset() {
|
|
8411
|
+
await applyClock(baseline.clock ?? REAL_CLOCK);
|
|
8412
|
+
clockDirty = false;
|
|
8413
|
+
},
|
|
8414
|
+
status: () => currentClock
|
|
8415
|
+
};
|
|
8416
|
+
function settingOp(name) {
|
|
8417
|
+
const op = driver[name];
|
|
8418
|
+
if (typeof op !== "function") {
|
|
8419
|
+
throw toE2eError({ code: "E_UNSUPPORTED", message: `this driver has no ${name} op`, retryable: false }, name);
|
|
8420
|
+
}
|
|
8421
|
+
return op.bind(driver);
|
|
8422
|
+
}
|
|
8423
|
+
const recordPermissions = (bundleId) => {
|
|
8424
|
+
const value = {
|
|
8425
|
+
grants: [...permissionStates].map(([permission, state]) => ({ bundleId, permission, state }))
|
|
8426
|
+
};
|
|
8427
|
+
recorder?.record("permission", value);
|
|
8428
|
+
emit({ area: "permission", value });
|
|
8429
|
+
};
|
|
8430
|
+
async function applyPermissions(grants, options) {
|
|
8431
|
+
permissionsDirty = true;
|
|
8432
|
+
const op = settingOp("permissions");
|
|
8433
|
+
const state = await call("permissions", () => op(grants, options ?? { bundleId: env.bundleId }));
|
|
8434
|
+
for (const { permission, state: applied } of state.applied)
|
|
8435
|
+
permissionStates.set(permission, applied);
|
|
8436
|
+
permissionBundle = state.bundleId;
|
|
8437
|
+
recordPermissions(state.bundleId);
|
|
8438
|
+
return state;
|
|
8439
|
+
}
|
|
8440
|
+
const permissionsApi = {
|
|
8441
|
+
set: applyPermissions,
|
|
8442
|
+
async reset(options) {
|
|
8443
|
+
const baselinePermissions = baseline.permissions ?? {};
|
|
8444
|
+
const back = {};
|
|
8445
|
+
for (const name of permissionStates.keys())
|
|
8446
|
+
back[name] = baselinePermissions[name] ?? "unset";
|
|
8447
|
+
if (Object.keys(back).length === 0)
|
|
8448
|
+
return;
|
|
8449
|
+
await applyPermissions(back, options ?? (permissionBundle ? { bundleId: permissionBundle } : undefined));
|
|
8450
|
+
permissionsDirty = false;
|
|
8451
|
+
permissionStates.clear();
|
|
8452
|
+
recorder?.settle("permission", { grants: [] }, true);
|
|
8453
|
+
}
|
|
8454
|
+
};
|
|
8455
|
+
function recordLocale() {
|
|
8456
|
+
const empty = currentLocale.locale === undefined && currentLocale.timezone === undefined;
|
|
8457
|
+
if (empty)
|
|
8458
|
+
recorder?.settle("locale", currentLocale, true);
|
|
8459
|
+
else
|
|
8460
|
+
recorder?.record("locale", currentLocale);
|
|
8461
|
+
emit({ area: "locale", value: currentLocale });
|
|
8462
|
+
}
|
|
8463
|
+
async function applyTimezone(tz) {
|
|
8464
|
+
localeDirty = true;
|
|
8465
|
+
const op = settingOp("timezone");
|
|
8466
|
+
const previous = currentLocale.previousTimezone;
|
|
8467
|
+
const state = await call("timezone", () => op(tz, previous === undefined ? undefined : { previous }));
|
|
8468
|
+
const { timezone: _gone, ...rest } = currentLocale;
|
|
8469
|
+
currentLocale = {
|
|
8470
|
+
...rest,
|
|
8471
|
+
...tz === null ? {} : { timezone: tz },
|
|
8472
|
+
...previous ?? state.previous ? { previousTimezone: previous ?? state.previous } : {}
|
|
8473
|
+
};
|
|
8474
|
+
recordLocale();
|
|
8475
|
+
return state;
|
|
8476
|
+
}
|
|
8477
|
+
async function applyLocale(tag, options) {
|
|
8478
|
+
localeDirty = true;
|
|
8479
|
+
const op = settingOp("locale");
|
|
8480
|
+
const state = await call("locale", () => op(tag, options ?? { bundleId: env.bundleId }));
|
|
8481
|
+
const { locale: _gone, ...rest } = currentLocale;
|
|
8482
|
+
currentLocale = {
|
|
8483
|
+
...rest,
|
|
8484
|
+
...tag === null ? {} : { locale: tag },
|
|
8485
|
+
...state.bundleId === undefined ? {} : { bundleId: state.bundleId }
|
|
8486
|
+
};
|
|
8487
|
+
recordLocale();
|
|
8488
|
+
return state;
|
|
8489
|
+
}
|
|
8490
|
+
function checkedA11y(op) {
|
|
8491
|
+
if (op === null)
|
|
8492
|
+
return;
|
|
8493
|
+
if (op.textSize !== undefined) {
|
|
8494
|
+
const size = resolveTextSize(op.textSize);
|
|
8495
|
+
if (!size.success)
|
|
8496
|
+
throw invalid2(size.error);
|
|
8497
|
+
}
|
|
8498
|
+
if (Object.keys(op).length === 0)
|
|
8499
|
+
throw invalid2("a11y needs at least one of textSize, boldText, contrast, reduceMotion");
|
|
8500
|
+
}
|
|
8501
|
+
async function applyA11y(op) {
|
|
8502
|
+
checkedA11y(op);
|
|
8503
|
+
a11yDirty = true;
|
|
8504
|
+
const run = settingOp("a11y");
|
|
8505
|
+
const state = await call("a11y", () => run(op));
|
|
8506
|
+
const resolved = op?.textSize === undefined ? undefined : resolveTextSize(op.textSize);
|
|
8507
|
+
currentA11y = op === null ? {} : {
|
|
8508
|
+
...currentA11y,
|
|
8509
|
+
...appliedA11y(op, state.applied),
|
|
8510
|
+
...resolved?.success ? { fontScale: resolved.data.scale } : {}
|
|
8511
|
+
};
|
|
8512
|
+
if (op?.screenReader === false && currentA11y.screenReaderPrevious !== undefined) {
|
|
8513
|
+
const { screenReader: _off, screenReaderPrevious: _prior, ...rest } = currentA11y;
|
|
8514
|
+
currentA11y = rest;
|
|
8515
|
+
} else if (state.screenReaderPrevious !== undefined) {
|
|
8516
|
+
currentA11y = { ...currentA11y, screenReaderPrevious: state.screenReaderPrevious };
|
|
8517
|
+
}
|
|
8518
|
+
if (Object.keys(currentA11y).length === 0)
|
|
8519
|
+
recorder?.settle("a11y", currentA11y, true);
|
|
8520
|
+
else
|
|
8521
|
+
recorder?.record("a11y", currentA11y);
|
|
8522
|
+
emit({ area: "a11y", value: currentA11y });
|
|
8523
|
+
return state;
|
|
8524
|
+
}
|
|
8525
|
+
async function applyBattery(op) {
|
|
8526
|
+
batteryDirty = true;
|
|
8527
|
+
const run = settingOp("battery");
|
|
8528
|
+
const state = await call("battery", () => run(op));
|
|
8529
|
+
const value = op === null ? {} : { ...op, cosmetic: state.cosmetic };
|
|
8530
|
+
if (op === null)
|
|
8531
|
+
recorder?.settle("battery", value, true);
|
|
8532
|
+
else
|
|
8533
|
+
recorder?.record("battery", value);
|
|
8534
|
+
emit({ area: "battery", value });
|
|
8535
|
+
return state;
|
|
8536
|
+
}
|
|
8537
|
+
const home = deps.wardenHome ?? wardenHome(process.env);
|
|
8538
|
+
async function applyCamera(next) {
|
|
8539
|
+
cameraDirty = true;
|
|
8540
|
+
const run = settingOp("camera");
|
|
8541
|
+
const empty = Object.keys(next).length === 0;
|
|
8542
|
+
const state = await call("camera", () => run(empty ? null : next));
|
|
8543
|
+
currentCamera = state.feeds;
|
|
8544
|
+
if (Object.keys(currentCamera).length === 0)
|
|
8545
|
+
recorder?.settle("camera", currentCamera, true);
|
|
8546
|
+
else
|
|
8547
|
+
recorder?.record("camera", currentCamera);
|
|
8548
|
+
emit({ area: "camera", value: currentCamera });
|
|
8549
|
+
return state;
|
|
8550
|
+
}
|
|
8551
|
+
function stage(facing, source, loop) {
|
|
8552
|
+
const staged = stageCameraFeed({ wardenHome: home, udid: env.udid, facing, source, loop });
|
|
8553
|
+
if (!staged.success) {
|
|
8554
|
+
throw invalid2(`camera failed: ${staged.error.code}: ${staged.error.message}`);
|
|
8555
|
+
}
|
|
8556
|
+
return staged.data;
|
|
8557
|
+
}
|
|
8558
|
+
function specPath(path, dir) {
|
|
8559
|
+
if (typeof path !== "string" || path === "")
|
|
8560
|
+
throw invalid2("camera.set needs the path of an image or video");
|
|
8561
|
+
return resolve3(dir ?? (env.specsDir !== undefined ? resolve3(env.specsDir) : process.cwd()), path);
|
|
8562
|
+
}
|
|
8563
|
+
const cameraApi = {
|
|
8564
|
+
async set(path, options = {}) {
|
|
8565
|
+
const dir = (deps.specDir ?? callingSpecDir)();
|
|
8566
|
+
const source = specPath(path, dir);
|
|
8567
|
+
const facings = facingsOf(options.facing);
|
|
8568
|
+
const loop = options.loop ?? true;
|
|
8569
|
+
const next = { ...currentCamera };
|
|
8570
|
+
for (const facing of facings) {
|
|
8571
|
+
const feed = stage(facing, source, loop);
|
|
8572
|
+
if (feed !== undefined)
|
|
8573
|
+
next[facing] = feed;
|
|
8574
|
+
}
|
|
8575
|
+
return applyCamera(next);
|
|
8576
|
+
},
|
|
8577
|
+
async clear(facing = "both") {
|
|
8578
|
+
const next = { ...currentCamera };
|
|
8579
|
+
for (const f of facingsOf(facing))
|
|
8580
|
+
delete next[f];
|
|
8581
|
+
return applyCamera(next);
|
|
8582
|
+
},
|
|
8583
|
+
async status() {
|
|
8584
|
+
return { feeds: currentCamera };
|
|
8585
|
+
}
|
|
8586
|
+
};
|
|
8587
|
+
function baselineCamera() {
|
|
8588
|
+
const feeds = {};
|
|
8589
|
+
for (const facing of ["back", "front"]) {
|
|
8590
|
+
const configured = env.cameraFeeds?.[facing];
|
|
8591
|
+
if (configured === undefined)
|
|
8592
|
+
continue;
|
|
8593
|
+
const feed = stage(facing, configured.path, configured.loop);
|
|
8594
|
+
if (feed !== undefined)
|
|
8595
|
+
feeds[facing] = feed;
|
|
8596
|
+
}
|
|
8597
|
+
return feeds;
|
|
8598
|
+
}
|
|
8599
|
+
async function startTimelineRun(steps) {
|
|
8600
|
+
const converted = steps.map((step, index) => {
|
|
8601
|
+
const atMs = durationMs(step.at, `timeline step ${index} at`);
|
|
8602
|
+
const converted = { atMs };
|
|
8603
|
+
if (step.network !== undefined) {
|
|
8604
|
+
const condition = network(step.network);
|
|
8605
|
+
if (condition.kind === "flap")
|
|
8606
|
+
throw invalid2(`timeline step ${index}: use device.network.flap for a flap`);
|
|
8607
|
+
converted.network = condition;
|
|
8608
|
+
}
|
|
8609
|
+
if (step.location !== undefined)
|
|
8610
|
+
converted.location = locationOf(step.location);
|
|
8611
|
+
return converted;
|
|
8612
|
+
});
|
|
8613
|
+
networkDirty = true;
|
|
8614
|
+
locationDirty = true;
|
|
8615
|
+
const scheduler = startTimeline(target(), converted, {
|
|
8616
|
+
...ticker,
|
|
8617
|
+
...deps.sink === undefined ? {} : { sink: deps.sink }
|
|
8618
|
+
});
|
|
8619
|
+
const status = scheduler.current();
|
|
8620
|
+
if (status.state === "failed")
|
|
8621
|
+
throw invalid2(status.error);
|
|
8622
|
+
const run = track(scheduler);
|
|
8623
|
+
const last = converted.at(-1);
|
|
8624
|
+
if (last?.network !== undefined)
|
|
8625
|
+
currentNetwork = last.network;
|
|
8626
|
+
return run;
|
|
8627
|
+
}
|
|
8628
|
+
async function stopRuns() {
|
|
8629
|
+
const failures = [];
|
|
8630
|
+
for (const run of [...runs]) {
|
|
8631
|
+
const outcome = await run.stop();
|
|
8632
|
+
if (outcome.stopped === "failed")
|
|
8633
|
+
failures.push(outcome.error);
|
|
8634
|
+
}
|
|
8635
|
+
if (failures.length > 0) {
|
|
8636
|
+
throw new E2eError2("ACTION_FAILED", `a network flap or timeline stopped on its own after failing: ${failures[0]}`);
|
|
8637
|
+
}
|
|
8638
|
+
}
|
|
8639
|
+
const declared = baseline.network !== undefined || baseline.location !== undefined || baseline.clock !== undefined || baseline.permissions !== undefined || baseline.locale !== undefined || baseline.timezone !== undefined || baseline.a11y !== undefined || env.cameraFeeds !== undefined;
|
|
8640
|
+
const internals = {
|
|
8641
|
+
declared,
|
|
8642
|
+
dirty: () => networkDirty || locationDirty || clockDirty || permissionsDirty || localeDirty || a11yDirty || batteryDirty || cameraDirty || runs.size > 0,
|
|
8643
|
+
wantsInject: () => baseline.network !== undefined || baseline.clock !== undefined || currentNetwork.kind !== "online" || currentClock.kind !== "real",
|
|
8644
|
+
async applyBaseline() {
|
|
8645
|
+
if (baseline.permissions !== undefined) {
|
|
8646
|
+
for (const [name, state] of Object.entries(baseline.permissions))
|
|
8647
|
+
permissionStates.set(name, state);
|
|
8648
|
+
await applyPermissions(baseline.permissions);
|
|
8649
|
+
}
|
|
8650
|
+
if (baseline.timezone !== undefined)
|
|
8651
|
+
await applyTimezone(baseline.timezone);
|
|
8652
|
+
if (baseline.locale !== undefined)
|
|
8653
|
+
await applyLocale(baseline.locale);
|
|
8654
|
+
if (baseline.a11y !== undefined)
|
|
8655
|
+
await applyA11y(a11yOpOf(baseline.a11y));
|
|
8656
|
+
if (baseline.clock !== undefined)
|
|
8657
|
+
await applyClock(baseline.clock);
|
|
8658
|
+
if (baseline.location !== undefined)
|
|
8659
|
+
await applyLocation(baseline.location);
|
|
8660
|
+
if (baseline.network !== undefined)
|
|
8661
|
+
await applyNetwork(baseline.network, { relaunch: true });
|
|
8662
|
+
if (env.cameraFeeds !== undefined)
|
|
8663
|
+
await applyCamera(baselineCamera());
|
|
8664
|
+
cameraDirty = false;
|
|
8665
|
+
networkDirty = false;
|
|
8666
|
+
locationDirty = false;
|
|
8667
|
+
clockDirty = false;
|
|
8668
|
+
permissionsDirty = false;
|
|
8669
|
+
localeDirty = false;
|
|
8670
|
+
a11yDirty = false;
|
|
8671
|
+
batteryDirty = false;
|
|
8672
|
+
},
|
|
8673
|
+
async teardown() {
|
|
8674
|
+
const errors = [];
|
|
8675
|
+
const attempt = async (run) => {
|
|
8676
|
+
try {
|
|
8677
|
+
await run();
|
|
8678
|
+
} catch (error) {
|
|
8679
|
+
errors.push(error);
|
|
8680
|
+
}
|
|
8681
|
+
};
|
|
8682
|
+
await attempt(stopRuns);
|
|
8683
|
+
if (networkDirty)
|
|
8684
|
+
await attempt(resetNetwork);
|
|
8685
|
+
if (locationDirty) {
|
|
8686
|
+
await attempt(async () => {
|
|
8687
|
+
const goal = baseline.location ?? { kind: "clear" };
|
|
8688
|
+
await applyLocation(goal);
|
|
8689
|
+
locationDirty = false;
|
|
8690
|
+
recorder?.settle("location", goal, goal.kind === "clear");
|
|
8691
|
+
});
|
|
8692
|
+
}
|
|
8693
|
+
if (clockDirty)
|
|
8694
|
+
await attempt(clockApi.reset);
|
|
8695
|
+
if (permissionsDirty)
|
|
8696
|
+
await attempt(() => permissionsApi.reset());
|
|
8697
|
+
if (localeDirty) {
|
|
8698
|
+
await attempt(async () => {
|
|
8699
|
+
await applyTimezone(baseline.timezone ?? null);
|
|
8700
|
+
await applyLocale(baseline.locale ?? null);
|
|
8701
|
+
localeDirty = false;
|
|
8702
|
+
});
|
|
8703
|
+
}
|
|
8704
|
+
if (a11yDirty) {
|
|
8705
|
+
await attempt(async () => {
|
|
8706
|
+
await applyA11y(null);
|
|
8707
|
+
if (baseline.a11y !== undefined)
|
|
8708
|
+
await applyA11y(a11yOpOf(baseline.a11y));
|
|
8709
|
+
a11yDirty = false;
|
|
8710
|
+
});
|
|
8711
|
+
}
|
|
8712
|
+
if (batteryDirty) {
|
|
8713
|
+
await attempt(async () => {
|
|
8714
|
+
await applyBattery(null);
|
|
8715
|
+
batteryDirty = false;
|
|
8716
|
+
});
|
|
8717
|
+
}
|
|
8718
|
+
if (cameraDirty) {
|
|
8719
|
+
await attempt(async () => {
|
|
8720
|
+
await applyCamera(baselineCamera());
|
|
8721
|
+
cameraDirty = false;
|
|
8722
|
+
});
|
|
8723
|
+
}
|
|
8724
|
+
if (errors.length > 0)
|
|
8725
|
+
throw errors[0];
|
|
8726
|
+
}
|
|
8727
|
+
};
|
|
8728
|
+
return {
|
|
8729
|
+
network: networkApi,
|
|
8730
|
+
location: locationApi,
|
|
8731
|
+
clock: clockApi,
|
|
8732
|
+
permissions: permissionsApi,
|
|
8733
|
+
timezone: applyTimezone,
|
|
8734
|
+
locale: applyLocale,
|
|
8735
|
+
a11y: applyA11y,
|
|
8736
|
+
battery: applyBattery,
|
|
8737
|
+
camera: cameraApi,
|
|
8738
|
+
memoryWarning: async (options) => {
|
|
8739
|
+
const op = settingOp("memoryWarning");
|
|
8740
|
+
await call("memoryWarning", () => op(options ?? { bundleId: env.bundleId }));
|
|
8741
|
+
},
|
|
8742
|
+
push: async (payload, options) => {
|
|
8743
|
+
const op = settingOp("push");
|
|
8744
|
+
await call("push", () => op(payload, options ?? { bundleId: env.bundleId }));
|
|
8745
|
+
},
|
|
8746
|
+
timeline: startTimelineRun,
|
|
8747
|
+
internals
|
|
8748
|
+
};
|
|
8749
|
+
}
|
|
8750
|
+
|
|
8751
|
+
// ../../packages/e2e/src/perf/device-perf.ts
|
|
8752
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync6, writeFileSync } from "fs";
|
|
8753
|
+
import { join as join6 } from "path";
|
|
8754
|
+
|
|
8755
|
+
// ../../packages/e2e/src/perf/budgets.ts
|
|
8756
|
+
var BUDGET_KEYS = ["startupColdMs", "jankPct", "maxCommitMs", "p95CpuPct", "peakMemoryMb"];
|
|
8757
|
+
function measured(report, metric) {
|
|
8758
|
+
const m = report.metrics;
|
|
8759
|
+
switch (metric) {
|
|
8760
|
+
case "startupColdMs":
|
|
8761
|
+
return m.startupColdMs;
|
|
8762
|
+
case "jankPct":
|
|
8763
|
+
return m.jankPct;
|
|
8764
|
+
case "maxCommitMs":
|
|
8765
|
+
return m.topCommitMs;
|
|
8766
|
+
case "p95CpuPct":
|
|
8767
|
+
return m.cpuP95Pct;
|
|
8768
|
+
case "peakMemoryMb": {
|
|
8769
|
+
const kb = Math.max(m.peakPssKb ?? 0, m.peakRssKb ?? 0);
|
|
8770
|
+
return kb > 0 ? kb / 1024 : undefined;
|
|
8771
|
+
}
|
|
8772
|
+
}
|
|
8773
|
+
}
|
|
8774
|
+
function checkBudgets(report, base, overrides) {
|
|
8775
|
+
const budgets = { ...base, ...overrides };
|
|
8776
|
+
const breaches = [];
|
|
8777
|
+
for (const metric of BUDGET_KEYS) {
|
|
8778
|
+
const budget = budgets[metric];
|
|
8779
|
+
if (budget === undefined)
|
|
8780
|
+
continue;
|
|
8781
|
+
const value = measured(report, metric);
|
|
8782
|
+
if (value !== undefined && value > budget)
|
|
8783
|
+
breaches.push({ metric, value, budget });
|
|
8784
|
+
}
|
|
8785
|
+
return breaches;
|
|
8786
|
+
}
|
|
8787
|
+
var round = (n) => Math.round(n * 10) / 10;
|
|
8788
|
+
function describeBreaches(breaches, reportPath) {
|
|
8789
|
+
const lines = breaches.map((b) => ` ${b.metric}: ${round(b.value)} > budget ${b.budget}`);
|
|
8790
|
+
return [
|
|
8791
|
+
`perf_budget: ${breaches.length} metric${breaches.length === 1 ? "" : "s"} over budget`,
|
|
8792
|
+
...lines,
|
|
8793
|
+
...reportPath === undefined ? [] : [` report: ${reportPath}`]
|
|
8794
|
+
].join(`
|
|
8795
|
+
`);
|
|
8796
|
+
}
|
|
8797
|
+
function parseBudgets(raw) {
|
|
8798
|
+
if (raw === undefined || raw === "")
|
|
8799
|
+
return {};
|
|
8800
|
+
let json;
|
|
8801
|
+
try {
|
|
8802
|
+
json = JSON.parse(raw);
|
|
8803
|
+
} catch {
|
|
8804
|
+
return {};
|
|
8805
|
+
}
|
|
8806
|
+
if (typeof json !== "object" || json === null)
|
|
8807
|
+
return {};
|
|
8808
|
+
const out = {};
|
|
8809
|
+
for (const key of BUDGET_KEYS) {
|
|
8810
|
+
const v = json[key];
|
|
8811
|
+
if (typeof v === "number" && Number.isFinite(v) && v >= 0)
|
|
8812
|
+
out[key] = v;
|
|
8813
|
+
}
|
|
8814
|
+
return out;
|
|
8815
|
+
}
|
|
8816
|
+
|
|
8817
|
+
// ../../packages/e2e/src/perf/device-perf.ts
|
|
8818
|
+
var PERF_DIR = "perf";
|
|
8819
|
+
function parseEnvelope2(stdout) {
|
|
8820
|
+
try {
|
|
8821
|
+
const json = JSON.parse(stdout.trim().split(`
|
|
8822
|
+
`).at(-1) ?? "");
|
|
8823
|
+
if (typeof json !== "object" || json === null || !("ok" in json))
|
|
8824
|
+
return;
|
|
8825
|
+
return json;
|
|
8826
|
+
} catch {
|
|
8827
|
+
return;
|
|
8828
|
+
}
|
|
8829
|
+
}
|
|
8830
|
+
var flagsOf = (o) => [
|
|
8831
|
+
...o.js ? ["--js"] : [],
|
|
8832
|
+
...o.react ? ["--react"] : [],
|
|
8833
|
+
...typeof o.native === "string" ? [`--native=${o.native}`] : o.native ? ["--native"] : [],
|
|
8834
|
+
...o.metrics ? ["--metrics"] : [],
|
|
8835
|
+
...o.frames ? ["--frames"] : [],
|
|
8836
|
+
...o.logs ? ["--logs"] : [],
|
|
8837
|
+
...o.all ? ["--all"] : [],
|
|
8838
|
+
...o.relaunch ? ["--relaunch"] : [],
|
|
8839
|
+
...o.reset ? ["--reset"] : [],
|
|
8840
|
+
...o.maxDurationMs === undefined ? [] : ["--max-duration", `${Math.ceil(o.maxDurationMs / 1000)}s`]
|
|
8841
|
+
];
|
|
8842
|
+
var isRecord2 = (v) => typeof v === "object" && v !== null;
|
|
8843
|
+
function saveReport(deps, result, report, sessionId) {
|
|
8844
|
+
const sessionReport = typeof result.reportPath === "string" ? result.reportPath : undefined;
|
|
8845
|
+
const attempt = deps.attemptDir();
|
|
8846
|
+
if (attempt === undefined)
|
|
8847
|
+
return sessionReport;
|
|
8848
|
+
const out = join6(attempt, PERF_DIR);
|
|
8849
|
+
mkdirSync6(out, { recursive: true });
|
|
8850
|
+
const sessionDir = typeof result.dir === "string" ? result.dir : undefined;
|
|
8851
|
+
const write = (name, from, fallback) => {
|
|
8852
|
+
if (from !== undefined && existsSync4(from))
|
|
8853
|
+
copyFileSync2(from, join6(out, name));
|
|
8854
|
+
else if (fallback !== undefined)
|
|
8855
|
+
writeFileSync(join6(out, name), fallback);
|
|
8856
|
+
else
|
|
8857
|
+
return;
|
|
8858
|
+
deps.addFile?.(`${PERF_DIR}/${name}`);
|
|
8859
|
+
};
|
|
8860
|
+
write("report.json", sessionReport, `${JSON.stringify(report, null, 2)}
|
|
8861
|
+
`);
|
|
8862
|
+
write("report.md", sessionDir === undefined ? undefined : join6(sessionDir, "report.md"));
|
|
8863
|
+
write("session.json", undefined, `${JSON.stringify({ sessionId, dir: sessionDir }, null, 2)}
|
|
8864
|
+
`);
|
|
8865
|
+
return join6(out, "report.json");
|
|
8866
|
+
}
|
|
8867
|
+
function createDevicePerf(deps) {
|
|
8868
|
+
const run = async (sub, extra) => {
|
|
8869
|
+
if (deps.unavailable !== undefined)
|
|
8870
|
+
throw new E2eError2("SESSION", `device.perf unavailable: ${deps.unavailable}`);
|
|
8871
|
+
const { code, stdout, stderr } = await deps.exec(["perf", sub, "--json", "--udid", deps.udid, ...extra]);
|
|
5703
8872
|
const env = parseEnvelope2(stdout);
|
|
5704
8873
|
if (env === undefined || !env.ok) {
|
|
5705
8874
|
const why = env === undefined ? stderr.trim() || `exit ${code}` : `${env.error.code}: ${env.error.message}`;
|
|
5706
8875
|
throw new E2eError2("SESSION", `warden perf ${sub} failed: ${why}`);
|
|
5707
8876
|
}
|
|
5708
|
-
return
|
|
8877
|
+
return isRecord2(env.result) ? env.result : {};
|
|
5709
8878
|
};
|
|
5710
8879
|
let active = false;
|
|
5711
8880
|
return {
|
|
@@ -5727,7 +8896,7 @@ function createDevicePerf(deps) {
|
|
|
5727
8896
|
const result = await run("stop", []);
|
|
5728
8897
|
active = false;
|
|
5729
8898
|
const report = result.report;
|
|
5730
|
-
if (!
|
|
8899
|
+
if (!isRecord2(report))
|
|
5731
8900
|
throw new E2eError2("SESSION", "warden perf stop returned no report");
|
|
5732
8901
|
const sessionId = typeof result.sessionId === "string" ? result.sessionId : "";
|
|
5733
8902
|
const reportPath = saveReport(deps, result, report, sessionId);
|
|
@@ -5764,21 +8933,21 @@ function spawnWarden(bin) {
|
|
|
5764
8933
|
}
|
|
5765
8934
|
|
|
5766
8935
|
// ../../packages/e2e/src/runner/artifacts.ts
|
|
5767
|
-
import { createHash } from "crypto";
|
|
5768
|
-
import { basename, extname, join as
|
|
8936
|
+
import { createHash as createHash2 } from "crypto";
|
|
8937
|
+
import { basename, extname, join as join7, relative, sep } from "path";
|
|
5769
8938
|
var SLUG_MAX = 60;
|
|
5770
8939
|
function slug(text, fallback = "test") {
|
|
5771
8940
|
const slugged = text.normalize("NFKD").replace(/[\u0300-\u036F]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, SLUG_MAX).replace(/-+$/g, "");
|
|
5772
8941
|
return slugged === "" ? fallback : slugged;
|
|
5773
8942
|
}
|
|
5774
8943
|
function hash6(...parts) {
|
|
5775
|
-
return
|
|
8944
|
+
return createHash2("sha1").update(parts.join("\x00")).digest("hex").slice(0, 6);
|
|
5776
8945
|
}
|
|
5777
8946
|
function attemptDir(out, specId, title, hash) {
|
|
5778
|
-
return
|
|
8947
|
+
return join7(out, specId, `${slug(title.join(" "))}-${hash.slice(0, 6)}`);
|
|
5779
8948
|
}
|
|
5780
8949
|
function screenshotPath(dir, n, name) {
|
|
5781
|
-
return
|
|
8950
|
+
return join7(dir, "screenshots", `${String(n).padStart(3, "0")}-${slug(name, "screenshot")}.png`);
|
|
5782
8951
|
}
|
|
5783
8952
|
var SPEC_EXT = /\.e2e$/;
|
|
5784
8953
|
function specIdOf(file, specsDir) {
|
|
@@ -5795,9 +8964,9 @@ function specIdOf(file, specsDir) {
|
|
|
5795
8964
|
}
|
|
5796
8965
|
|
|
5797
8966
|
// ../../packages/e2e/src/devtools/native-log.ts
|
|
5798
|
-
import { copyFileSync as
|
|
8967
|
+
import { copyFileSync as copyFileSync3, mkdirSync as mkdirSync7, readdirSync, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
5799
8968
|
import { homedir } from "os";
|
|
5800
|
-
import { dirname as
|
|
8969
|
+
import { dirname as dirname6, join as join8 } from "path";
|
|
5801
8970
|
var NATIVE_LOG = "native.log";
|
|
5802
8971
|
var DEFAULT_TIMEOUT_MS5 = 1e4;
|
|
5803
8972
|
var SLACK_S = 5;
|
|
@@ -5806,23 +8975,23 @@ function realNativeFs() {
|
|
|
5806
8975
|
return {
|
|
5807
8976
|
listReports: (dir) => {
|
|
5808
8977
|
try {
|
|
5809
|
-
return readdirSync(dir).map((name) => ({ name, mtimeMs: statSync(
|
|
8978
|
+
return readdirSync(dir).map((name) => ({ name, mtimeMs: statSync(join8(dir, name)).mtimeMs }));
|
|
5810
8979
|
} catch {
|
|
5811
8980
|
return [];
|
|
5812
8981
|
}
|
|
5813
8982
|
},
|
|
5814
8983
|
copy: (from, to) => {
|
|
5815
|
-
|
|
5816
|
-
|
|
8984
|
+
mkdirSync7(dirname6(to), { recursive: true });
|
|
8985
|
+
copyFileSync3(from, to);
|
|
5817
8986
|
},
|
|
5818
8987
|
write: (path, text) => {
|
|
5819
|
-
|
|
8988
|
+
mkdirSync7(dirname6(path), { recursive: true });
|
|
5820
8989
|
writeFileSync2(path, text);
|
|
5821
8990
|
}
|
|
5822
8991
|
};
|
|
5823
8992
|
}
|
|
5824
8993
|
function realNativeLogDeps(exec, adb) {
|
|
5825
|
-
return { exec, adb, fs: realNativeFs(), reportsDir:
|
|
8994
|
+
return { exec, adb, fs: realNativeFs(), reportsDir: join8(homedir(), "Library", "Logs", "DiagnosticReports") };
|
|
5826
8995
|
}
|
|
5827
8996
|
function tail(text) {
|
|
5828
8997
|
return text.length > MAX_BYTES ? text.slice(text.length - MAX_BYTES) : text;
|
|
@@ -5838,7 +9007,7 @@ async function iosProcessName(target, exec, timeoutMs) {
|
|
|
5838
9007
|
const app = container.stdout.trim();
|
|
5839
9008
|
if (container.exitCode !== 0 || !app)
|
|
5840
9009
|
return { error: failed(`app container of ${target.bundleId}`, container) };
|
|
5841
|
-
const name = await exec(["plutil", "-extract", "CFBundleExecutable", "raw", "-o", "-",
|
|
9010
|
+
const name = await exec(["plutil", "-extract", "CFBundleExecutable", "raw", "-o", "-", join8(app, "Info.plist")], {
|
|
5842
9011
|
timeoutMs
|
|
5843
9012
|
});
|
|
5844
9013
|
const exe = name.stdout.trim();
|
|
@@ -5867,7 +9036,7 @@ async function iosNativeLog(target, deps, win, timeoutMs) {
|
|
|
5867
9036
|
`process == "${proc.exe}"`
|
|
5868
9037
|
], { timeoutMs });
|
|
5869
9038
|
if (show.exitCode === 0) {
|
|
5870
|
-
deps.fs.write(
|
|
9039
|
+
deps.fs.write(join8(win.dir, NATIVE_LOG), tail(show.stdout));
|
|
5871
9040
|
capture.files.push(NATIVE_LOG);
|
|
5872
9041
|
} else {
|
|
5873
9042
|
capture.problems.push(`native log: ${failed("log show", show)}`);
|
|
@@ -5875,7 +9044,7 @@ async function iosNativeLog(target, deps, win, timeoutMs) {
|
|
|
5875
9044
|
for (const report of deps.fs.listReports(deps.reportsDir)) {
|
|
5876
9045
|
if (!report.name.startsWith(proc.exe) || !report.name.endsWith(".ips") || report.mtimeMs < win.sinceMs)
|
|
5877
9046
|
continue;
|
|
5878
|
-
deps.fs.copy(
|
|
9047
|
+
deps.fs.copy(join8(deps.reportsDir, report.name), join8(win.dir, report.name));
|
|
5879
9048
|
capture.files.push(report.name);
|
|
5880
9049
|
}
|
|
5881
9050
|
return capture;
|
|
@@ -5897,7 +9066,7 @@ async function androidNativeLog(target, deps, win, timeoutMs) {
|
|
|
5897
9066
|
crash.exitCode === 0 ? `--- crash buffer ---
|
|
5898
9067
|
${crash.stdout}` : ""
|
|
5899
9068
|
];
|
|
5900
|
-
deps.fs.write(
|
|
9069
|
+
deps.fs.write(join8(win.dir, NATIVE_LOG), tail(sections.filter(Boolean).join(`
|
|
5901
9070
|
`)));
|
|
5902
9071
|
capture.files.push(NATIVE_LOG);
|
|
5903
9072
|
return capture;
|
|
@@ -5911,24 +9080,6 @@ async function captureNativeLog(target, deps, win) {
|
|
|
5911
9080
|
}
|
|
5912
9081
|
}
|
|
5913
9082
|
|
|
5914
|
-
// ../../packages/e2e/src/runner/callsite.ts
|
|
5915
|
-
import { join as join5 } from "path";
|
|
5916
|
-
var OWN_FILES = new Set(["callsite.ts", "scope.ts", "test.ts", "attempt.ts"].map((name) => join5(import.meta.dir, name)).concat(join5(import.meta.dir, "..", "index.ts")));
|
|
5917
|
-
var FRAME = /(?:\(|\s)((?:file:\/\/)?\/[^\s()]+?):\d+(?::\d+)?\)?\s*$/;
|
|
5918
|
-
function callerFile(stack = new Error().stack) {
|
|
5919
|
-
for (const line of (stack ?? "").split(`
|
|
5920
|
-
`)) {
|
|
5921
|
-
const raw = FRAME.exec(line)?.[1];
|
|
5922
|
-
if (raw === undefined)
|
|
5923
|
-
continue;
|
|
5924
|
-
const path = raw.startsWith("file://") ? raw.slice("file://".length) : raw;
|
|
5925
|
-
if (OWN_FILES.has(path) || path.includes("/node_modules/"))
|
|
5926
|
-
continue;
|
|
5927
|
-
return path;
|
|
5928
|
-
}
|
|
5929
|
-
return "unknown";
|
|
5930
|
-
}
|
|
5931
|
-
|
|
5932
9083
|
// ../../packages/e2e/src/runner/scope.ts
|
|
5933
9084
|
var BUN_TIMEOUT_MARGIN_MS2 = 30000;
|
|
5934
9085
|
var hookOwners = new WeakMap;
|
|
@@ -6005,7 +9156,8 @@ function registrationDefaults(env = process.env) {
|
|
|
6005
9156
|
timeoutMs: parsed?.timeoutMs ?? DEFAULT_TIMEOUT_MS4,
|
|
6006
9157
|
actionTimeoutMs: parsed?.actionTimeoutMs ?? DEFAULT_ACTION_TIMEOUT_MS,
|
|
6007
9158
|
assertionTimeoutMs: parsed?.assertionTimeoutMs ?? DEFAULT_ASSERTION_TIMEOUT_MS,
|
|
6008
|
-
platform: parsed?.platform
|
|
9159
|
+
platform: parsed?.platform,
|
|
9160
|
+
deviceKind: parsed?.deviceKind
|
|
6009
9161
|
};
|
|
6010
9162
|
}
|
|
6011
9163
|
var ONLY_KEY = Symbol.for("@delacour/warden-e2e/only");
|
|
@@ -6018,6 +9170,6 @@ function onlyWasSeen() {
|
|
|
6018
9170
|
|
|
6019
9171
|
// ../../packages/e2e/src/version.ts
|
|
6020
9172
|
var WORKSPACE_VERSION = "0.0.0-workspace";
|
|
6021
|
-
var WARDEN_E2E_VERSION2 = "0.
|
|
9173
|
+
var WARDEN_E2E_VERSION2 = "0.8.0";
|
|
6022
9174
|
|
|
6023
|
-
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,
|
|
9175
|
+
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, 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, realNativeLogDeps, captureNativeLog, BUN_TIMEOUT_MARGIN_MS2, ownHook, currentScope, createScope, runInScope, hooksFor, resolveUse, registrationDefaults, markOnly, onlyWasSeen, WORKSPACE_VERSION, WARDEN_E2E_VERSION2 };
|