@swmansion/argent 0.17.1-next.0 → 0.17.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.
@@ -81164,7 +81164,7 @@ var require_websocket = __commonJS({
81164
81164
  var http5 = require("http");
81165
81165
  var net6 = require("net");
81166
81166
  var tls = require("tls");
81167
- var { randomBytes, createHash: createHash3 } = require("crypto");
81167
+ var { randomBytes, createHash: createHash4 } = require("crypto");
81168
81168
  var { Duplex, Readable } = require("stream");
81169
81169
  var { URL: URL2 } = require("url");
81170
81170
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -81832,7 +81832,7 @@ var require_websocket = __commonJS({
81832
81832
  abortHandshake(websocket, socket, "Invalid Upgrade header");
81833
81833
  return;
81834
81834
  }
81835
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
81835
+ const digest = createHash4("sha1").update(key + GUID).digest("base64");
81836
81836
  if (res.headers["sec-websocket-accept"] !== digest) {
81837
81837
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
81838
81838
  return;
@@ -82201,7 +82201,7 @@ var require_websocket_server = __commonJS({
82201
82201
  var EventEmitter2 = require("events");
82202
82202
  var http5 = require("http");
82203
82203
  var { Duplex } = require("stream");
82204
- var { createHash: createHash3 } = require("crypto");
82204
+ var { createHash: createHash4 } = require("crypto");
82205
82205
  var extension2 = require_extension();
82206
82206
  var PerMessageDeflate2 = require_permessage_deflate();
82207
82207
  var subprotocol2 = require_subprotocol();
@@ -82508,7 +82508,7 @@ var require_websocket_server = __commonJS({
82508
82508
  );
82509
82509
  }
82510
82510
  if (this._state > RUNNING) return abortHandshake(socket, 503);
82511
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
82511
+ const digest = createHash4("sha1").update(key + GUID).digest("base64");
82512
82512
  const headers = [
82513
82513
  "HTTP/1.1 101 Switching Protocols",
82514
82514
  "Upgrade: websocket",
@@ -83162,10 +83162,10 @@ function avdRootCandidates() {
83162
83162
  return candidates.filter((p) => Boolean(p && (0, import_node_path6.isAbsolute)(p)));
83163
83163
  }
83164
83164
  async function resolveAvdPath(avdName) {
83165
- const { readFile: readFile11 } = await import("node:fs/promises");
83165
+ const { readFile: readFile12 } = await import("node:fs/promises");
83166
83166
  for (const root2 of avdRootCandidates()) {
83167
83167
  try {
83168
- const content = await readFile11(`${root2}/${avdName}.ini`, "utf-8");
83168
+ const content = await readFile12(`${root2}/${avdName}.ini`, "utf-8");
83169
83169
  const raw = (0, import_ini.parse)(content).path;
83170
83170
  if (typeof raw !== "string") continue;
83171
83171
  const trimmed = raw.trim();
@@ -90969,7 +90969,7 @@ var require_png = __commonJS({
90969
90969
  var Parser = require_parser_async();
90970
90970
  var Packer = require_packer_async();
90971
90971
  var PNGSync = require_png_sync();
90972
- var PNG3 = exports2.PNG = function(options) {
90972
+ var PNG4 = exports2.PNG = function(options) {
90973
90973
  Stream2.call(this);
90974
90974
  options = options || {};
90975
90975
  this.width = options.width | 0;
@@ -90998,9 +90998,9 @@ var require_png = __commonJS({
90998
90998
  this._parser.on("close", this._handleClose.bind(this));
90999
90999
  this._packer.on("error", this.emit.bind(this, "error"));
91000
91000
  };
91001
- util.inherits(PNG3, Stream2);
91002
- PNG3.sync = PNGSync;
91003
- PNG3.prototype.pack = function() {
91001
+ util.inherits(PNG4, Stream2);
91002
+ PNG4.sync = PNGSync;
91003
+ PNG4.prototype.pack = function() {
91004
91004
  if (!this.data || !this.data.length) {
91005
91005
  this.emit("error", "No data provided");
91006
91006
  return this;
@@ -91012,7 +91012,7 @@ var require_png = __commonJS({
91012
91012
  );
91013
91013
  return this;
91014
91014
  };
91015
- PNG3.prototype.parse = function(data, callback) {
91015
+ PNG4.prototype.parse = function(data, callback) {
91016
91016
  if (callback) {
91017
91017
  let onParsed, onError;
91018
91018
  onParsed = function(parsedData) {
@@ -91030,27 +91030,27 @@ var require_png = __commonJS({
91030
91030
  this.end(data);
91031
91031
  return this;
91032
91032
  };
91033
- PNG3.prototype.write = function(data) {
91033
+ PNG4.prototype.write = function(data) {
91034
91034
  this._parser.write(data);
91035
91035
  return true;
91036
91036
  };
91037
- PNG3.prototype.end = function(data) {
91037
+ PNG4.prototype.end = function(data) {
91038
91038
  this._parser.end(data);
91039
91039
  };
91040
- PNG3.prototype._metadata = function(metadata) {
91040
+ PNG4.prototype._metadata = function(metadata) {
91041
91041
  this.width = metadata.width;
91042
91042
  this.height = metadata.height;
91043
91043
  this.emit("metadata", metadata);
91044
91044
  };
91045
- PNG3.prototype._gamma = function(gamma) {
91045
+ PNG4.prototype._gamma = function(gamma) {
91046
91046
  this.gamma = gamma;
91047
91047
  };
91048
- PNG3.prototype._handleClose = function() {
91048
+ PNG4.prototype._handleClose = function() {
91049
91049
  if (!this._parser.writable && !this._packer.readable) {
91050
91050
  this.emit("close");
91051
91051
  }
91052
91052
  };
91053
- PNG3.bitblt = function(src, dst, srcX, srcY, width, height, deltaX, deltaY) {
91053
+ PNG4.bitblt = function(src, dst, srcX, srcY, width, height, deltaX, deltaY) {
91054
91054
  srcX |= 0;
91055
91055
  srcY |= 0;
91056
91056
  width |= 0;
@@ -91072,11 +91072,11 @@ var require_png = __commonJS({
91072
91072
  );
91073
91073
  }
91074
91074
  };
91075
- PNG3.prototype.bitblt = function(dst, srcX, srcY, width, height, deltaX, deltaY) {
91076
- PNG3.bitblt(this, dst, srcX, srcY, width, height, deltaX, deltaY);
91075
+ PNG4.prototype.bitblt = function(dst, srcX, srcY, width, height, deltaX, deltaY) {
91076
+ PNG4.bitblt(this, dst, srcX, srcY, width, height, deltaX, deltaY);
91077
91077
  return this;
91078
91078
  };
91079
- PNG3.adjustGamma = function(src) {
91079
+ PNG4.adjustGamma = function(src) {
91080
91080
  if (src.gamma) {
91081
91081
  for (let y = 0; y < src.height; y++) {
91082
91082
  for (let x = 0; x < src.width; x++) {
@@ -91091,8 +91091,8 @@ var require_png = __commonJS({
91091
91091
  src.gamma = 0;
91092
91092
  }
91093
91093
  };
91094
- PNG3.prototype.adjustGamma = function() {
91095
- PNG3.adjustGamma(this);
91094
+ PNG4.prototype.adjustGamma = function() {
91095
+ PNG4.adjustGamma(this);
91096
91096
  };
91097
91097
  }
91098
91098
  });
@@ -146419,8 +146419,17 @@ function toYamlStep(step) {
146419
146419
  return {
146420
146420
  rotate: step.selector ? { on: selectorToYaml(step.selector), by: step.by } : { by: step.by }
146421
146421
  };
146422
- case "snapshot":
146423
- return step.maxMismatch === void 0 ? { snapshot: step.name } : { snapshot: { name: step.name, maxMismatch: step.maxMismatch } };
146422
+ case "snapshot": {
146423
+ if (step.maxMismatch === void 0 && step.cropOn === void 0) {
146424
+ return { snapshot: step.name };
146425
+ }
146426
+ const body = {
146427
+ name: step.name
146428
+ };
146429
+ if (step.maxMismatch !== void 0) body.maxMismatch = step.maxMismatch;
146430
+ if (step.cropOn !== void 0) body.cropOn = selectorToYaml(step.cropOn);
146431
+ return { snapshot: body };
146432
+ }
146424
146433
  case "tool":
146425
146434
  default: {
146426
146435
  const y = {
@@ -147008,7 +147017,12 @@ function fromYamlStep(raw, whenDepth = 0) {
147008
147017
  if ("snapshot" in raw) {
147009
147018
  const body = raw.snapshot;
147010
147019
  if (body !== null && typeof body === "object" && !Array.isArray(body)) {
147011
- rejectUnknownKeys(raw, body, ["name", "maxMismatch"], "snapshot");
147020
+ rejectUnknownKeys(
147021
+ raw,
147022
+ body,
147023
+ ["name", "maxMismatch", "cropOn"],
147024
+ "snapshot"
147025
+ );
147012
147026
  }
147013
147027
  const b = typeof body === "string" ? { name: body } : body;
147014
147028
  if (!b || typeof b !== "object" || typeof b.name !== "string" || !b.name) {
@@ -147031,6 +147045,9 @@ function fromYamlStep(raw, whenDepth = 0) {
147031
147045
  }
147032
147046
  step.maxMismatch = m;
147033
147047
  }
147048
+ if (b.cropOn !== void 0) {
147049
+ step.cropOn = parseSelector(b.cropOn, "snapshot.cropOn");
147050
+ }
147034
147051
  return step;
147035
147052
  }
147036
147053
  if ("tool" in raw) {
@@ -148439,9 +148456,11 @@ function assertReason(condition, selector, expectedText, textMatch, matches2) {
148439
148456
  }
148440
148457
 
148441
148458
  // ../tool-server/src/tools/flows/flow-visual.ts
148459
+ var import_node_crypto8 = require("node:crypto");
148442
148460
  var fs44 = __toESM(require("node:fs/promises"));
148443
148461
  var os12 = __toESM(require("node:os"));
148444
148462
  var path32 = __toESM(require("node:path"));
148463
+ var import_pngjs3 = __toESM(require_png());
148445
148464
 
148446
148465
  // ../tool-server/src/tools/screenshot-diff/screenshot-diff.ts
148447
148466
  var import_promises11 = __toESM(require("fs/promises"));
@@ -150004,7 +150023,7 @@ async function diffPngFiles(options) {
150004
150023
  decodePngFile(options.baselinePath),
150005
150024
  decodePngFile(options.currentPath)
150006
150025
  ]);
150007
- const normalized = normalizeToCommonSize(decodedBaseline, decodedCurrent);
150026
+ const normalized = options.normalizeSizes === false ? sameDimensionsOrNull(decodedBaseline, decodedCurrent) : normalizeToCommonSize(decodedBaseline, decodedCurrent);
150008
150027
  if (!normalized) {
150009
150028
  return summarizeResult(
150010
150029
  buildDimensionMismatchResult({
@@ -150071,6 +150090,9 @@ function resolveDiffSettings(topMask) {
150071
150090
  function summarizeResult(result) {
150072
150091
  return { ...result, summary: formatScreenshotDiffSummary(result) };
150073
150092
  }
150093
+ function sameDimensionsOrNull(baseline, current) {
150094
+ return baseline.width === current.width && baseline.height === current.height ? { baseline, current } : null;
150095
+ }
150074
150096
  function buildDimensionMismatchResult(params) {
150075
150097
  return {
150076
150098
  totalPixels: params.baseline.width * params.baseline.height,
@@ -150490,6 +150512,15 @@ async function pngDimensions(file2) {
150490
150512
  await fh.close();
150491
150513
  }
150492
150514
  }
150515
+ function cropIdentity(s) {
150516
+ return JSON.stringify([
150517
+ s.text ?? null,
150518
+ s.textMatches ?? null,
150519
+ s.identifier ?? null,
150520
+ s.role ?? null,
150521
+ s.loose ?? false
150522
+ ]);
150523
+ }
150493
150524
  function baselineDir(flowsDir, flowName) {
150494
150525
  return path32.join(flowsDir, "__baselines__", flowName);
150495
150526
  }
@@ -150506,80 +150537,153 @@ async function cleanupDiffDir(dir, keep) {
150506
150537
  } catch {
150507
150538
  }
150508
150539
  }
150540
+ async function cropPngFile(src, dest, frame) {
150541
+ const png = import_pngjs3.PNG.sync.read(await fs44.readFile(src));
150542
+ const left = Math.max(0, Math.round(frame.x * png.width));
150543
+ const top = Math.max(0, Math.round(frame.y * png.height));
150544
+ const right = Math.min(png.width, Math.round((frame.x + frame.width) * png.width));
150545
+ const bottom = Math.min(png.height, Math.round((frame.y + frame.height) * png.height));
150546
+ const w = right - left;
150547
+ const h = bottom - top;
150548
+ if (w < 1 || h < 1) return null;
150549
+ const out = new import_pngjs3.PNG({ width: w, height: h });
150550
+ import_pngjs3.PNG.bitblt(png, out, left, top, w, h, 0, 0);
150551
+ await fs44.writeFile(dest, import_pngjs3.PNG.sync.write(out));
150552
+ return { w, h };
150553
+ }
150509
150554
  async function runSnapshot(env, opts) {
150510
- try {
150511
- await settleTree(env);
150512
- } catch {
150555
+ if (opts.cropOn === void 0) {
150556
+ try {
150557
+ await settleTree(env);
150558
+ } catch {
150559
+ }
150513
150560
  }
150514
150561
  if (env.signal?.aborted) {
150515
150562
  return { status: "skip", reason: "run aborted during snapshot settle" };
150516
150563
  }
150564
+ let cropFrame;
150565
+ if (opts.cropOn !== void 0) {
150566
+ const frame = await waitForFrame(env, opts.cropOn);
150567
+ if (frame === "aborted") {
150568
+ return { status: "skip", reason: "run aborted while resolving cropOn" };
150569
+ }
150570
+ if (frame === void 0) {
150571
+ return { status: "fail", reason: offscreenHint(opts.cropOn) };
150572
+ }
150573
+ cropFrame = frame;
150574
+ }
150517
150575
  const store = requireArtifacts(env.ctx);
150518
150576
  const shot = await invokeOnDevice(env, "screenshot", {
150519
150577
  scale: 1,
150520
150578
  includeImageInContext: false
150521
150579
  });
150522
- const currentPath = shot.image.hostPath;
150523
- const { w, h } = await pngDimensions(currentPath);
150524
- const snapshotKey = `${opts.name}__${env.device.platform}-${w}x${h}`;
150580
+ const { w, h } = await pngDimensions(shot.image.hostPath);
150581
+ const cropSuffix = opts.cropOn === void 0 ? "" : `-crop-${(0, import_node_crypto8.createHash)("sha256").update(cropIdentity(opts.cropOn)).digest("hex").slice(0, 8)}`;
150582
+ const snapshotKey = `${opts.name}__${env.device.platform}-${w}x${h}${cropSuffix}`;
150525
150583
  const key = `${snapshotKey}.png`;
150526
150584
  const dir = baselineDir(opts.flowsDir, opts.flowName);
150527
150585
  const baselinePath = path32.join(dir, key);
150528
- const exists2 = await fs44.access(baselinePath).then(() => true).catch(() => false);
150529
- if (opts.updateBaselines) {
150530
- await fs44.mkdir(dir, { recursive: true });
150531
- await fs44.copyFile(currentPath, baselinePath);
150532
- const baseline = await store.register(baselinePath, { mimeType: "image/png" });
150533
- return {
150534
- status: "pass",
150535
- reason: exists2 ? `baseline updated (${key})` : `baseline written (${key})`,
150536
- snapshotKey,
150537
- artifacts: { baseline }
150538
- };
150539
- }
150540
- if (!exists2) {
150541
- return {
150542
- status: "fail",
150543
- reason: `no baseline for "${opts.name}" on this device class \u2014 expected ${baselinePath}, nothing was compared. Run with updateBaselines (--update-baselines) to adopt the current screen, then review and commit it`,
150544
- snapshotKey,
150545
- artifacts: { current: shot.image }
150546
- };
150547
- }
150548
- const outputDir = await fs44.mkdtemp(path32.join(os12.tmpdir(), "argent-flow-diff-"));
150549
- let keepInOutputDir;
150586
+ let currentPath = shot.image.hostPath;
150587
+ let cropDir;
150588
+ let keepCropped = false;
150589
+ const currentArtifact = async () => {
150590
+ if (cropDir === void 0) return shot.image;
150591
+ keepCropped = true;
150592
+ return store.register(currentPath, {
150593
+ mimeType: "image/png",
150594
+ filename: `${snapshotKey}-current.png`
150595
+ });
150596
+ };
150550
150597
  try {
150551
- const result = await diffPngFiles({ baselinePath, currentPath, outputDir });
150552
- if (result.dimensionMismatch) {
150553
- const { expected, actual } = result.dimensionMismatch;
150598
+ if (cropFrame !== void 0) {
150599
+ cropDir = await fs44.mkdtemp(path32.join(os12.tmpdir(), "argent-flow-crop-"));
150600
+ const croppedPath = path32.join(cropDir, key);
150601
+ const cropped = await cropPngFile(shot.image.hostPath, croppedPath, cropFrame);
150602
+ if (cropped === null) {
150603
+ return {
150604
+ status: "fail",
150605
+ reason: `cropOn matched ${describeSelector(opts.cropOn)} but its on-screen region is empty at this resolution \u2014 nothing was compared`,
150606
+ snapshotKey,
150607
+ // No crop exists to show, so attach the full capture (already registered).
150608
+ artifacts: { current: shot.image }
150609
+ };
150610
+ }
150611
+ currentPath = croppedPath;
150612
+ }
150613
+ const exists2 = await fs44.access(baselinePath).then(() => true).catch(() => false);
150614
+ if (opts.updateBaselines) {
150615
+ await fs44.mkdir(dir, { recursive: true });
150616
+ await fs44.copyFile(currentPath, baselinePath);
150617
+ const baseline = await store.register(baselinePath, { mimeType: "image/png" });
150554
150618
  return {
150555
- status: "fail",
150556
- reason: `baseline is ${expected.width}x${expected.height} but the capture is ${actual.width}x${actual.height} (${key}) \u2014 nothing was compared`,
150619
+ status: "pass",
150620
+ reason: exists2 ? `baseline updated (${key})` : `baseline written (${key})`,
150557
150621
  snapshotKey,
150558
- artifacts: {
150559
- baseline: await store.register(baselinePath, { mimeType: "image/png" }),
150560
- current: shot.image
150561
- }
150622
+ artifacts: { baseline }
150562
150623
  };
150563
150624
  }
150564
- const within = result.mismatchPercentage <= opts.maxMismatch;
150565
- const reason = `diff ${result.mismatchPercentage.toFixed(2)}% ${within ? "\u2264" : ">"} ${opts.maxMismatch}% (${key})`;
150566
- if (within) {
150567
- return { status: "pass", reason };
150625
+ if (!exists2) {
150626
+ return {
150627
+ status: "fail",
150628
+ reason: `no baseline for "${opts.name}" on this device class \u2014 expected ${baselinePath}, nothing was compared. Run with updateBaselines (--update-baselines) to adopt the current screen, then review and commit it`,
150629
+ snapshotKey,
150630
+ artifacts: { current: await currentArtifact() }
150631
+ };
150568
150632
  }
150569
- const artifacts = {
150570
- baseline: await store.register(baselinePath, { mimeType: "image/png" }),
150571
- current: shot.image
150572
- };
150573
- if (result.contextDiffPath) {
150574
- artifacts.diff = await store.register(result.contextDiffPath, {
150575
- mimeType: "image/png",
150576
- filename: `${snapshotKey}-diff.png`
150633
+ const outputDir = await fs44.mkdtemp(path32.join(os12.tmpdir(), "argent-flow-diff-"));
150634
+ let keepInOutputDir;
150635
+ try {
150636
+ const result = await diffPngFiles({
150637
+ baselinePath,
150638
+ currentPath,
150639
+ outputDir,
150640
+ // A crop compares EVERY pixel of the element's region — no masking,
150641
+ // wherever the crop sits. Masking a crop's overlap with the screen's
150642
+ // top status-bar band would degenerate into comparing nothing for an
150643
+ // element inside the band (a vacuous pass); a crop overlapping the
150644
+ // band leans on best-effort pinStatusBar until frame resolution
150645
+ // asserts the element clears it.
150646
+ topMask: cropFrame === void 0 ? "status-bar" : "none",
150647
+ // Crop dimensions track the element — size drift must hard-fail below
150648
+ // instead of being resampled away like a full-screen scale difference.
150649
+ ...cropFrame !== void 0 && { normalizeSizes: false }
150577
150650
  });
150578
- keepInOutputDir = result.contextDiffPath;
150651
+ if (result.dimensionMismatch) {
150652
+ const { expected, actual } = result.dimensionMismatch;
150653
+ return {
150654
+ status: "fail",
150655
+ reason: `baseline is ${expected.width}x${expected.height} but the ${opts.cropOn ? "cropOn region" : "capture"} is ${actual.width}x${actual.height} (${key}) \u2014 nothing was compared` + (opts.cropOn ? `. The element's size drifted \u2014 crop a fixed-size container, or re-adopt with updateBaselines` : ""),
150656
+ snapshotKey,
150657
+ artifacts: {
150658
+ baseline: await store.register(baselinePath, { mimeType: "image/png" }),
150659
+ current: await currentArtifact()
150660
+ }
150661
+ };
150662
+ }
150663
+ const within = result.mismatchPercentage <= opts.maxMismatch;
150664
+ const reason = `diff ${result.mismatchPercentage.toFixed(2)}% ${within ? "\u2264" : ">"} ${opts.maxMismatch}% (${key})`;
150665
+ if (within) {
150666
+ return { status: "pass", reason };
150667
+ }
150668
+ const artifacts = {
150669
+ baseline: await store.register(baselinePath, { mimeType: "image/png" }),
150670
+ current: await currentArtifact()
150671
+ };
150672
+ if (result.contextDiffPath) {
150673
+ artifacts.diff = await store.register(result.contextDiffPath, {
150674
+ mimeType: "image/png",
150675
+ filename: `${snapshotKey}-diff.png`
150676
+ });
150677
+ keepInOutputDir = result.contextDiffPath;
150678
+ }
150679
+ return { status: "fail", reason, snapshotKey, artifacts };
150680
+ } finally {
150681
+ await cleanupDiffDir(outputDir, keepInOutputDir);
150579
150682
  }
150580
- return { status: "fail", reason, snapshotKey, artifacts };
150581
150683
  } finally {
150582
- await cleanupDiffDir(outputDir, keepInOutputDir);
150684
+ if (cropDir !== void 0) {
150685
+ await cleanupDiffDir(cropDir, keepCropped ? currentPath : void 0);
150686
+ }
150583
150687
  }
150584
150688
  }
150585
150689
 
@@ -150795,8 +150899,9 @@ holds; \`tap\`/\`long-press\` alternatively take a raw normalized point \u2014 b
150795
150899
  two-finger rotation gesture (\`rotate: { on?, by }\` \u2014 degrees, + clockwise, within \xB13000\xB0; screen center
150796
150900
  when \`on\` is omitted; distinct from the \`rotate\` tool, which changes device orientation); \`await\` waits
150797
150901
  for a UI condition; \`wait\` pauses for a fixed number of milliseconds; \`assert\` checks one now; \`snapshot\`
150798
- diffs a screenshot against a stored baseline (a missing baseline fails the step \u2014 set updateBaselines
150799
- to adopt the current screen); \`echo\` annotates; \`run\` executes a referenced fragment inline.
150902
+ diffs a screenshot \u2014 or, with \`cropOn: <selector>\`, one element's cropped region \u2014 against a stored
150903
+ baseline (a missing baseline fails the step \u2014 set updateBaselines to adopt the current screen; a
150904
+ cropped element whose size drifted fails on dimensions); \`echo\` annotates; \`run\` executes a referenced fragment inline.
150800
150905
  A \`when:\` block (condition + \`steps:\`, no else) runs its steps only if the condition holds \u2014
150801
150906
  checked once with the short assert grace \u2014 for one-sided divergences like interstitials and coach
150802
150907
  marks; a skipped block reports distinctly and failures inside an entered block are real failures.
@@ -150987,7 +151092,7 @@ function stepTarget(step) {
150987
151092
  return step.selector ? `${selectorLabel2(step.selector)} (${by})` : by;
150988
151093
  }
150989
151094
  case "snapshot":
150990
- return `"${step.name}"`;
151095
+ return step.cropOn ? `"${step.name}" cropOn ${selectorLabel2(step.cropOn)}` : `"${step.name}"`;
150991
151096
  default:
150992
151097
  return void 0;
150993
151098
  }
@@ -151191,7 +151296,8 @@ async function execLeafStep(state3, step, index, scope) {
151191
151296
  flowName: state3.topFlowName,
151192
151297
  name: step.name,
151193
151298
  maxMismatch: step.maxMismatch ?? DEFAULT_MAX_MISMATCH,
151194
- updateBaselines: state3.updateBaselines
151299
+ updateBaselines: state3.updateBaselines,
151300
+ cropOn: step.cropOn
151195
151301
  });
151196
151302
  return {
151197
151303
  ...base,
@@ -151799,7 +151905,7 @@ var dismissUpdateTool = {
151799
151905
  };
151800
151906
 
151801
151907
  // ../tool-server/src/tools/screenshot-diff/index.ts
151802
- var import_node_crypto8 = __toESM(require("node:crypto"));
151908
+ var import_node_crypto9 = __toESM(require("node:crypto"));
151803
151909
  var import_promises13 = __toESM(require("fs/promises"));
151804
151910
  var import_node_os13 = __toESM(require("node:os"));
151805
151911
  var import_path10 = __toESM(require("path"));
@@ -151885,7 +151991,7 @@ async function resolveOutputDir(params, options) {
151885
151991
  const dir = import_path10.default.join(
151886
151992
  import_node_os13.default.tmpdir(),
151887
151993
  "argent-screenshot-diff",
151888
- import_node_crypto8.default.randomBytes(6).toString("hex")
151994
+ import_node_crypto9.default.randomBytes(6).toString("hex")
151889
151995
  );
151890
151996
  await import_promises13.default.mkdir(dir, { recursive: true });
151891
151997
  return dir;
@@ -151962,7 +152068,7 @@ async function captureLiveInput(params) {
151962
152068
  } catch {
151963
152069
  capture = await params.captureScreenshot(params.api, params.rotation, params.signal);
151964
152070
  }
151965
- const suffix = import_node_crypto8.default.randomBytes(4).toString("hex");
152071
+ const suffix = import_node_crypto9.default.randomBytes(4).toString("hex");
151966
152072
  const destination = import_path10.default.join(params.outputDir, `${params.name}-${suffix}.live.png`);
151967
152073
  await import_promises13.default.mkdir(params.outputDir, { recursive: true });
151968
152074
  await import_promises13.default.copyFile(capture.path, destination);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/argent",
3
- "version": "0.17.1-next.0",
3
+ "version": "0.17.1-next.2",
4
4
  "description": "MCP server for iOS Simulator and Android Emulator control",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -32,7 +32,7 @@ Beyond raw `tool:` steps and `echo:`, flows support declarative directives inter
32
32
  | `await` | `- await: { visible: Home }` | wait for a UI condition |
33
33
  | `wait` | `- wait: 500` | pause for a fixed number of milliseconds (last resort — prefer `await`) |
34
34
  | `assert` | `- assert: { visible: Welcome }` | check a condition, hard-fail if it never holds |
35
- | `snapshot` | `- snapshot: home` or `- snapshot: { name: home, maxMismatch: 0.5 }` | diff a screenshot against a stored baseline |
35
+ | `snapshot` | `- snapshot: home` or `- snapshot: { name: home, maxMismatch: 0.5, cropOn: { id: order-summary } }` | diff a screenshot — or one element's region — against a stored baseline |
36
36
  | `run` | `- run: login` | execute another flow's steps inline (fragment or e2e) |
37
37
  | `when` | `- when: { visible: "What's new" }` + `steps: [...]` | run a guarded step block only when the condition holds (no else) |
38
38
 
@@ -67,6 +67,10 @@ Never record a real credential into a flow — the YAML is committed to the repo
67
67
 
68
68
  `scroll-to` takes an optional `direction` (`up` | `down` | `left` | `right`, default `down` — so the common case is just `- scroll-to: <selector>`) and optionally a `within: <selector>` that anchors the scroll inside a specific container — required to drive a **nested** scroller (e.g. a horizontal carousel inside a vertical list), since the device can't be asked which container to scroll. It scrolls in bounded momentum-free increments, re-checks after each, and stops if a scroll reveals nothing new (end of the container). `tap`/`type` do **not** scroll — add a `scroll-to` before any target that may be off-screen. It's a no-op when the target is already visible, so a defensive `scroll-to` costs nothing on replay and keeps the flow working on smaller screens.
69
69
 
70
+ ### `snapshot` cropping
71
+
72
+ `cropOn: <selector>` narrows a snapshot's comparison to one element's region — `- snapshot: { name: cart-total, cropOn: { id: order-summary } }`. The selector resolves like a directive target (settled tree, auto-wait, the standard not-found failure; selector-only, no point form), and the **cropped** image is what gets compared, stored as the baseline, and reported as the `current` artifact; the baseline filename still keys on the full capture's resolution — plus a `-crop-<hash>` selector suffix — so device-class drift is still caught. A cropped comparison never masks any region — every pixel of the crop is compared — so prefer elements clear of the top status-bar band (a crop overlapping it leans on best-effort status-bar pinning, and the clock/battery may diff). Crop **fixed-size containers**, addressed by `id`: a text selector resolves to the smallest matching node — typically the label itself, whose frame tracks text metrics — and the crop tracks the element's frame, so an element that grew or shrank by a pixel fails on dimensions ("nothing was compared") rather than on content. Baseline storage and seeding are covered under _Standalone runner_.
73
+
70
74
  ### TV targets (Vega)
71
75
 
72
76
  A Vega (Fire TV) device is remote-driven — there is no touch input, so the touch directives (`tap`, `long-press`, `type`, `scroll-to`, `pinch`, `rotate`) fail on it with guidance. Drive focus with `tool: tv-remote` steps and type with `tool: keyboard` instead; everything else (`launch`, `await`, `assert`, `wait`, `snapshot`, `echo`, `run`, selectors) works unchanged — the tree comes from the on-device automation toolkit, which attaches at app launch (the `launch` step waits for it, so a leading `launch` also guarantees selectors resolve).