@swmansion/argent 0.16.1-next.0 → 0.16.1-next.2

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.
@@ -144726,6 +144726,28 @@ function textWaitToYaml(selector, expectedText, textMatch) {
144726
144726
  }
144727
144727
  }
144728
144728
  }
144729
+ function targetToYaml(step) {
144730
+ const hasPointField = step.x !== void 0 || step.y !== void 0;
144731
+ if (step.selector !== void 0) {
144732
+ if (hasPointField) {
144733
+ throw new Error(
144734
+ "Cannot serialize flow gesture target: use a selector or x/y coordinates, not both"
144735
+ );
144736
+ }
144737
+ return selectorToYaml(step.selector);
144738
+ }
144739
+ if (typeof step.x !== "number" || typeof step.y !== "number") {
144740
+ throw new Error(
144741
+ "Cannot serialize flow gesture target: a coordinate target needs numeric x and y"
144742
+ );
144743
+ }
144744
+ if (!(step.x >= 0 && step.x <= 1) || !(step.y >= 0 && step.y <= 1)) {
144745
+ throw new Error(
144746
+ "Cannot serialize flow gesture target: coordinates are normalized 0\u20131 fractions of the screen, not pixels"
144747
+ );
144748
+ }
144749
+ return { x: step.x, y: step.y };
144750
+ }
144729
144751
  function waitToYaml(condition, selector, expectedText, textMatch, timeoutMs) {
144730
144752
  const sel = selectorToYaml(selector);
144731
144753
  let body;
@@ -144755,18 +144777,13 @@ function toYamlStep(step) {
144755
144777
  case "run":
144756
144778
  return { run: step.flow };
144757
144779
  case "tap": {
144758
- if (step.selector) {
144759
- const sel = selectorToYaml(step.selector);
144760
- return { tap: step.times !== void 0 ? { on: sel, times: step.times } : sel };
144761
- }
144762
- const body = { x: step.x, y: step.y };
144763
- if (step.times !== void 0) body.times = step.times;
144764
- return { tap: body };
144780
+ const target = targetToYaml(step);
144781
+ return { tap: step.times !== void 0 ? { on: target, times: step.times } : target };
144765
144782
  }
144766
144783
  case "long-press": {
144767
- const sel = selectorToYaml(step.selector);
144784
+ const target = targetToYaml(step);
144768
144785
  return {
144769
- "long-press": step.duration !== void 0 ? { on: sel, duration: step.duration } : sel
144786
+ "long-press": step.duration !== void 0 ? { on: target, duration: step.duration } : target
144770
144787
  };
144771
144788
  }
144772
144789
  case "type": {
@@ -145079,35 +145096,6 @@ var STEP_DIRECTIVE_KEYS = [
145079
145096
  "scroll-to",
145080
145097
  "snapshot"
145081
145098
  ];
145082
- function parseLongPress(body, entry) {
145083
- const obj = body !== null && typeof body === "object" ? body : {};
145084
- if (obj.on !== void 0 || obj.duration !== void 0) {
145085
- if (obj.text !== void 0 || obj.id !== void 0 || obj.identifier !== void 0 || obj.role !== void 0) {
145086
- badEntry(
145087
- entry,
145088
- 'the long-press options form takes a nested selector \u2014 e.g. long-press: { on: { text: "Row" }, duration: 1200 }'
145089
- );
145090
- }
145091
- if (!Object.keys(obj).every((k) => k === "on" || k === "duration")) {
145092
- badEntry(entry, "the long-press options form accepts only { on, duration }");
145093
- }
145094
- if (obj.on === void 0) {
145095
- badEntry(entry, 'long-press needs a target \u2014 e.g. long-press: { on: "Row", duration: 1200 }');
145096
- }
145097
- const step = { kind: "long-press", selector: parseSelector(obj.on, "long-press.on") };
145098
- if (obj.duration !== void 0) {
145099
- if (typeof obj.duration !== "number" || !Number.isFinite(obj.duration) || obj.duration <= 0) {
145100
- badEntry(
145101
- entry,
145102
- "long-press.duration needs a positive number of milliseconds (e.g. `duration: 1200`)"
145103
- );
145104
- }
145105
- step.duration = obj.duration;
145106
- }
145107
- return step;
145108
- }
145109
- return { kind: "long-press", selector: parseSelector(body, "long-press") };
145110
- }
145111
145099
  function parseTapTimes(raw, entry) {
145112
145100
  if (raw === void 0) return void 0;
145113
145101
  if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1 || raw > 10) {
@@ -145115,23 +145103,32 @@ function parseTapTimes(raw, entry) {
145115
145103
  }
145116
145104
  return raw === 1 ? void 0 : raw;
145117
145105
  }
145118
- function parseTap(body, entry) {
145119
- const obj = body !== null && typeof body === "object" ? body : {};
145120
- if (obj.x !== void 0 || obj.y !== void 0) {
145121
- if (obj.on !== void 0 || obj.text !== void 0 || obj.id !== void 0 || obj.identifier !== void 0 || obj.role !== void 0) {
145122
- badEntry(entry, "tap takes a selector or x/y coordinates, not both");
145123
- }
145124
- if (typeof obj.x !== "number" || typeof obj.y !== "number") {
145125
- badEntry(entry, "a coordinate tap needs numeric x and y");
145126
- }
145127
- if (!Object.keys(obj).every((k) => k === "x" || k === "y" || k === "times")) {
145128
- badEntry(entry, "a coordinate tap takes only { x, y, times }");
145106
+ function parseTarget(raw, where) {
145107
+ if (raw !== null && typeof raw === "object") {
145108
+ const obj = raw;
145109
+ if (obj.x !== void 0 || obj.y !== void 0) {
145110
+ if (obj.text !== void 0 || obj.id !== void 0 || obj.identifier !== void 0 || obj.role !== void 0) {
145111
+ badEntry(raw, `${where} takes a selector or x/y coordinates, not both`);
145112
+ }
145113
+ if (typeof obj.x !== "number" || typeof obj.y !== "number") {
145114
+ badEntry(raw, `${where}: a coordinate target needs numeric x and y`);
145115
+ }
145116
+ if (!(obj.x >= 0 && obj.x <= 1) || !(obj.y >= 0 && obj.y <= 1)) {
145117
+ badEntry(
145118
+ raw,
145119
+ `${where}: coordinates are normalized 0\u20131 fractions of the screen, not pixels`
145120
+ );
145121
+ }
145122
+ if (!Object.keys(obj).every((k) => k === "x" || k === "y")) {
145123
+ badEntry(raw, `${where}: a coordinate target takes only { x, y }`);
145124
+ }
145125
+ return { x: obj.x, y: obj.y };
145129
145126
  }
145130
- const step = { kind: "tap", x: obj.x, y: obj.y };
145131
- const times = parseTapTimes(obj.times, entry);
145132
- if (times !== void 0) step.times = times;
145133
- return step;
145134
145127
  }
145128
+ return { selector: parseSelector(raw, where) };
145129
+ }
145130
+ function parseTap(body, entry) {
145131
+ const obj = body !== null && typeof body === "object" ? body : {};
145135
145132
  if (obj.on !== void 0 || obj.times !== void 0) {
145136
145133
  if (obj.text !== void 0 || obj.id !== void 0 || obj.identifier !== void 0 || obj.role !== void 0) {
145137
145134
  badEntry(
@@ -145139,18 +145136,59 @@ function parseTap(body, entry) {
145139
145136
  'the tap options form takes a nested selector \u2014 e.g. tap: { on: { text: "Photo" }, times: 2 }'
145140
145137
  );
145141
145138
  }
145139
+ if (obj.x !== void 0 || obj.y !== void 0) {
145140
+ badEntry(
145141
+ entry,
145142
+ "the tap options form takes a nested point \u2014 e.g. tap: { on: { x: 0.5, y: 0.5 }, times: 2 }"
145143
+ );
145144
+ }
145142
145145
  if (!Object.keys(obj).every((k) => k === "on" || k === "times")) {
145143
145146
  badEntry(entry, "the tap options form accepts only { on, times }");
145144
145147
  }
145145
145148
  if (obj.on === void 0) {
145146
145149
  badEntry(entry, 'tap with times needs a target \u2014 e.g. tap: { on: "Photo", times: 2 }');
145147
145150
  }
145148
- const step = { kind: "tap", selector: parseSelector(obj.on, "tap.on") };
145151
+ const step = { kind: "tap", ...parseTarget(obj.on, "tap.on") };
145149
145152
  const times = parseTapTimes(obj.times, entry);
145150
145153
  if (times !== void 0) step.times = times;
145151
145154
  return step;
145152
145155
  }
145153
- return { kind: "tap", selector: parseSelector(body, "tap") };
145156
+ return { kind: "tap", ...parseTarget(body, "tap") };
145157
+ }
145158
+ function parseLongPress(body, entry) {
145159
+ const obj = body !== null && typeof body === "object" ? body : {};
145160
+ if (obj.on !== void 0 || obj.duration !== void 0) {
145161
+ if (obj.text !== void 0 || obj.id !== void 0 || obj.identifier !== void 0 || obj.role !== void 0) {
145162
+ badEntry(
145163
+ entry,
145164
+ 'the long-press options form takes a nested selector \u2014 e.g. long-press: { on: { text: "Row" }, duration: 1200 }'
145165
+ );
145166
+ }
145167
+ if (obj.x !== void 0 || obj.y !== void 0) {
145168
+ badEntry(
145169
+ entry,
145170
+ "the long-press options form takes a nested point \u2014 e.g. long-press: { on: { x: 0.5, y: 0.5 }, duration: 1200 }"
145171
+ );
145172
+ }
145173
+ if (!Object.keys(obj).every((k) => k === "on" || k === "duration")) {
145174
+ badEntry(entry, "the long-press options form accepts only { on, duration }");
145175
+ }
145176
+ if (obj.on === void 0) {
145177
+ badEntry(entry, 'long-press needs a target \u2014 e.g. long-press: { on: "Row", duration: 1200 }');
145178
+ }
145179
+ const step = { kind: "long-press", ...parseTarget(obj.on, "long-press.on") };
145180
+ if (obj.duration !== void 0) {
145181
+ if (typeof obj.duration !== "number" || !Number.isFinite(obj.duration) || obj.duration <= 0) {
145182
+ badEntry(
145183
+ entry,
145184
+ "long-press.duration needs a positive number of milliseconds (e.g. `duration: 1200`)"
145185
+ );
145186
+ }
145187
+ step.duration = obj.duration;
145188
+ }
145189
+ return step;
145190
+ }
145191
+ return { kind: "long-press", ...parseTarget(body, "long-press") };
145154
145192
  }
145155
145193
  function fromYamlStep(raw) {
145156
145194
  const entry = raw;
@@ -145980,12 +146018,7 @@ async function captureRunTarget(session, args) {
145980
146018
  try {
145981
146019
  assertSafeFlowName(name);
145982
146020
  const fragPath = path28.join(path28.dirname(session.filePath), `${name}.yaml`);
145983
- const fragment = parseFlow(await fs37.readFile(fragPath, "utf8"));
145984
- if (isE2eFlow(fragment)) {
145985
- return {
145986
- warning: `"${name}" is an e2e flow (starts with a launch step); only fragments compose via run: \u2014 kept the raw flow-execute step`
145987
- };
145988
- }
146021
+ parseFlow(await fs37.readFile(fragPath, "utf8"));
145989
146022
  return { flow: name };
145990
146023
  } catch (err) {
145991
146024
  return {
@@ -146113,9 +146146,8 @@ You can still edit the .yaml file directly afterwards to remove or reorder steps
146113
146146
  case "run":
146114
146147
  return `${n}. run: ${step.flow}`;
146115
146148
  case "tap":
146116
- return `${n}. tap: ${step.selector ? selectorLabel(step.selector) : `(${step.x}, ${step.y})`}`;
146117
146149
  case "long-press":
146118
- return `${n}. long-press: ${selectorLabel(step.selector)}`;
146150
+ return `${n}. ${step.kind}: ${step.selector ? selectorLabel(step.selector) : `(${step.x}, ${step.y})`}`;
146119
146151
  case "type":
146120
146152
  return `${n}. type: ${selectorLabel(step.into)} \u2190 "${step.text}"`;
146121
146153
  case "await":
@@ -146393,20 +146425,23 @@ async function runDirective(env, step) {
146393
146425
  }
146394
146426
  }
146395
146427
  }
146396
- async function runTap(env, target) {
146397
- let point;
146428
+ async function resolveTargetPoint(env, target) {
146398
146429
  if (target.selector) {
146399
146430
  const frame = await waitForFrame(env, target.selector);
146400
- if (frame === "aborted") return ABORTED_OUTCOME;
146431
+ if (frame === "aborted") return { fail: ABORTED_OUTCOME };
146401
146432
  if (!frame) {
146402
- return { ok: false, reason: offscreenHint(target.selector) };
146433
+ return { fail: { ok: false, reason: offscreenHint(target.selector) } };
146403
146434
  }
146404
- point = getDescribeTapPoint(frame);
146405
- } else if (typeof target.x === "number" && typeof target.y === "number") {
146406
- point = { x: target.x, y: target.y };
146407
- } else {
146408
- return { ok: false, reason: "tap needs a selector or x/y coordinates" };
146435
+ return getDescribeTapPoint(frame);
146436
+ }
146437
+ if (typeof target.x === "number" && typeof target.y === "number") {
146438
+ return { x: target.x, y: target.y };
146409
146439
  }
146440
+ return { fail: { ok: false, reason: "gesture needs a selector or x/y coordinates" } };
146441
+ }
146442
+ async function runTap(env, target) {
146443
+ const point = await resolveTargetPoint(env, target);
146444
+ if ("fail" in point) return point.fail;
146410
146445
  await invokeOnDevice(env, "gesture-tap", {
146411
146446
  ...point,
146412
146447
  ...target.times !== void 0 ? { clickCount: target.times } : {}
@@ -146415,12 +146450,8 @@ async function runTap(env, target) {
146415
146450
  }
146416
146451
  var DEFAULT_LONG_PRESS_MS = 800;
146417
146452
  async function runLongPress(env, step) {
146418
- const frame = await waitForFrame(env, step.selector);
146419
- if (frame === "aborted") return ABORTED_OUTCOME;
146420
- if (!frame) {
146421
- return { ok: false, reason: offscreenHint(step.selector) };
146422
- }
146423
- const point = getDescribeTapPoint(frame);
146453
+ const point = await resolveTargetPoint(env, step);
146454
+ if ("fail" in point) return point.fail;
146424
146455
  const duration3 = step.duration ?? DEFAULT_LONG_PRESS_MS;
146425
146456
  if (env.device.platform === "chromium") {
146426
146457
  await invokeOnDevice(env, "gesture-drag", {
@@ -148830,6 +148861,13 @@ async function treeSourceGate(registry2, device, bundleId, signal) {
148830
148861
  async function runLaunch(state3, app) {
148831
148862
  const { registry: registry2, device, signal } = state3;
148832
148863
  if (device.platform === "chromium") {
148864
+ if (state3.chromiumLaunched) {
148865
+ return {
148866
+ ok: false,
148867
+ reason: `chromium launches only the top-level flow's app, once per run \u2014 a nested launch can't boot its own instance and would run against the already-launched app. Nested chromium e2e flows aren't supported: run this flow at the top level, or drop its launch step to make it a fragment.`
148868
+ };
148869
+ }
148870
+ state3.chromiumLaunched = true;
148833
148871
  if (state3.chromiumBooted) {
148834
148872
  if (!await sleepOrAbort(POST_LAUNCH_SETTLE_MS, signal)) return ABORTED_OUTCOME;
148835
148873
  return { ok: true };
@@ -148875,8 +148913,9 @@ function createRunFlowTool(registry2) {
148875
148913
  description: `Run a saved flow from the .argent/flows/ directory.
148876
148914
  Steps run in order: \`launch\` starts an app from scratch (terminate + relaunch) and waits until it is
148877
148915
  ready; \`tool\` calls dispatch through the registry; \`tap\`/\`long-press\`/\`type\` resolve a selector to an
148878
- element and act on it (\`long-press: { on, duration }\` presses and holds); \`scroll-to\` scrolls
148879
- (momentum-free) until a target is visible; \`await\` waits for a UI
148916
+ element and act on it (\`tap: { on, times: 2 }\` double-taps; \`long-press: { on, duration }\` presses and
148917
+ holds; \`tap\`/\`long-press\` alternatively take a raw normalized point \u2014 bare \`{ x, y }\` or \`on: { x, y }\`);
148918
+ \`scroll-to\` scrolls (momentum-free) until a target is visible; \`await\` waits for a UI
148880
148919
  condition; \`wait\` pauses for a fixed number of milliseconds; \`assert\` checks one now; \`snapshot\`
148881
148920
  diffs a screenshot against a stored baseline (a missing baseline fails the step \u2014 set updateBaselines
148882
148921
  to adopt the current screen); \`echo\` annotates; \`run\` executes a referenced fragment inline.
@@ -148920,6 +148959,7 @@ returns a notice with the prerequisite instead of running.`,
148920
148959
  stopped: false,
148921
148960
  pinned: statusBarPinned,
148922
148961
  chromiumBooted: resolved.booted !== null,
148962
+ chromiumLaunched: false,
148923
148963
  ...ctx?.emitProgress ? { onStepReport: ctx.emitProgress } : {}
148924
148964
  };
148925
148965
  try {
@@ -149029,11 +149069,10 @@ function selectorLabel2(sel) {
149029
149069
  function stepTarget(step) {
149030
149070
  switch (step.kind) {
149031
149071
  case "tap":
149072
+ case "long-press":
149032
149073
  if (step.selector) return selectorLabel2(step.selector);
149033
149074
  if (step.x !== void 0 && step.y !== void 0) return `(${step.x}, ${step.y})`;
149034
149075
  return void 0;
149035
- case "long-press":
149036
- return selectorLabel2(step.selector);
149037
149076
  case "type":
149038
149077
  return `into ${selectorLabel2(step.into)}`;
149039
149078
  case "await":
@@ -149109,11 +149148,6 @@ async function execRunStep(state3, step, runStack) {
149109
149148
  } catch (err) {
149110
149149
  return fail(`could not load fragment "${target}": ${errMsg2(err)}`);
149111
149150
  }
149112
- if (isE2eFlow(fragment)) {
149113
- return fail(
149114
- `"${target}" is an e2e flow (starts with a launch step); only fragments can be run from another flow`
149115
- );
149116
- }
149117
149151
  pushReport(state3, { index, kind: "run", status: "pass", flow: target });
149118
149152
  await execSteps(state3, fragment.steps, target, [...runStack, target]);
149119
149153
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/argent",
3
- "version": "0.16.1-next.0",
3
+ "version": "0.16.1-next.2",
4
4
  "description": "MCP server for iOS Simulator and Android Emulator control",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -11,7 +11,7 @@ Flows store **no device id**: the runner binds a device (the single booted one,
11
11
 
12
12
  **Two flow types**
13
13
 
14
- - **e2e** — begins with a `launch:` step, which starts that app from scratch (terminate + relaunch), so the flow controls its own start state. No `executionPrerequisite`. May `run:` fragments; cannot itself be a `run:` target. Record one by adding a `restart-app` of the app under test as the **first** step — it is captured as the `launch` step.
14
+ - **e2e** — begins with a `launch:` step, which starts that app from scratch (terminate + relaunch), so the flow controls its own start state. No `executionPrerequisite`. May `run:` other flows, and (on iOS/Android) may itself be a `run:` target — when nested, its `launch` runs inline, restarting the app for that sub-scenario. **Chromium is the exception:** the runner boots one Electron app per run (the top-level flow's), so a nested chromium e2e flow's `launch` can't boot its own instance and fails the run — keep chromium e2e flows top-level. Record one by adding a `restart-app` of the app under test as the **first** step — it is captured as the `launch` step.
15
15
  - **fragment** — doesn't begin with a launch; runs against the device's current state. May declare an `executionPrerequisite` (a documented entry-state contract). Invoked from other flows via a `run:` step, or directly by you at any time.
16
16
 
17
17
  Both run via `argent flow run <name>` — a fragment simply runs against whatever is on screen (its prerequisite is printed as a reminder). Only e2e flows are meaningful CI/suite entries, since only they give a deterministic verdict from a clean start.
@@ -20,18 +20,18 @@ Both run via `argent flow run <name>` — a fragment simply runs against whateve
20
20
 
21
21
  Beyond raw `tool:` steps and `echo:`, flows support declarative directives interpreted by the runner (they are **not** agent-callable tools). **Every directive hard-stops the flow on failure**; later steps are reported `skip`.
22
22
 
23
- | Directive | YAML | Meaning |
24
- | ------------ | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
25
- | `launch` | `- launch: com.acme.app` or `- launch: { ios: …, android: … }` | start the app from scratch (terminate + relaunch) and wait until ready |
26
- | `tap` | `- tap: Login`, `- tap: { x: 0.5, y: 0.57 }`, `- tap: { on: Login, times: 2 }` | tap by selector (auto-waits) or raw point; `times: 2` = double-tap |
27
- | `long-press` | `- long-press: Row 3` or `- long-press: { on: <sel>, duration: 1200 }` | press and hold an element (default 800ms; Chromium: mouse press-hold) |
28
- | `type` | `- type: { into: email, text: "a@b.com" }` | focus a field, type, then press Enter to submit + dismiss the keyboard |
29
- | `scroll-to` | `- scroll-to: "Order #1234"` (scrolls down) or `- scroll-to: { target: …, direction: right, within: … }` | momentum-free scroll until the target is visible |
30
- | `await` | `- await: { visible: Home }` | wait for a UI condition |
31
- | `wait` | `- wait: 500` | pause for a fixed number of milliseconds (last resort — prefer `await`) |
32
- | `assert` | `- assert: { visible: Welcome }` | check a condition, hard-fail if it never holds |
33
- | `snapshot` | `- snapshot: home` or `- snapshot: { name: home, maxMismatch: 0.5 }` | diff a screenshot against a stored baseline |
34
- | `run` | `- run: login` | execute a fragment's steps inline |
23
+ | Directive | YAML | Meaning |
24
+ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
25
+ | `launch` | `- launch: com.acme.app` or `- launch: { ios: …, android: … }` | start the app from scratch (terminate + relaunch) and wait until ready |
26
+ | `tap` | `- tap: Login`, `- tap: { x: 0.5, y: 0.57 }`, `- tap: { on: Login, times: 2 }`, `- tap: { on: { x: 0.5, y: 0.57 }, times: 2 }` | tap by selector (auto-waits) or raw point; `times` (2 = double-tap) needs the target nested under `on:` — a selector or a point (`{ x, y, times }` is rejected) |
27
+ | `long-press` | `- long-press: Row 3`, `- long-press: { x: 0.5, y: 0.6 }`, `- long-press: { on: <sel>, duration: 1200 }`, `- long-press: { on: { x: 0.5, y: 0.6 }, duration: 1200 }` | press and hold an element or raw point (default 800ms; Chromium: mouse press-hold); `duration` needs the target nested under `on:` — a selector or a point |
28
+ | `type` | `- type: { into: email, text: "a@b.com" }` | focus a field, type, then press Enter to submit + dismiss the keyboard |
29
+ | `scroll-to` | `- scroll-to: "Order #1234"` (scrolls down) or `- scroll-to: { target: …, direction: right, within: … }` | momentum-free scroll until the target is visible |
30
+ | `await` | `- await: { visible: Home }` | wait for a UI condition |
31
+ | `wait` | `- wait: 500` | pause for a fixed number of milliseconds (last resort — prefer `await`) |
32
+ | `assert` | `- assert: { visible: Welcome }` | check a condition, hard-fail if it never holds |
33
+ | `snapshot` | `- snapshot: home` or `- snapshot: { name: home, maxMismatch: 0.5 }` | diff a screenshot against a stored baseline |
34
+ | `run` | `- run: login` | execute another flow's steps inline (fragment or e2e) |
35
35
 
36
36
  ### Selectors
37
37