Files
software-mansion__argent/packages/tool-server/test/flows/flow-snapshot-step.test.ts
Jakub Piasecki e23f0e285e feat(flow): snapshot cropOn for single-element visual diffs (#516)
`snapshot` gains an optional `cropOn: <selector>` — diff one element's
region instead of the whole screen:

`- snapshot: { name: cart-total, cropOn: "Order summary" }`

- The selector resolves like a directive target: settled tree,
auto-wait, the standard
not-found failure. Selector-only — no point form (a point has no extent
to crop to).
- The frame resolves **before** the capture, and the **cropped** image
is what gets
compared, stored as the baseline, and reported as the `current`
artifact. The pixel
rect is the normalized frame × capture size; the crop is a pngjs
`bitblt` written to
  a swept scratch dir and registered only when a failure returns it.
- The baseline key stays on the **full** capture's WxH (device-class
identity), so the
previously unreachable `dimensionMismatch` guard becomes load-bearing:
an element that
grew or shrank fails with "nothing was compared" plus a
crop-fixed-size-containers
hint. Deliberately strict — a 1px layout drift fails on dimensions, not
content (the
  same tradeoff as Playwright element screenshots).
- A tree-source outage while resolving `cropOn` errors the step instead
of degrading to
  a full-screen compare against a cropped baseline.
- No tool-schema changes; `snapshotKey` and `--output` behavior ride
along unchanged.

Tests: parse/serialize round-trip + rejection cases, and runSnapshot
coverage with real
PNGs (crop dims, key vs. content, artifact retention/sweep, unresolved
selector, abort,
size-drift message, sub-pixel region).
2026-07-27 10:07:49 +02:00

61 lines
2.1 KiB
TypeScript

import { describe, it, expect, vi } from "vitest";
import { createFlowTestHarness } from "./harness";
import { runSnapshot } from "../../src/tools/flows/flow-visual";
// Mock ONLY runSnapshot: these tests pin the YAML-to-execution join in
// flow-run's snapshot arm (the single line threading the parsed step into
// runSnapshot's opts), which the flow-visual suite — calling runSnapshot
// directly with hand-built opts — cannot see. Dropping `cropOn: step.cropOn`
// there would keep every other test green while a declared crop silently ran
// as a full-screen snapshot.
vi.mock("../../src/tools/flows/flow-visual", async (importOriginal) => ({
...(await importOriginal<typeof import("../../src/tools/flows/flow-visual")>()),
runSnapshot: vi.fn(),
}));
const { writeFlow, run } = createFlowTestHarness({
tempDirectoryPrefix: "flow-snapshot-step-",
reset: () => {
vi.mocked(runSnapshot).mockReset();
vi.mocked(runSnapshot).mockResolvedValue({
status: "pass",
reason: "diff 0.00% ≤ 0.5% (row__ios-390x844.png)",
});
},
});
describe("snapshot step wiring", () => {
it("threads the parsed step — name, maxMismatch, cropOn — into runSnapshot", async () => {
await writeFlow("crop", {
executionPrerequisite: "",
steps: [{ kind: "snapshot", name: "row", maxMismatch: 1.5, cropOn: { identifier: "hdr" } }],
});
const result = await run("crop");
expect(result.steps).toEqual([expect.objectContaining({ status: "pass" })]);
expect(vi.mocked(runSnapshot)).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
name: "row",
maxMismatch: 1.5,
cropOn: expect.objectContaining({ identifier: "hdr" }),
})
);
});
it("passes no cropOn for a plain snapshot step", async () => {
await writeFlow("plain", {
executionPrerequisite: "",
steps: [{ kind: "snapshot", name: "home" }],
});
await run("plain");
expect(vi.mocked(runSnapshot)).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ name: "home", cropOn: undefined })
);
});
});