Files
explore/test/node/build-diff.test.mjs
devin-ai-integration[bot]andbrent 9063de5c52 Inspector: stack Before over After for landscape screenshots (#24)
Co-authored-by: brent <[email protected]>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-18 16:55:35 -07:00

213 lines
10 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import { createBuildWorkspaceFixture } from "../helpers/build-workspace-fixture.mjs";
const { tsImport } = await import("tsx/esm/api");
const diffModel = await tsImport("../../viewer/src/utils/build-diff.ts", import.meta.url);
const { removedScreenId } = await tsImport("../../src/contracts/comparison-projection.ts", import.meta.url);
test("reviewed build diff preserves exact cross-build evidence and split/merge status", async () => {
const fixture = await createBuildWorkspaceFixture();
const currentBuild = fixture.catalog.builds.find((build) => build.id === "build-2.0.0");
const baselineBuild = fixture.catalog.builds.find((build) => build.id === "build-1.0.0");
assert.ok(currentBuild);
assert.ok(baselineBuild);
const updatedEntry = fixture.comparison.entries.find((entry) => entry.id === "updated-profile");
assert.ok(updatedEntry);
updatedEntry.changeSummary = "The profile layout now emphasizes account controls.";
for (const id of ["removed-legacy", "added-card", "split-settings"]) {
const legacyEntry = fixture.comparison.entries.find((entry) => entry.id === id);
assert.ok(legacyEntry);
delete legacyEntry.changeSummary;
}
const diff = diffModel.buildDiff({
currentBuild,
currentMap: fixture.currentMap,
baselineBuild,
baselineMap: fixture.baselineMap,
comparison: fixture.comparison,
});
assert.deepEqual(diff.counts, { added: 1, modified: 4, removed: 1, unchanged: 1, "needs-review": 0, "source-affected": 0, "not-compared": 0, unavailable: 0, variant: 0 });
assert.equal(diff.map.screens.length, fixture.currentMap.screens.length + 1);
assert.equal(diff.screens.get("shared-home")?.status, "unchanged");
assert.equal(diff.screens.get("current-profile")?.status, "modified");
assert.equal(diff.screens.get("current-added")?.status, "added");
assert.equal(diff.screens.get("current-split-a")?.status, "modified");
assert.equal(diff.screens.get("current-split-b")?.status, "modified");
assert.equal(diff.screens.get("current-merged")?.status, "modified");
// The reviewed secondary observation, not the screen's primary/latest fallback,
// supplies the current side of this comparison.
const updated = diff.screens.get("current-profile");
assert.equal(updated?.changeSummary, updatedEntry.changeSummary);
assert.notEqual(updated?.changeSummary, updatedEntry.rationale);
assert.match(
updated?.beforeScreenshot,
/^\/api\/builds\/build-1\.0\.0\/artifacts\/artifacts\/screenshots\/baseline-profile-primary\.svg\?sha256=[a-f0-9]{64}$/,
);
assert.match(
updated?.afterScreenshot,
/^\/api\/builds\/build-2\.0\.0\/artifacts\/artifacts\/screenshots\/current-profile-reviewed\.svg\?sha256=[a-f0-9]{64}$/,
);
// Both builds intentionally use this relative path. Build qualification is what keeps
// visually distinct evidence from colliding or borrowing across maps.
const unchanged = diff.screens.get("shared-home");
assert.equal(unchanged?.changeSummary, "“shared home” is unchanged.");
assert.match(
unchanged?.beforeScreenshot,
/^\/api\/builds\/build-1\.0\.0\/artifacts\/artifacts\/screenshots\/shared-screen\.svg\?sha256=[a-f0-9]{64}$/,
);
assert.match(
unchanged?.afterScreenshot,
/^\/api\/builds\/build-2\.0\.0\/artifacts\/artifacts\/screenshots\/shared-screen\.svg\?sha256=[a-f0-9]{64}$/,
);
assert.notEqual(unchanged?.beforeScreenshot, unchanged?.afterScreenshot);
const removedId = removedScreenId("build-1.0.0", "baseline-legacy");
const removed = diff.screens.get(removedId);
assert.equal(removed?.status, "removed");
assert.equal(removed?.changeSummary, "Removed “baseline legacy”.");
assert.match(
removed?.beforeScreenshot,
/^\/api\/builds\/build-1\.0\.0\/artifacts\/artifacts\/screenshots\/baseline-legacy-primary\.svg\?sha256=[a-f0-9]{64}$/,
);
assert.equal(removed?.afterScreenshot, undefined);
assert.ok(diff.index.screens.has(removedId));
assert.equal(diff.index.screens.has("baseline-split"), false);
assert.equal(diff.index.screens.has("baseline-merge-a"), false);
assert.equal(diff.index.screens.has("baseline-merge-b"), false);
const added = diff.screens.get("current-added");
assert.equal(added?.beforeScreenshot, undefined);
assert.equal(added?.changeSummary, "Added “current added”.");
assert.match(
added?.afterScreenshot,
/^\/api\/builds\/build-2\.0\.0\/artifacts\/artifacts\/screenshots\/current-added-primary\.svg\?sha256=[a-f0-9]{64}$/,
);
assert.equal(
diff.screens.get("current-split-a")?.changeSummary,
"Changed “baseline split” to “current split a”.",
);
});
test("a screen diff carries the compared screenshot's recorded shape, After's first", async () => {
const fixture = await createBuildWorkspaceFixture();
const currentBuild = fixture.catalog.builds.find((build) => build.id === "build-2.0.0");
const baselineBuild = fixture.catalog.builds.find((build) => build.id === "build-1.0.0");
const artifact = (map, id) => map.artifacts.find((entry) => entry.id === `artifact-observation-${id}`);
// The reviewed After turned landscape; Before stays portrait.
Object.assign(artifact(fixture.currentMap, "build-2.0.0-current-profile-reviewed"), { width: 60, height: 30 });
// A capture without a recorded size (not a PNG) gives the frame nothing to take.
const added = artifact(fixture.currentMap, "build-2.0.0-current-added-primary");
delete added.width;
delete added.height;
const diff = diffModel.buildDiff({
currentBuild,
currentMap: fixture.currentMap,
baselineBuild,
baselineMap: fixture.baselineMap,
comparison: fixture.comparison,
});
assert.equal(diff.screens.get("current-profile")?.screenshotAspect, 2);
assert.equal(diff.screens.get(removedScreenId("build-1.0.0", "baseline-legacy"))?.screenshotAspect, 0.5);
assert.equal(diff.screens.get("current-added")?.screenshotAspect, undefined);
});
test("catalog route context contains only the two real builds", async () => {
const fixture = await createBuildWorkspaceFixture();
assert.deepEqual(diffModel.routeContextForCatalog(fixture.catalog), {
defaultBaseline: "build-1.0.0",
baselines: ["build-1.0.0"],
});
const catalogWithUnrelatedEntries = {
...fixture.catalog,
builds: [
...fixture.catalog.builds,
{ id: "placeholder-update", kind: "build", version: "EAS Update", mapDir: "unused" },
{ id: "placeholder-pr", kind: "build", version: "PR 482", mapDir: "unused" },
],
};
assert.deepEqual(diffModel.routeContextForCatalog(catalogWithUnrelatedEntries), {
defaultBaseline: "build-1.0.0",
baselines: ["build-1.0.0"],
});
});
test("baseline gaps survive the diff projection and cannot certify an unreviewed screen as unchanged", async () => {
const fixture = await createBuildWorkspaceFixture();
const currentBuild = fixture.catalog.builds.find(build => build.id === "build-2.0.0");
const baselineBuild = fixture.catalog.builds.find(build => build.id === "build-1.0.0");
const removed = fixture.baselineMap.screens.find(screen => screen.id === "baseline-legacy");
const removedObservation = fixture.baselineMap.observations.find(observation => observation.screenId === removed.id);
fixture.baselineMap.gaps.push({ id: "unverified-legacy", kind: "coverage", status: "open", screenId: removed.id,
observationId: removedObservation.id, message: "Captured; destination unverified", createdAt: "2026-08-25T09:00:00.000Z" });
const entry = fixture.comparison.entries.find(entry => entry.id === "updated-profile");
entry.status = "source-affected";
const diff = diffModel.buildDiff({ currentBuild, baselineBuild, currentMap: fixture.currentMap,
baselineMap: fixture.baselineMap, comparison: fixture.comparison,
review: { surfaces: { "current-profile": { reviewerChanges: [], runtimeEvidenceRefs: [], noVisibleChange: true } }, runtimeEvidence: {} },
});
const removedId = removedScreenId(baselineBuild.id, removed.id);
const gap = diff.map.gaps.find(gap => gap.screenId === removedId);
assert.ok(gap);
assert.equal(gap.status, "open");
assert.match(gap.message, /^Baseline: Captured; destination unverified$/);
assert.ok(diff.index.observations.has(gap.observationId), "baseline gap observations use their imported namespace");
assert.equal(diff.map.run.status, "partial");
assert.equal(diff.screens.get(removedId).status, "removed");
assert.equal(diff.screens.get("current-profile").status, "source-affected");
assert.equal(diff.screens.get("current-profile").noVisibleChange, false);
});
test("baseline-only namespacing refuses collisions with current evidence", async (t) => {
const fixture = await createBuildWorkspaceFixture();
const currentBuild = fixture.catalog.builds.find((build) => build.id === "build-2.0.0");
const baselineBuild = fixture.catalog.builds.find((build) => build.id === "build-1.0.0");
assert.ok(currentBuild);
assert.ok(baselineBuild);
const removedScreen = fixture.baselineMap.screens.find((screen) => screen.id === "baseline-legacy");
assert.ok(removedScreen);
const removedObservation = fixture.baselineMap.observations.find(
(observation) => observation.screenId === removedScreen.id,
);
assert.ok(removedObservation);
const removedArtifact = fixture.baselineMap.artifacts.find(
(artifact) => artifact.id === removedObservation.screenshotArtifactId,
);
assert.ok(removedArtifact);
const build = (currentMap) => diffModel.buildDiff({
currentBuild,
currentMap,
baselineBuild,
baselineMap: fixture.baselineMap,
comparison: fixture.comparison,
});
const clone = () => structuredClone(fixture.currentMap);
await t.test("screen id", () => {
const currentMap = clone();
currentMap.screens[0].id = removedScreenId(baselineBuild.id, removedScreen.id);
assert.throws(() => build(currentMap), /Baseline-only screen id collides/);
});
await t.test("observation id", () => {
const currentMap = clone();
currentMap.observations[0].id = `removed:${baselineBuild.id}:observation:${removedObservation.id}`;
assert.throws(() => build(currentMap), /Baseline-only observation id collides/);
});
await t.test("artifact id", () => {
const currentMap = clone();
currentMap.artifacts[0].id = `removed:${baselineBuild.id}:artifact:${removedArtifact.id}`;
assert.throws(() => build(currentMap), /Baseline-only artifact id collides/);
});
});