@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.
- package/dist/tool-server.cjs +191 -85
- package/package.json +1 -1
- package/skills/argent-create-flow/SKILL.md +5 -1
package/dist/tool-server.cjs
CHANGED
|
@@ -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:
|
|
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 =
|
|
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:
|
|
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 =
|
|
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:
|
|
83165
|
+
const { readFile: readFile12 } = await import("node:fs/promises");
|
|
83166
83166
|
for (const root2 of avdRootCandidates()) {
|
|
83167
83167
|
try {
|
|
83168
|
-
const content = await
|
|
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
|
|
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(
|
|
91002
|
-
|
|
91003
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91033
|
+
PNG4.prototype.write = function(data) {
|
|
91034
91034
|
this._parser.write(data);
|
|
91035
91035
|
return true;
|
|
91036
91036
|
};
|
|
91037
|
-
|
|
91037
|
+
PNG4.prototype.end = function(data) {
|
|
91038
91038
|
this._parser.end(data);
|
|
91039
91039
|
};
|
|
91040
|
-
|
|
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
|
-
|
|
91045
|
+
PNG4.prototype._gamma = function(gamma) {
|
|
91046
91046
|
this.gamma = gamma;
|
|
91047
91047
|
};
|
|
91048
|
-
|
|
91048
|
+
PNG4.prototype._handleClose = function() {
|
|
91049
91049
|
if (!this._parser.writable && !this._packer.readable) {
|
|
91050
91050
|
this.emit("close");
|
|
91051
91051
|
}
|
|
91052
91052
|
};
|
|
91053
|
-
|
|
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
|
-
|
|
91076
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91095
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
150511
|
-
|
|
150512
|
-
|
|
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
|
|
150523
|
-
const
|
|
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
|
-
|
|
150529
|
-
|
|
150530
|
-
|
|
150531
|
-
|
|
150532
|
-
|
|
150533
|
-
|
|
150534
|
-
|
|
150535
|
-
|
|
150536
|
-
snapshotKey
|
|
150537
|
-
|
|
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
|
-
|
|
150552
|
-
|
|
150553
|
-
const
|
|
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: "
|
|
150556
|
-
reason: `baseline
|
|
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
|
-
|
|
150565
|
-
|
|
150566
|
-
|
|
150567
|
-
|
|
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
|
|
150570
|
-
|
|
150571
|
-
|
|
150572
|
-
|
|
150573
|
-
|
|
150574
|
-
|
|
150575
|
-
|
|
150576
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
150799
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
@@ -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 }`
|
|
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).
|