@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.2";
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, or --platform chromium); a
37154
- multi-platform launch auto-detects a device instead. Pass --device to attach to
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
- return `${counts.failed === 0 ? "PASS" : "FAIL"} \u2014 ${counts.total} e2e test${counts.total === 1 ? "" : "s"}: ${counts.passed} passed, ${counts.failed} failed, ${counts.skipped} skipped${durationSuffix(durationMs)}`;
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
- { ok: counts.failed === 0, ...counts, durationMs, e2eTests: results, flows: results },
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",
@@ -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.2";
16939
+ return "0.32.1-next.3";
16938
16940
  }
16939
16941
  return "0.0.0";
16940
16942
  }
@@ -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`. */
@@ -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.2";
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 isRemoteTvOsSimulator(udid, options) {
103756
+ async function getRemoteSimulatorRuntimeKind(udid, options) {
103755
103757
  const id = stripRemotePrefix(udid);
103756
103758
  const cached3 = remoteRuntimeKindCache.get(id);
103757
- if (cached3) return cached3 === "tv";
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 (!kind) return false;
103764
- remoteRuntimeKindCache.set(id, kind);
103765
- return kind === "tv";
103765
+ if (kind) remoteRuntimeKindCache.set(id, kind);
103766
+ return kind;
103766
103767
  }
103767
103768
  } catch {
103768
103769
  }
103769
- return false;
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 = { steps: flow.steps.map(toYamlStep) };
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. Use e2e-add-echo to add labels, and
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
- if (scoped.length === 1) {
171066
- const id = deviceEntryId(scoped[0]);
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 (scoped.length === 0) {
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
- `${scoped.length} booted devices matched \u2014 pass --device or --platform to disambiguate.`,
171078
- scoped
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 spec = leading && chromiumBootSpec(leading.app, params.platform);
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, resolveOpts(params)),
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
- if (getFailureSignal(err)?.error_code !== FAILURE_CODES.FLOW_DEVICE_RESOLUTION) throw err;
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, resolveOpts(params));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/argent",
3
- "version": "0.32.1-next.2",
3
+ "version": "0.32.1-next.4",
4
4
  "mcpName": "io.github.software-mansion/argent",
5
5
  "description": "MCP server for iOS Simulator and Android Emulator control",
6
6
  "license": "Apache-2.0",
@@ -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: