mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
feat(cli): persistence-tiered findings, frozen-sweep guard, occlusion coverage
Layout findings now distinguish held defects from entrance/exit transients: a dynamic issue seen at a single grid sample demotes to info, while content_overlap held across two-plus samples (or 500ms+) promotes to error, resolving the long-standing re-promotion TODO. Static compositions keep their severity. check gains a sweep_static error when a 3s+ composition shows zero geometry change across every sample (a frozen timeline makes every green verdict unreliable); skipped when the motion sidecar already reported motion_frozen. text_occluded findings carry a coveredFraction; atomic labels (short, no whitespace) flag on any cover while prose needs 15%, since partial cover changes what a short label reads as. Deprecation-test scaffolding consolidates into deprecationTestHarness; tier logic and logger tests restructured under the complexity gate without suppression markers. Detection mechanics adapted from Adam Rosler's open-sourced visual-linter design (github.com/Adam-Rosler/hyperframes-visual-linter-design); the elementFromPoint paint model, opt-out attributes, and single-audit architecture are unchanged.
This commit is contained in:
@@ -108,10 +108,14 @@ function anchor(selector: string, time: number): CheckAnchor {
|
||||
};
|
||||
}
|
||||
|
||||
function layoutIssue(severity: "error" | "warning" | "info" = "error"): AnchoredLayoutIssue {
|
||||
function layoutIssue(
|
||||
severity: "error" | "warning" | "info" = "error",
|
||||
overrides: { time?: number; code?: AnchoredLayoutIssue["code"] } = {},
|
||||
): AnchoredLayoutIssue {
|
||||
const time = overrides.time ?? 0.5;
|
||||
return {
|
||||
...anchor("#hero", 0.5),
|
||||
code: severity === "warning" ? "content_overlap" : "clipped_text",
|
||||
...anchor("#hero", time),
|
||||
code: overrides.code ?? (severity === "warning" ? "content_overlap" : "clipped_text"),
|
||||
severity,
|
||||
text: "Hero",
|
||||
message: severity === "warning" ? "Text may overlap." : "Text is clipped.",
|
||||
@@ -133,6 +137,10 @@ function contrastEntry(overrides: Partial<ContrastAuditEntry> = {}): ContrastAud
|
||||
}
|
||||
|
||||
function fakeDriver(overrides: Partial<CheckAuditDriver> = {}): CheckAuditDriver {
|
||||
// A distinct string per call so the frozen-sweep guard (#U10) never fires
|
||||
// by accident in unrelated scenarios — tests that want it force a constant
|
||||
// via `collectLayoutGeometry: vi.fn(async () => "same")`.
|
||||
let geometryCallCount = 0;
|
||||
return {
|
||||
initialize: vi.fn(async (_contrast: boolean) => undefined),
|
||||
getDuration: vi.fn(async () => 9),
|
||||
@@ -141,6 +149,7 @@ function fakeDriver(overrides: Partial<CheckAuditDriver> = {}): CheckAuditDriver
|
||||
findAmbiguousSelectors: vi.fn(async (_selectors: string[]) => []),
|
||||
seek: vi.fn(async (_time: number) => undefined),
|
||||
collectLayout: vi.fn(async (_time: number, _tolerance: number) => []),
|
||||
collectLayoutGeometry: vi.fn(async () => `geometry-${geometryCallCount++}`),
|
||||
collectGeometryCandidates: vi.fn(async () => []),
|
||||
collectMotionFrame: vi.fn(async (time: number) => ({ time, data: {}, liveness: {} })),
|
||||
anchorMotionIssues: vi.fn(async (issues: LayoutIssue[]) =>
|
||||
@@ -883,7 +892,9 @@ describe("check pipeline", () => {
|
||||
|
||||
it("preserves a resolving selector, source file, identity, bbox, and sample time", async () => {
|
||||
const { report } = await runScenario(
|
||||
fakeDriver({ collectLayout: vi.fn(async () => [layoutIssue()]) }),
|
||||
fakeDriver({
|
||||
collectLayout: vi.fn(async (time: number) => [layoutIssue("error", { time })]),
|
||||
}),
|
||||
);
|
||||
expect(report.layout.findings[0]).toMatchObject({
|
||||
selector: "#hero",
|
||||
@@ -896,7 +907,9 @@ describe("check pipeline", () => {
|
||||
|
||||
it("reports layout and runtime errors from one browser session", async () => {
|
||||
const { report, browser } = await runScenario(
|
||||
fakeDriver({ collectLayout: vi.fn(async () => [layoutIssue()]) }),
|
||||
fakeDriver({
|
||||
collectLayout: vi.fn(async (time: number) => [layoutIssue("error", { time })]),
|
||||
}),
|
||||
{},
|
||||
{ runtime: [runtimeError()] },
|
||||
);
|
||||
@@ -961,7 +974,9 @@ describe("check pipeline", () => {
|
||||
) => ["snapshots/finding-00-clipped_text.png"],
|
||||
);
|
||||
const { report } = await runScenario(
|
||||
fakeDriver({ collectLayout: vi.fn(async () => [layoutIssue()]) }),
|
||||
fakeDriver({
|
||||
collectLayout: vi.fn(async (time: number) => [layoutIssue("error", { time })]),
|
||||
}),
|
||||
{ snapshots: true },
|
||||
{ captureFindingCrops: capture },
|
||||
);
|
||||
@@ -978,7 +993,9 @@ describe("check pipeline", () => {
|
||||
|
||||
const withoutSnapshots = vi.fn(async () => ["unused.png"]);
|
||||
await runScenario(
|
||||
fakeDriver({ collectLayout: vi.fn(async () => [layoutIssue()]) }),
|
||||
fakeDriver({
|
||||
collectLayout: vi.fn(async (time: number) => [layoutIssue("error", { time })]),
|
||||
}),
|
||||
{ snapshots: false },
|
||||
{ captureFindingCrops: withoutSnapshots },
|
||||
);
|
||||
@@ -986,7 +1003,15 @@ describe("check pipeline", () => {
|
||||
|
||||
const noErrors = vi.fn(async () => ["unused.png"]);
|
||||
await runScenario(
|
||||
fakeDriver({ collectLayout: vi.fn(async () => [layoutIssue("warning")]) }),
|
||||
fakeDriver({
|
||||
collectLayout: vi.fn(
|
||||
async (time: number) =>
|
||||
// container_overflow, not content_overlap: this fixture wants a plain
|
||||
// warning-severity finding held across the whole run, unaffected by
|
||||
// content_overlap's #U10 held-duration re-promotion to error.
|
||||
[layoutIssue("warning", { time, code: "container_overflow" })],
|
||||
),
|
||||
}),
|
||||
{ snapshots: true },
|
||||
{ captureFindingCrops: noErrors },
|
||||
);
|
||||
@@ -995,7 +1020,15 @@ describe("check pipeline", () => {
|
||||
|
||||
it("--strict flips a warnings-only result from exit 0 to exit 1", async () => {
|
||||
const warningDriver = () =>
|
||||
fakeDriver({ collectLayout: vi.fn(async () => [layoutIssue("warning")]) });
|
||||
fakeDriver({
|
||||
collectLayout: vi.fn(
|
||||
async (time: number) =>
|
||||
// container_overflow, not content_overlap: this fixture wants a plain
|
||||
// warning-severity finding held across the whole run, unaffected by
|
||||
// content_overlap's #U10 held-duration re-promotion to error.
|
||||
[layoutIssue("warning", { time, code: "container_overflow" })],
|
||||
),
|
||||
});
|
||||
const normal = await runScenario(warningDriver(), { strict: false });
|
||||
const strict = await runScenario(warningDriver(), { strict: true });
|
||||
|
||||
@@ -1026,6 +1059,57 @@ describe("check pipeline", () => {
|
||||
);
|
||||
expect(checkExitCode(report)).toBe(1);
|
||||
});
|
||||
|
||||
describe("frozen-sweep guard (#U10)", () => {
|
||||
it("fails with sweep_static when a 6s composition's geometry never changes across samples", async () => {
|
||||
const driver = fakeDriver({
|
||||
getDuration: vi.fn(async () => 6),
|
||||
collectLayoutGeometry: vi.fn(async () => "frozen"),
|
||||
});
|
||||
const { report } = await runScenario(driver);
|
||||
|
||||
expect(report.ok).toBe(false);
|
||||
expect(
|
||||
report.layout.findings.some(
|
||||
(finding) =>
|
||||
finding.code === "sweep_static" &&
|
||||
finding.severity === "error" &&
|
||||
finding.message.includes("did not advance"),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not flag a 1.5s static title card — too short for the guard to apply", async () => {
|
||||
const driver = fakeDriver({
|
||||
getDuration: vi.fn(async () => 1.5),
|
||||
collectLayoutGeometry: vi.fn(async () => "frozen"),
|
||||
});
|
||||
const { report } = await runScenario(driver);
|
||||
|
||||
expect(report.layout.findings.some((finding) => finding.code === "sweep_static")).toBe(false);
|
||||
});
|
||||
|
||||
it("does not double-report when a motion_frozen finding already covers the same symptom", async () => {
|
||||
const motion: MotionSpecResolution = {
|
||||
kind: "valid",
|
||||
path: "/project/index.motion.json",
|
||||
spec: { assertions: [{ kind: "keepsMoving" }] },
|
||||
};
|
||||
const driver = fakeDriver({
|
||||
getDuration: vi.fn(async () => 6),
|
||||
collectLayoutGeometry: vi.fn(async () => "frozen"),
|
||||
collectMotionFrame: vi.fn(async (time: number) => ({
|
||||
time,
|
||||
data: {},
|
||||
liveness: { "*": "unchanging" },
|
||||
})),
|
||||
});
|
||||
const { report } = await runScenario(driver, {}, { motion });
|
||||
|
||||
expect(report.motion.findings.some((finding) => finding.code === "motion_frozen")).toBe(true);
|
||||
expect(report.layout.findings.some((finding) => finding.code === "sweep_static")).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("check report telemetry", () => {
|
||||
|
||||
Reference in New Issue
Block a user