@swmansion/argent 0.32.1-next.2 → 0.32.1-next.4
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/dist/cli-cmds.mjs
CHANGED
|
@@ -10708,6 +10708,8 @@ var FAILURE_CODES = {
|
|
|
10708
10708
|
FLOW_ENTRY_UNRECOGNIZED: "FLOW_ENTRY_UNRECOGNIZED",
|
|
10709
10709
|
FLOW_E2E_HAS_PREREQUISITE: "FLOW_E2E_HAS_PREREQUISITE",
|
|
10710
10710
|
FLOW_DEVICE_RESOLUTION: "FLOW_DEVICE_RESOLUTION",
|
|
10711
|
+
// The target does not satisfy the test's `requires:` block; a directory run skips it.
|
|
10712
|
+
FLOW_REQUIREMENTS_UNMET: "FLOW_REQUIREMENTS_UNMET",
|
|
10711
10713
|
PROFILER_QUERY_MODE_INVALID: "PROFILER_QUERY_MODE_INVALID",
|
|
10712
10714
|
PROFILER_QUERY_REQUIRED_PARAM_MISSING: "PROFILER_QUERY_REQUIRED_PARAM_MISSING",
|
|
10713
10715
|
PROFILER_DATA_NOT_LOADED: "PROFILER_DATA_NOT_LOADED",
|
|
@@ -35809,7 +35811,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
|
|
|
35809
35811
|
var SESSION_ID2 = randomUUID5();
|
|
35810
35812
|
function readCliVersion() {
|
|
35811
35813
|
if (true) {
|
|
35812
|
-
return "0.32.1-next.
|
|
35814
|
+
return "0.32.1-next.3";
|
|
35813
35815
|
}
|
|
35814
35816
|
return "0.0.0";
|
|
35815
35817
|
}
|
|
@@ -37150,8 +37152,9 @@ contain only letters, numbers, "_", or "-" \u2014 the same charset a name must
|
|
|
37150
37152
|
match. A test is standalone when its first non-\`echo\`/\`script\` step is \`launch\`.
|
|
37151
37153
|
Other tests are fragments and use the device's current state. Exception: a fragment whose first step \`run:\`s a chromium standalone
|
|
37152
37154
|
test boots that test's app before step 1 \u2014 when that launch is unambiguously
|
|
37153
|
-
chromium (a lone \`{ chromium: ... }\` target,
|
|
37154
|
-
|
|
37155
|
+
chromium (a lone \`{ chromium: ... }\` target, --platform chromium, or a
|
|
37156
|
+
\`requires:\` block that allows only chromium); a multi-platform launch
|
|
37157
|
+
auto-detects a device instead. Pass --device to attach to
|
|
37155
37158
|
a running instance.
|
|
37156
37159
|
|
|
37157
37160
|
A directory run prints only the steps that need attention (each failure, each
|
|
@@ -37164,6 +37167,12 @@ file, or a device it cannot resolve. A transport failure, a rejection the server
|
|
|
37164
37167
|
does not mark as validation, or a reply that is not a report stops the batch and
|
|
37165
37168
|
counts the remaining tests skipped.
|
|
37166
37169
|
|
|
37170
|
+
A test can declare a \`requires:\` block (platform, runtimeKind) that names the
|
|
37171
|
+
targets it supports. A directory run skips each test whose requirements the
|
|
37172
|
+
target does not meet, so the same directory runs on each platform without a
|
|
37173
|
+
separate list of tests. A single-test run fails instead. A directory run where
|
|
37174
|
+
no test ran exits 2.
|
|
37175
|
+
|
|
37167
37176
|
The CLI sends a run to its tool-server: the local one that starts
|
|
37168
37177
|
automatically, or the one that \`argent link\` or ARGENT_TOOLS_URL names. Over
|
|
37169
37178
|
a link, the CLI uploads these files and writes the new baselines that the run
|
|
@@ -37346,7 +37355,8 @@ function renderFailedSteps(report) {
|
|
|
37346
37355
|
return lines;
|
|
37347
37356
|
}
|
|
37348
37357
|
function renderBatchSummary(counts, durationMs) {
|
|
37349
|
-
|
|
37358
|
+
const verdict = counts.failed > 0 ? "FAIL" : counts.passed === 0 ? "NONE RAN" : "PASS";
|
|
37359
|
+
return `${verdict} \u2014 ${counts.total} e2e test${counts.total === 1 ? "" : "s"}: ${counts.passed} passed, ${counts.failed} failed, ${counts.skipped} skipped${durationSuffix(durationMs)}`;
|
|
37350
37360
|
}
|
|
37351
37361
|
function summarizeFailure(report) {
|
|
37352
37362
|
let n3 = 0;
|
|
@@ -37734,6 +37744,8 @@ function rejectionVerdict(code) {
|
|
|
37734
37744
|
return "not run (invalid e2e test)";
|
|
37735
37745
|
case FAILURE_CODES.FLOW_DEVICE_RESOLUTION:
|
|
37736
37746
|
return "not run (no device resolved)";
|
|
37747
|
+
case FAILURE_CODES.FLOW_REQUIREMENTS_UNMET:
|
|
37748
|
+
return "not run (requires not met)";
|
|
37737
37749
|
case FAILURE_CODES.FILE_INPUT_UPLOAD_FAILED:
|
|
37738
37750
|
return "not run (upload failed)";
|
|
37739
37751
|
default:
|
|
@@ -37810,6 +37822,14 @@ async function runFlowDirectory(dir, args, projectRoot, options) {
|
|
|
37810
37822
|
const err = unreachableErr ?? thrown;
|
|
37811
37823
|
const message = err instanceof Error ? err.message : String(err);
|
|
37812
37824
|
const toolErr = err instanceof ToolInvocationError ? err : void 0;
|
|
37825
|
+
if (toolErr?.errorCode === FAILURE_CODES.FLOW_REQUIREMENTS_UNMET) {
|
|
37826
|
+
const reason = message.replace(/^\[Tool:[^\]]+\] /, "");
|
|
37827
|
+
results.push({ path: rel, status: "skip", reason });
|
|
37828
|
+
if (!args.json)
|
|
37829
|
+
console.log(` ${STATUS_GLYPH.skip} skipped (requires not met)
|
|
37830
|
+
${reason}`);
|
|
37831
|
+
continue;
|
|
37832
|
+
}
|
|
37813
37833
|
const rejectedThisFlowOnly = toolErr?.errorKind === "validation";
|
|
37814
37834
|
const verdict = unreachableErr ? UNREACHABLE_VERDICT : rejectedThisFlowOnly ? rejectionVerdict(toolErr?.errorCode) : "did not finish (run error)";
|
|
37815
37835
|
if (!args.json) console.log(` ${STATUS_GLYPH.error} ${verdict}`);
|
|
@@ -37860,11 +37880,18 @@ async function runFlowDirectory(dir, args, projectRoot, options) {
|
|
|
37860
37880
|
skipped: results.filter((r2) => r2.status === "skip").length
|
|
37861
37881
|
};
|
|
37862
37882
|
const durationMs = Date.now() - batchStartedAt;
|
|
37883
|
+
const noneRan = counts.passed === 0 && counts.failed === 0;
|
|
37863
37884
|
if (args.json) {
|
|
37864
37885
|
console.log(
|
|
37865
37886
|
JSON.stringify(
|
|
37866
37887
|
// `flows` is the deprecated spelling of `e2eTests`.
|
|
37867
|
-
{
|
|
37888
|
+
{
|
|
37889
|
+
ok: counts.failed === 0 && !noneRan,
|
|
37890
|
+
...counts,
|
|
37891
|
+
durationMs,
|
|
37892
|
+
e2eTests: results,
|
|
37893
|
+
flows: results
|
|
37894
|
+
},
|
|
37868
37895
|
null,
|
|
37869
37896
|
2
|
|
37870
37897
|
)
|
|
@@ -37873,8 +37900,9 @@ async function runFlowDirectory(dir, args, projectRoot, options) {
|
|
|
37873
37900
|
for (const line of renderFailedFlows(failures)) console.log(line);
|
|
37874
37901
|
console.log(`
|
|
37875
37902
|
${renderBatchSummary(counts, durationMs)}`);
|
|
37903
|
+
if (noneRan) console.error(`No e2e test in ${dir} ran on the selected target.`);
|
|
37876
37904
|
}
|
|
37877
|
-
return exitAfterFlush(unreachable ? 2 : counts.failed === 0 ? 0 : 1);
|
|
37905
|
+
return exitAfterFlush(unreachable || noneRan ? 2 : counts.failed === 0 ? 0 : 1);
|
|
37878
37906
|
}
|
|
37879
37907
|
async function e2e(argv, options) {
|
|
37880
37908
|
const [sub, ...rest] = argv;
|
package/dist/client.js
CHANGED
|
@@ -9281,6 +9281,8 @@ var FAILURE_CODES = {
|
|
|
9281
9281
|
FLOW_ENTRY_UNRECOGNIZED: "FLOW_ENTRY_UNRECOGNIZED",
|
|
9282
9282
|
FLOW_E2E_HAS_PREREQUISITE: "FLOW_E2E_HAS_PREREQUISITE",
|
|
9283
9283
|
FLOW_DEVICE_RESOLUTION: "FLOW_DEVICE_RESOLUTION",
|
|
9284
|
+
// The target does not satisfy the test's `requires:` block; a directory run skips it.
|
|
9285
|
+
FLOW_REQUIREMENTS_UNMET: "FLOW_REQUIREMENTS_UNMET",
|
|
9284
9286
|
PROFILER_QUERY_MODE_INVALID: "PROFILER_QUERY_MODE_INVALID",
|
|
9285
9287
|
PROFILER_QUERY_REQUIRED_PARAM_MISSING: "PROFILER_QUERY_REQUIRED_PARAM_MISSING",
|
|
9286
9288
|
PROFILER_DATA_NOT_LOADED: "PROFILER_DATA_NOT_LOADED",
|
package/dist/installer.mjs
CHANGED
|
@@ -16045,6 +16045,8 @@ var FAILURE_CODES = {
|
|
|
16045
16045
|
FLOW_ENTRY_UNRECOGNIZED: "FLOW_ENTRY_UNRECOGNIZED",
|
|
16046
16046
|
FLOW_E2E_HAS_PREREQUISITE: "FLOW_E2E_HAS_PREREQUISITE",
|
|
16047
16047
|
FLOW_DEVICE_RESOLUTION: "FLOW_DEVICE_RESOLUTION",
|
|
16048
|
+
// The target does not satisfy the test's `requires:` block; a directory run skips it.
|
|
16049
|
+
FLOW_REQUIREMENTS_UNMET: "FLOW_REQUIREMENTS_UNMET",
|
|
16048
16050
|
PROFILER_QUERY_MODE_INVALID: "PROFILER_QUERY_MODE_INVALID",
|
|
16049
16051
|
PROFILER_QUERY_REQUIRED_PARAM_MISSING: "PROFILER_QUERY_REQUIRED_PARAM_MISSING",
|
|
16050
16052
|
PROFILER_DATA_NOT_LOADED: "PROFILER_DATA_NOT_LOADED",
|
|
@@ -16934,7 +16936,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
|
|
|
16934
16936
|
var SESSION_ID = randomUUID4();
|
|
16935
16937
|
function readCliVersion() {
|
|
16936
16938
|
if (true) {
|
|
16937
|
-
return "0.32.1-next.
|
|
16939
|
+
return "0.32.1-next.3";
|
|
16938
16940
|
}
|
|
16939
16941
|
return "0.0.0";
|
|
16940
16942
|
}
|
package/dist/mcp-server.mjs
CHANGED
|
@@ -25991,6 +25991,8 @@ var FAILURE_CODES = {
|
|
|
25991
25991
|
FLOW_ENTRY_UNRECOGNIZED: "FLOW_ENTRY_UNRECOGNIZED",
|
|
25992
25992
|
FLOW_E2E_HAS_PREREQUISITE: "FLOW_E2E_HAS_PREREQUISITE",
|
|
25993
25993
|
FLOW_DEVICE_RESOLUTION: "FLOW_DEVICE_RESOLUTION",
|
|
25994
|
+
// The target does not satisfy the test's `requires:` block; a directory run skips it.
|
|
25995
|
+
FLOW_REQUIREMENTS_UNMET: "FLOW_REQUIREMENTS_UNMET",
|
|
25994
25996
|
PROFILER_QUERY_MODE_INVALID: "PROFILER_QUERY_MODE_INVALID",
|
|
25995
25997
|
PROFILER_QUERY_REQUIRED_PARAM_MISSING: "PROFILER_QUERY_REQUIRED_PARAM_MISSING",
|
|
25996
25998
|
PROFILER_DATA_NOT_LOADED: "PROFILER_DATA_NOT_LOADED",
|
|
@@ -231,9 +231,9 @@ export interface ArgentToolArgs {
|
|
|
231
231
|
e2e_test_file?: string;
|
|
232
232
|
/** Absolute path to an e2e test .yaml on the client. */
|
|
233
233
|
e2e_test_path?: string;
|
|
234
|
-
/** Device id to run against (iOS UDID, Android/Vega serial, Chromium id), as list-devices reports it. Auto-detected when omitted, but only when exactly one booted device matches; with several booted the run fails and lists them, so pass this explicitly whenever more than one device is up. */
|
|
234
|
+
/** Device id to run against (iOS UDID, Android/Vega serial, Chromium id), as list-devices reports it. Auto-detected when omitted, but only when exactly one booted device matches (optionally narrowed by `platform` and the e2e test's `requires:` block); with several booted the run fails and lists them, so pass this explicitly whenever more than one device is up. */
|
|
235
235
|
device?: string;
|
|
236
|
-
/** Restrict auto-detection to this platform when several devices are booted. `ios` selects local simulators only; `ios-remote` selects a remote one. `chromium` with no `device` also makes the runner boot an Electron instance for a standalone e2e test from its `launch` step's chromium value and tear it down after the run, with no fallback to auto-detection (a single-key `launch: { chromium: … }` map does this on its own; a fragment, or a launch map without a `chromium` key, is still auto-detected). That value must be an Electron app path on the tool-server host: the bare-string `launch:` the recorder writes holds a bundle id, which fails the run with `Electron boot: path does not exist`, so first edit it to `{ chromium: <app path> }`. */
|
|
236
|
+
/** Restrict auto-detection to this platform when several devices are booted. `ios` selects local simulators only; `ios-remote` selects a remote one. `chromium` with no `device` also makes the runner boot an Electron instance for a standalone e2e test from its `launch` step's chromium value and tear it down after the run, with no fallback to auto-detection (a single-key `launch: { chromium: … }` map, or a `requires:` block that allows only chromium, does this on its own; a fragment, or a launch map without a `chromium` key, is still auto-detected). That value must be an Electron app path on the tool-server host: the bare-string `launch:` the recorder writes holds a bundle id, which fails the run with `Electron boot: path does not exist`, so first edit it to `{ chromium: <app path> }`. */
|
|
237
237
|
platform?: "ios" | "android" | "chromium" | "vega" | "ios-remote";
|
|
238
238
|
/** Write/refresh screenshot baselines for `snapshot` steps instead of diffing against them, also in the e2e tests that tool: e2e-execute steps run, unless such a step sets updateBaselines itself. Over a link, the client writes the new baselines when the run ends. */
|
|
239
239
|
updateBaselines?: boolean;
|
|
@@ -271,6 +271,11 @@ export interface ArgentToolArgs {
|
|
|
271
271
|
project_root: string;
|
|
272
272
|
/** Fragments only: the app/device state assumed on entry (e.g. "Settings app open on General page"). Omit it for a standalone e2e test. restart-app has no chromium support, so a chromium e2e test records as a fragment; add the `launch: { chromium: <app path> }` line to the YAML afterward, deleting the executionPrerequisite line if you passed one, since an e2e test that starts with a launch must not declare it. */
|
|
273
273
|
executionPrerequisite?: string;
|
|
274
|
+
/** The e2e test's `requires:` block: the targets it supports, ANDed. `platform` is one platform or a list (`ios` also covers a remote simulator); `runtimeKind` is `tv` or `mobile`. Omit it for a test that runs on any target. */
|
|
275
|
+
requires?: {
|
|
276
|
+
platform?: "ios" | "android" | "chromium" | "vega" | Array<"ios" | "android" | "chromium" | "vega">;
|
|
277
|
+
runtimeKind?: "mobile" | "tv";
|
|
278
|
+
};
|
|
274
279
|
};
|
|
275
280
|
fill: {
|
|
276
281
|
/** Target device id from `list-devices`. */
|
package/dist/tool-server.cjs
CHANGED
|
@@ -1751,6 +1751,8 @@ var init_failure_codes = __esm({
|
|
|
1751
1751
|
FLOW_ENTRY_UNRECOGNIZED: "FLOW_ENTRY_UNRECOGNIZED",
|
|
1752
1752
|
FLOW_E2E_HAS_PREREQUISITE: "FLOW_E2E_HAS_PREREQUISITE",
|
|
1753
1753
|
FLOW_DEVICE_RESOLUTION: "FLOW_DEVICE_RESOLUTION",
|
|
1754
|
+
// The target does not satisfy the test's `requires:` block; a directory run skips it.
|
|
1755
|
+
FLOW_REQUIREMENTS_UNMET: "FLOW_REQUIREMENTS_UNMET",
|
|
1754
1756
|
PROFILER_QUERY_MODE_INVALID: "PROFILER_QUERY_MODE_INVALID",
|
|
1755
1757
|
PROFILER_QUERY_REQUIRED_PARAM_MISSING: "PROFILER_QUERY_REQUIRED_PARAM_MISSING",
|
|
1756
1758
|
PROFILER_DATA_NOT_LOADED: "PROFILER_DATA_NOT_LOADED",
|
|
@@ -101157,7 +101159,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
|
|
|
101157
101159
|
var SESSION_ID = (0, import_node_crypto3.randomUUID)();
|
|
101158
101160
|
function readCliVersion() {
|
|
101159
101161
|
if (true) {
|
|
101160
|
-
return "0.32.1-next.
|
|
101162
|
+
return "0.32.1-next.3";
|
|
101161
101163
|
}
|
|
101162
101164
|
return "0.0.0";
|
|
101163
101165
|
}
|
|
@@ -103751,22 +103753,24 @@ async function simctlListDevices(options) {
|
|
|
103751
103753
|
}
|
|
103752
103754
|
}
|
|
103753
103755
|
var remoteRuntimeKindCache = /* @__PURE__ */ new Map();
|
|
103754
|
-
async function
|
|
103756
|
+
async function getRemoteSimulatorRuntimeKind(udid, options) {
|
|
103755
103757
|
const id = stripRemotePrefix(udid);
|
|
103756
103758
|
const cached3 = remoteRuntimeKindCache.get(id);
|
|
103757
|
-
if (cached3) return cached3
|
|
103759
|
+
if (cached3) return cached3;
|
|
103758
103760
|
try {
|
|
103759
103761
|
const { devices } = await simctlListDevices(options);
|
|
103760
103762
|
for (const [runtime, entries] of Object.entries(devices)) {
|
|
103761
103763
|
if (!entries.some((d) => d.udid === id)) continue;
|
|
103762
103764
|
const kind = simulatorRuntimeKind(runtime);
|
|
103763
|
-
if (
|
|
103764
|
-
|
|
103765
|
-
return kind === "tv";
|
|
103765
|
+
if (kind) remoteRuntimeKindCache.set(id, kind);
|
|
103766
|
+
return kind;
|
|
103766
103767
|
}
|
|
103767
103768
|
} catch {
|
|
103768
103769
|
}
|
|
103769
|
-
return
|
|
103770
|
+
return void 0;
|
|
103771
|
+
}
|
|
103772
|
+
async function isRemoteTvOsSimulator(udid, options) {
|
|
103773
|
+
return await getRemoteSimulatorRuntimeKind(udid, options) === "tv";
|
|
103770
103774
|
}
|
|
103771
103775
|
async function simctlBoot(udid) {
|
|
103772
103776
|
await run(["simctl", "boot", stripRemotePrefix(udid)]);
|
|
@@ -126584,7 +126588,8 @@ async function listRemoteIosSimulators() {
|
|
|
126584
126588
|
udid: withRemotePrefix(d.udid),
|
|
126585
126589
|
name: d.name,
|
|
126586
126590
|
state: d.state,
|
|
126587
|
-
runtime
|
|
126591
|
+
runtime,
|
|
126592
|
+
runtimeKind: simulatorRuntimeKind(runtime)
|
|
126588
126593
|
});
|
|
126589
126594
|
}
|
|
126590
126595
|
}
|
|
@@ -146094,6 +146099,8 @@ async function runChromium(api, params) {
|
|
|
146094
146099
|
}
|
|
146095
146100
|
await api.dispatchKeyEvent({
|
|
146096
146101
|
type: "keyDown",
|
|
146102
|
+
// Chromium needs carriage-return text for Enter's default form submission.
|
|
146103
|
+
...named.key === "Enter" ? { text: "\r" } : {},
|
|
146097
146104
|
key: named.key,
|
|
146098
146105
|
code: named.code,
|
|
146099
146106
|
windowsVirtualKeyCode: named.windowsVirtualKeyCode
|
|
@@ -168886,6 +168893,7 @@ var FOLD_POSTURES = ["closed", "half-open", "open"];
|
|
|
168886
168893
|
function isFoldPosture(value) {
|
|
168887
168894
|
return typeof value === "string" && FOLD_POSTURES.includes(value);
|
|
168888
168895
|
}
|
|
168896
|
+
var RUNTIME_KINDS = ["mobile", "tv"];
|
|
168889
168897
|
function blockSteps(step) {
|
|
168890
168898
|
return isBlockStep(step) ? step.steps : void 0;
|
|
168891
168899
|
}
|
|
@@ -169691,6 +169699,42 @@ function authoringPlatform(platform) {
|
|
|
169691
169699
|
return platform === "ios-remote" ? "ios" : platform;
|
|
169692
169700
|
}
|
|
169693
169701
|
var LAUNCH_MAP_KEYS = ["native", ...LAUNCH_PLATFORMS];
|
|
169702
|
+
var REQUIRES_KEYS = ["platform", "runtimeKind"];
|
|
169703
|
+
function parseRequires(raw) {
|
|
169704
|
+
const bad = (detail) => badEntry({ requires: raw }, detail);
|
|
169705
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
169706
|
+
bad(`requires must be a map of ${REQUIRES_KEYS.join(" / ")}`);
|
|
169707
|
+
}
|
|
169708
|
+
const b = raw;
|
|
169709
|
+
rejectUnknownKeys({ requires: raw }, b, REQUIRES_KEYS, "requires");
|
|
169710
|
+
const out = {};
|
|
169711
|
+
if (b.platform !== void 0) {
|
|
169712
|
+
const list = Array.isArray(b.platform) ? b.platform : [b.platform];
|
|
169713
|
+
const oneOf2 = `one of ${LAUNCH_PLATFORMS.join(", ")}`;
|
|
169714
|
+
if (list.length === 0) bad(`requires.platform must name at least ${oneOf2}`);
|
|
169715
|
+
const platforms = [];
|
|
169716
|
+
for (const p of list) {
|
|
169717
|
+
if (typeof p !== "string" || !LAUNCH_PLATFORMS.includes(p)) {
|
|
169718
|
+
bad(`requires.platform must be ${oneOf2} (or a list of them)`);
|
|
169719
|
+
}
|
|
169720
|
+
platforms.push(p);
|
|
169721
|
+
}
|
|
169722
|
+
out.platform = platforms;
|
|
169723
|
+
}
|
|
169724
|
+
if (b.runtimeKind !== void 0) {
|
|
169725
|
+
const kind = b.runtimeKind;
|
|
169726
|
+
if (typeof kind !== "string" || !RUNTIME_KINDS.includes(kind)) {
|
|
169727
|
+
bad(`requires.runtimeKind must be one of ${RUNTIME_KINDS.join(", ")}`);
|
|
169728
|
+
}
|
|
169729
|
+
out.runtimeKind = kind;
|
|
169730
|
+
}
|
|
169731
|
+
if (out.platform === void 0 && out.runtimeKind === void 0) {
|
|
169732
|
+
bad(
|
|
169733
|
+
`requires must declare at least one of ${REQUIRES_KEYS.join(", ")} (omit the block for a flow that runs on any target)`
|
|
169734
|
+
);
|
|
169735
|
+
}
|
|
169736
|
+
return out;
|
|
169737
|
+
}
|
|
169694
169738
|
function rejectUnquotedLaunchArg(raw, args, where) {
|
|
169695
169739
|
const i = args.findIndex((a) => typeof a === "number" || typeof a === "boolean");
|
|
169696
169740
|
if (i === -1) return;
|
|
@@ -170576,8 +170620,17 @@ function fromYamlStep(raw, blockDepth = 0) {
|
|
|
170576
170620
|
}
|
|
170577
170621
|
return badEntry(raw, "unrecognized step kind");
|
|
170578
170622
|
}
|
|
170623
|
+
function requiresToYaml({ platform, runtimeKind }) {
|
|
170624
|
+
return {
|
|
170625
|
+
...platform ? { platform: platform.length === 1 ? platform[0] : platform } : {},
|
|
170626
|
+
...runtimeKind ? { runtimeKind } : {}
|
|
170627
|
+
};
|
|
170628
|
+
}
|
|
170579
170629
|
function serializeFlow(flow) {
|
|
170580
|
-
const doc = {
|
|
170630
|
+
const doc = {
|
|
170631
|
+
...flow.requires ? { requires: requiresToYaml(flow.requires) } : {},
|
|
170632
|
+
steps: flow.steps.map(toYamlStep)
|
|
170633
|
+
};
|
|
170581
170634
|
if (flow.executionPrerequisite) doc.executionPrerequisite = flow.executionPrerequisite;
|
|
170582
170635
|
return (0, import_yaml.stringify)(doc, { blockQuote: false });
|
|
170583
170636
|
}
|
|
@@ -170623,7 +170676,7 @@ function parseFlow(content) {
|
|
|
170623
170676
|
error_kind: "validation"
|
|
170624
170677
|
});
|
|
170625
170678
|
}
|
|
170626
|
-
const topKeys = ["executionPrerequisite", "steps"];
|
|
170679
|
+
const topKeys = ["executionPrerequisite", "requires", "steps"];
|
|
170627
170680
|
const unknownTop = Object.keys(parsed).filter((k) => !topKeys.includes(k));
|
|
170628
170681
|
if (unknownTop.length > 0) {
|
|
170629
170682
|
throw new FailureError(
|
|
@@ -170642,6 +170695,7 @@ function parseFlow(content) {
|
|
|
170642
170695
|
});
|
|
170643
170696
|
const flow = {
|
|
170644
170697
|
executionPrerequisite: parsed.executionPrerequisite ?? "",
|
|
170698
|
+
...parsed.requires === void 0 ? {} : { requires: parseRequires(parsed.requires) },
|
|
170645
170699
|
steps
|
|
170646
170700
|
};
|
|
170647
170701
|
validateFlow(flow);
|
|
@@ -170904,6 +170958,12 @@ var zodSchema66 = external_exports.object({
|
|
|
170904
170958
|
),
|
|
170905
170959
|
executionPrerequisite: external_exports.string().optional().describe(
|
|
170906
170960
|
'Fragments only: the app/device state assumed on entry (e.g. "Settings app open on General page"). Omit it for a standalone e2e test. restart-app has no chromium support, so a chromium e2e test records as a fragment; add the `launch: { chromium: <app path> }` line to the YAML afterward, deleting the executionPrerequisite line if you passed one, since an e2e test that starts with a launch must not declare it.'
|
|
170961
|
+
),
|
|
170962
|
+
requires: external_exports.object({
|
|
170963
|
+
platform: external_exports.union([external_exports.enum(LAUNCH_PLATFORMS), external_exports.array(external_exports.enum(LAUNCH_PLATFORMS)).min(1)]).optional(),
|
|
170964
|
+
runtimeKind: external_exports.enum(RUNTIME_KINDS).optional()
|
|
170965
|
+
}).optional().describe(
|
|
170966
|
+
"The e2e test's `requires:` block: the targets it supports, ANDed. `platform` is one platform or a list (`ios` also covers a remote simulator); `runtimeKind` is `tv` or `mobile`. Omit it for a test that runs on any target."
|
|
170907
170967
|
)
|
|
170908
170968
|
});
|
|
170909
170969
|
var flowStartRecordingTool = {
|
|
@@ -170924,7 +170984,9 @@ not hold, or may fail once converted to \`await:\`/\`assert:\`. A wait nested in
|
|
|
170924
170984
|
recorded run-sequence gets no such warning, so read its \`toolResult\`. For a
|
|
170925
170985
|
standalone e2e test, record a restart-app of the app under test as the FIRST step
|
|
170926
170986
|
(captured as the e2e test's \`launch\` step); for a reusable fragment, skip that and
|
|
170927
|
-
pass executionPrerequisite instead.
|
|
170987
|
+
pass executionPrerequisite instead. Starting over an existing e2e test replaces the
|
|
170988
|
+
whole file, including its \`requires:\` block; pass \`requires\` to keep it.
|
|
170989
|
+
Use e2e-add-echo to add labels, and
|
|
170928
170990
|
e2e-add-script to run a local .mjs file and record it as a \`script:\` step.
|
|
170929
170991
|
Call e2e-finish-recording when done.
|
|
170930
170992
|
|
|
@@ -170949,6 +171011,7 @@ and without a link, an edit that renumbers the steps drops their warnings.`,
|
|
|
170949
171011
|
async execute(_services, params, ctx) {
|
|
170950
171012
|
const flow = {
|
|
170951
171013
|
executionPrerequisite: params.executionPrerequisite ?? "",
|
|
171014
|
+
...params.requires ? { requires: parseRequires(params.requires) } : {},
|
|
170952
171015
|
steps: []
|
|
170953
171016
|
};
|
|
170954
171017
|
validateFlow(flow);
|
|
@@ -171011,7 +171074,9 @@ init_device_info();
|
|
|
171011
171074
|
|
|
171012
171075
|
// ../tool-server/src/tools/flows/flow-device.ts
|
|
171013
171076
|
init_src2();
|
|
171077
|
+
init_adb();
|
|
171014
171078
|
init_device_info();
|
|
171079
|
+
init_external_devices();
|
|
171015
171080
|
var DEVICE_BIND_KEYS = ["udid", "device_id", "device"];
|
|
171016
171081
|
var DEVICE_BIND_LIST_KEYS = ["devices"];
|
|
171017
171082
|
var DEVICE_ARG_KEYS = DEVICE_BIND_KEYS;
|
|
@@ -171057,16 +171122,128 @@ function deviceResolutionError(message, all) {
|
|
|
171057
171122
|
error_kind: "validation"
|
|
171058
171123
|
});
|
|
171059
171124
|
}
|
|
171125
|
+
function describeRequires(requires) {
|
|
171126
|
+
const parts2 = [];
|
|
171127
|
+
if (requires.platform) parts2.push(`platform: [${requires.platform.join(", ")}]`);
|
|
171128
|
+
if (requires.runtimeKind) parts2.push(`runtimeKind: ${requires.runtimeKind}`);
|
|
171129
|
+
return `requires: { ${parts2.join(", ")} }`;
|
|
171130
|
+
}
|
|
171131
|
+
function requirementsError(message, code = FAILURE_CODES.FLOW_REQUIREMENTS_UNMET) {
|
|
171132
|
+
return new FailureError(message, {
|
|
171133
|
+
error_code: code,
|
|
171134
|
+
failure_stage: "flow_requirements",
|
|
171135
|
+
failure_area: "tool_server",
|
|
171136
|
+
error_kind: "validation"
|
|
171137
|
+
});
|
|
171138
|
+
}
|
|
171139
|
+
var CONSTANT_RUNTIME_KIND = {
|
|
171140
|
+
chromium: "mobile",
|
|
171141
|
+
harmony: "mobile",
|
|
171142
|
+
vega: "tv"
|
|
171143
|
+
};
|
|
171144
|
+
function platformMeets(platform, requires) {
|
|
171145
|
+
if (requires.platform && !requires.platform.includes(authoringPlatform(platform))) {
|
|
171146
|
+
return false;
|
|
171147
|
+
}
|
|
171148
|
+
const constant = CONSTANT_RUNTIME_KIND[platform];
|
|
171149
|
+
return !(requires.runtimeKind && constant && constant !== requires.runtimeKind);
|
|
171150
|
+
}
|
|
171151
|
+
function assertPlatformMeetsRequires(platform, requires, target, subject2 = "This e2e test") {
|
|
171152
|
+
if (!requires || platformMeets(platform, requires)) return;
|
|
171153
|
+
throw requirementsError(
|
|
171154
|
+
`${subject2} declares ${describeRequires(requires)}, which excludes ${target}.`
|
|
171155
|
+
);
|
|
171156
|
+
}
|
|
171157
|
+
async function probeRuntimeKind(device) {
|
|
171158
|
+
const constant = CONSTANT_RUNTIME_KIND[device.platform];
|
|
171159
|
+
if (constant) return constant;
|
|
171160
|
+
if (isIosPhysicalDevice(device)) return "mobile";
|
|
171161
|
+
switch (device.platform) {
|
|
171162
|
+
case "ios":
|
|
171163
|
+
return getSimulatorRuntimeKind(device.id);
|
|
171164
|
+
case "ios-remote":
|
|
171165
|
+
return getRemoteSimulatorRuntimeKind(device.id);
|
|
171166
|
+
case "android":
|
|
171167
|
+
return getAndroidRuntimeKind(externalNativeId(device.id));
|
|
171168
|
+
default:
|
|
171169
|
+
return void 0;
|
|
171170
|
+
}
|
|
171171
|
+
}
|
|
171172
|
+
async function assertDeviceMeetsRequires(device, requires, subject2 = "This e2e test") {
|
|
171173
|
+
if (!requires) return;
|
|
171174
|
+
assertPlatformMeetsRequires(
|
|
171175
|
+
device.platform,
|
|
171176
|
+
requires,
|
|
171177
|
+
`device ${device.id} (${device.platform})`,
|
|
171178
|
+
subject2
|
|
171179
|
+
);
|
|
171180
|
+
if (!requires.runtimeKind) return;
|
|
171181
|
+
const kind = await probeRuntimeKind(device);
|
|
171182
|
+
if (kind === void 0) {
|
|
171183
|
+
throw unknownRuntimeKindError(requires, [`${device.id} (${device.platform})`], subject2);
|
|
171184
|
+
}
|
|
171185
|
+
if (kind !== requires.runtimeKind) {
|
|
171186
|
+
throw requirementsError(
|
|
171187
|
+
`${subject2} declares ${describeRequires(requires)}, which excludes device ${device.id} (${device.platform}, ${kind}).`
|
|
171188
|
+
);
|
|
171189
|
+
}
|
|
171190
|
+
}
|
|
171191
|
+
function unknownRuntimeKindError(requires, devices, subject2 = "This e2e test", remedy = "") {
|
|
171192
|
+
return requirementsError(
|
|
171193
|
+
`${subject2} declares ${describeRequires(requires)}, but the runtime kind of ${devices.join(", ")} could not be determined.${remedy}`,
|
|
171194
|
+
FAILURE_CODES.FLOW_DEVICE_RESOLUTION
|
|
171195
|
+
);
|
|
171196
|
+
}
|
|
171197
|
+
function listedRuntimeKind(d) {
|
|
171198
|
+
return CONSTANT_RUNTIME_KIND[d.platform] ?? d.runtimeKind;
|
|
171199
|
+
}
|
|
171200
|
+
async function candidateRuntimeKind(d) {
|
|
171201
|
+
const id = deviceEntryId(d);
|
|
171202
|
+
return listedRuntimeKind(d) ?? (id ? probeRuntimeKind(resolveDevice(id)) : void 0);
|
|
171203
|
+
}
|
|
171060
171204
|
async function resolveFlowDevice(registry2, ctx, opts) {
|
|
171061
171205
|
if (opts.device) return resolveDevice(opts.device);
|
|
171062
171206
|
const { devices } = await invokeSubTool(registry2, ctx, "list-devices", {});
|
|
171063
171207
|
const booted = devices.filter(isBooted);
|
|
171064
171208
|
const scoped = opts.platform ? booted.filter((d) => d.platform === opts.platform) : booted;
|
|
171065
|
-
|
|
171066
|
-
|
|
171209
|
+
const requires = opts.requires;
|
|
171210
|
+
const candidates = requires ? scoped.filter((d) => platformMeets(d.platform, requires)) : scoped;
|
|
171211
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
171212
|
+
if (requires?.runtimeKind) {
|
|
171213
|
+
const wanted = requires.runtimeKind;
|
|
171214
|
+
const anyMatch = () => candidates.some((d) => kinds.get(d) === wanted);
|
|
171215
|
+
for (const d of candidates) kinds.set(d, listedRuntimeKind(d));
|
|
171216
|
+
if (!anyMatch()) {
|
|
171217
|
+
for (const d of candidates) {
|
|
171218
|
+
if (kinds.get(d) === void 0) kinds.set(d, await candidateRuntimeKind(d));
|
|
171219
|
+
}
|
|
171220
|
+
const unknown2 = candidates.filter((d) => kinds.get(d) === void 0);
|
|
171221
|
+
if (!anyMatch() && unknown2.length > 0) {
|
|
171222
|
+
throw unknownRuntimeKindError(
|
|
171223
|
+
requires,
|
|
171224
|
+
unknown2.map(describeDevice),
|
|
171225
|
+
void 0,
|
|
171226
|
+
" Pass --device or --platform to choose a device."
|
|
171227
|
+
);
|
|
171228
|
+
}
|
|
171229
|
+
}
|
|
171230
|
+
}
|
|
171231
|
+
const eligible = requires?.runtimeKind ? candidates.filter((d) => kinds.get(d) === requires.runtimeKind) : candidates;
|
|
171232
|
+
if (eligible.length === 1) {
|
|
171233
|
+
const id = deviceEntryId(eligible[0]);
|
|
171067
171234
|
if (id) return resolveDevice(id);
|
|
171068
171235
|
}
|
|
171069
|
-
if (
|
|
171236
|
+
if (eligible.length === 0 && requires && scoped.length > 0) {
|
|
171237
|
+
const listed = scoped.map((d) => {
|
|
171238
|
+
const known = kinds.get(d) ?? listedRuntimeKind(d);
|
|
171239
|
+
const kind = requires.runtimeKind ? `, ${known ?? "kind unknown"}` : "";
|
|
171240
|
+
return `${deviceEntryId(d) ?? "?"} (${d.platform}${kind})`;
|
|
171241
|
+
});
|
|
171242
|
+
throw requirementsError(
|
|
171243
|
+
`No booted device satisfies this e2e test's ${describeRequires(requires)}. Booted devices: ${listed.join(", ")}.`
|
|
171244
|
+
);
|
|
171245
|
+
}
|
|
171246
|
+
if (eligible.length === 0) {
|
|
171070
171247
|
const what = opts.platform ? `No booted ${opts.platform} device found.` : "No booted device found.";
|
|
171071
171248
|
throw deviceResolutionError(
|
|
171072
171249
|
`${what} Pass a device id or platform explicitly.${connectedIosDeviceHint(devices, opts.platform)}`,
|
|
@@ -171074,8 +171251,8 @@ async function resolveFlowDevice(registry2, ctx, opts) {
|
|
|
171074
171251
|
);
|
|
171075
171252
|
}
|
|
171076
171253
|
throw deviceResolutionError(
|
|
171077
|
-
`${
|
|
171078
|
-
|
|
171254
|
+
`${eligible.length} booted devices matched \u2014 pass --device or --platform to disambiguate.`,
|
|
171255
|
+
eligible
|
|
171079
171256
|
);
|
|
171080
171257
|
}
|
|
171081
171258
|
function stripDeviceKeys(args) {
|
|
@@ -178725,10 +178902,10 @@ var zodSchema71 = external_exports.object({
|
|
|
178725
178902
|
),
|
|
178726
178903
|
e2e_test_path: external_exports.string().optional().describe("Absolute path to an e2e test .yaml on the client."),
|
|
178727
178904
|
device: external_exports.string().optional().describe(
|
|
178728
|
-
"Device id to run against (iOS UDID, Android/Vega serial, Chromium id), as list-devices reports it. Auto-detected when omitted, but only when exactly one booted device matches; with several booted the run fails and lists them, so pass this explicitly whenever more than one device is up."
|
|
178905
|
+
"Device id to run against (iOS UDID, Android/Vega serial, Chromium id), as list-devices reports it. Auto-detected when omitted, but only when exactly one booted device matches (optionally narrowed by `platform` and the e2e test's `requires:` block); with several booted the run fails and lists them, so pass this explicitly whenever more than one device is up."
|
|
178729
178906
|
),
|
|
178730
178907
|
platform: external_exports.enum(SELECTABLE_PLATFORMS).optional().describe(
|
|
178731
|
-
"Restrict auto-detection to this platform when several devices are booted. `ios` selects local simulators only; `ios-remote` selects a remote one. `chromium` with no `device` also makes the runner boot an Electron instance for a standalone e2e test from its `launch` step's chromium value and tear it down after the run, with no fallback to auto-detection (a single-key `launch: { chromium: \u2026 }` map does this on its own; a fragment, or a launch map without a `chromium` key, is still auto-detected). That value must be an Electron app path on the tool-server host: the bare-string `launch:` the recorder writes holds a bundle id, which fails the run with `Electron boot: path does not exist`, so first edit it to `{ chromium: <app path> }`."
|
|
178908
|
+
"Restrict auto-detection to this platform when several devices are booted. `ios` selects local simulators only; `ios-remote` selects a remote one. `chromium` with no `device` also makes the runner boot an Electron instance for a standalone e2e test from its `launch` step's chromium value and tear it down after the run, with no fallback to auto-detection (a single-key `launch: { chromium: \u2026 }` map, or a `requires:` block that allows only chromium, does this on its own; a fragment, or a launch map without a `chromium` key, is still auto-detected). That value must be an Electron app path on the tool-server host: the bare-string `launch:` the recorder writes holds a bundle id, which fails the run with `Electron boot: path does not exist`, so first edit it to `{ chromium: <app path> }`."
|
|
178732
178909
|
),
|
|
178733
178910
|
updateBaselines: external_exports.boolean().optional().describe(
|
|
178734
178911
|
"Write/refresh screenshot baselines for `snapshot` steps instead of diffing against them, also in the e2e tests that tool: e2e-execute steps run, unless such a step sets updateBaselines itself. Over a link, the client writes the new baselines when the run ends."
|
|
@@ -178839,7 +179016,17 @@ Without a link, every step kind runs.`,
|
|
|
178839
179016
|
);
|
|
178840
179017
|
}
|
|
178841
179018
|
}
|
|
179019
|
+
if (params.device) {
|
|
179020
|
+
await assertDeviceMeetsRequires(resolveDevice(params.device), flow.requires);
|
|
179021
|
+
} else if (params.platform) {
|
|
179022
|
+
assertPlatformMeetsRequires(
|
|
179023
|
+
params.platform,
|
|
179024
|
+
flow.requires,
|
|
179025
|
+
`the ${params.platform} target this run was pointed at`
|
|
179026
|
+
);
|
|
179027
|
+
}
|
|
178842
179028
|
if (flow.executionPrerequisite && !params.prerequisiteAcknowledged) {
|
|
179029
|
+
await resolveRunDevice(registry2, ctx, flow, params, flowsDir, seed, viaUpload, project);
|
|
178843
179030
|
return {
|
|
178844
179031
|
e2eTest: flowName,
|
|
178845
179032
|
flow: flowName,
|
|
@@ -178939,8 +179126,15 @@ Without a link, every step kind runs.`,
|
|
|
178939
179126
|
async function resolveRunDevice(registry2, ctx, flow, params, flowDir, runStack, viaUpload, project) {
|
|
178940
179127
|
if (!params.device) {
|
|
178941
179128
|
const leading = await leadingLaunch(flow, runStack, { registry: registry2, project });
|
|
178942
|
-
const
|
|
179129
|
+
const chromiumOnly = flow.requires?.platform?.length === 1 && flow.requires.platform[0] === "chromium";
|
|
179130
|
+
const platform = params.platform ?? (chromiumOnly ? "chromium" : void 0);
|
|
179131
|
+
const spec = leading && chromiumBootSpec(leading.app, platform);
|
|
178943
179132
|
if (spec) {
|
|
179133
|
+
assertPlatformMeetsRequires(
|
|
179134
|
+
"chromium",
|
|
179135
|
+
flow.requires,
|
|
179136
|
+
"the chromium app its leading launch boots"
|
|
179137
|
+
);
|
|
178944
179138
|
let booted;
|
|
178945
179139
|
try {
|
|
178946
179140
|
booted = await bootChromiumForFlow(spec, flowDir, viaUpload);
|
|
@@ -178953,16 +179147,25 @@ async function resolveRunDevice(registry2, ctx, flow, params, flowDir, runStack,
|
|
|
178953
179147
|
if (!flowScopesDevice(registry2, flow.steps)) return { device: null, booted: null };
|
|
178954
179148
|
try {
|
|
178955
179149
|
return {
|
|
178956
|
-
device: await resolveFlowDevice(registry2, ctx,
|
|
179150
|
+
device: await resolveFlowDevice(registry2, ctx, {
|
|
179151
|
+
...resolveOpts(params),
|
|
179152
|
+
requires: flow.requires
|
|
179153
|
+
}),
|
|
178957
179154
|
booted: null
|
|
178958
179155
|
};
|
|
178959
179156
|
} catch (err) {
|
|
178960
|
-
|
|
179157
|
+
const signal = getFailureSignal(err);
|
|
179158
|
+
if (signal?.error_code !== FAILURE_CODES.FLOW_DEVICE_RESOLUTION || signal.failure_stage === "flow_requirements") {
|
|
179159
|
+
throw err;
|
|
179160
|
+
}
|
|
178961
179161
|
return { device: null, booted: null };
|
|
178962
179162
|
}
|
|
178963
179163
|
}
|
|
178964
179164
|
}
|
|
178965
|
-
const device = await resolveFlowDevice(registry2, ctx,
|
|
179165
|
+
const device = await resolveFlowDevice(registry2, ctx, {
|
|
179166
|
+
...resolveOpts(params),
|
|
179167
|
+
requires: flow.requires
|
|
179168
|
+
});
|
|
178966
179169
|
return { device, booted: null };
|
|
178967
179170
|
}
|
|
178968
179171
|
function resolveOpts(params) {
|
|
@@ -179368,6 +179571,13 @@ async function execRunStep(state3, step, scope) {
|
|
|
179368
179571
|
}
|
|
179369
179572
|
const refusal = fragmentLoadRefusal(state3.registry, fragment, target);
|
|
179370
179573
|
if (refusal !== void 0) return fail(refusal);
|
|
179574
|
+
if (state3.device) {
|
|
179575
|
+
try {
|
|
179576
|
+
await assertDeviceMeetsRequires(state3.device, fragment.requires, `Fragment "${target}"`);
|
|
179577
|
+
} catch (err) {
|
|
179578
|
+
return fail(errMsg4(err));
|
|
179579
|
+
}
|
|
179580
|
+
}
|
|
179371
179581
|
pushReport(state3, {
|
|
179372
179582
|
index,
|
|
179373
179583
|
kind: "run",
|
package/package.json
CHANGED
|
@@ -42,6 +42,8 @@ E2E tests never store a device id. The runner binds the device. `launch:` restar
|
|
|
42
42
|
|
|
43
43
|
The one exception is a device _scope_ rather than a target: `stop-all-simulator-servers`' `devices` list **is** kept in the YAML, because without it the step means the machine-wide sweep and would tear down devices other agents are mid-session on. Replay rebinds a recorded scope only when you pass `device` explicitly: an auto-detected device would retarget the teardown at a device the test never named. So the recorded ids are what run when you replay without `device`; on another host they reap nothing and come back in `unmatched`, so re-record the cleanup test there or pass `device`. A step that recorded no scope is narrowed onto the run's device **only when the run resolved one**. A cleanup test whose only step is that teardown needs no device, so with none or several booted it resolves none, replays as the machine-wide sweep, and still reports a pass. Record the scope, or pass `device` at replay, whenever the sweep must stay confined.
|
|
44
44
|
|
|
45
|
+
A top-level `requires:` block limits the targets a test runs on: `platform` (`ios`, `android`, `chromium`, `vega`, or a list; `ios` covers a remote simulator) and `runtimeKind` (`tv` or `mobile`). Add it for a platform-specific or TV-specific scenario. A directory run skips a test whose target does not match; a `run:` fragment that does not match fails its step.
|
|
46
|
+
|
|
45
47
|
## Selectors
|
|
46
48
|
|
|
47
49
|
Use values that meet the [stable-selector definition](../SKILL.md#stable-selectors). Always write explicit maps:
|