fix(cli): distinguish a skipped browser check from a genuinely clean one (#3901)

* fix(cli): distinguish a skipped browser check from a genuinely clean one

`hyperframes check`'s runtime/layout/motion/contrast JSON sections reported
the exact same ok:true/zero-findings shape whether the browser session
actually ran and found nothing, or never ran at all (a blocking lint error,
a lint-step crash, or an exception thrown before the audit could start all
took the same "empty result" fallback). Only the top-level ok field and an
undocumented layout.duration === 0 tell separated the two cases apart.

Add a skipped boolean to CheckBrowserResult, set at the only two places one
is constructed (false in the real audit result, true in the empty-result
fallback), and mirror it to a new top-level browserSkipped field on
CheckReport. The human-readable report now prints a warning when the
browser never ran; the JSON envelope carries the same signal for callers
that only check individual sections.

Co-Authored-By: Miguel Angel <miguel.sierra@heygen.com>

* docs(cli): document browserSkipped in the check --json envelope

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

* refactor(cli): trim browserSkipped docs and cover the linter-crash skip path

* chore(skills): refresh the hyperframes-cli manifest hash

* refactor(cli): drop the browserSkipped type comments to keep the comment share flat

---------

Co-authored-by: Miguel Angel <miguel.sierra@heygen.com>
This commit is contained in:
miga-heygen
2026-10-02 07:32:48 -07:00
committed by GitHub
co-authored by Miguel Angel
parent 94650480e2
commit b31fb02c7e
7 changed files with 58 additions and 5 deletions
+5 -1
View File
@@ -662,7 +662,7 @@ did, in one command and one browser session.
```bash
npx hyperframes check [dir]
npx hyperframes check [dir] --json # {ok, lint, runtime, layout, motion, contrast, snapshots}
npx hyperframes check [dir] --json # {ok, browserSkipped, lint, runtime, layout, motion, contrast, hdr, snapshots}
npx hyperframes check [dir] --snapshots
npx hyperframes check [dir] --at 1.5,4,7.25
npx hyperframes check [dir] --strict # warnings fail too
@@ -674,6 +674,10 @@ running every audit at every sample: runtime console errors and failed requests,
layout defects (overflow, clipping, held overlaps, occlusion, coordinate-frame
drift), `*.motion.json` assertions, and WCAG AA contrast.
When the browser never ran (lint errors, a linter crash, or a browser launch
failure), `browserSkipped` is `true` and the `layout`, `motion` and `contrast`
sections are empty, not clean.
| Flag | Description |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `--json` | One aggregated envelope. Every finding carries a selector, `data-*` identity, source file, bbox, and sample time. |
+39 -1
View File
@@ -949,6 +949,7 @@ function reportWithFindings(overrides: Partial<CheckReport> = {}): CheckReport {
return {
ok: true,
strict: false,
browserSkipped: false,
lint: { ...emptySection(), filesScanned: 0 },
runtime: emptySection(),
layout: {
@@ -1047,6 +1048,7 @@ describe("check pipeline", () => {
const envelope = JSON.parse(output);
expect(envelope).toMatchObject({
ok: true,
browserSkipped: false,
lint: { ok: true },
runtime: { ok: true },
layout: { ok: true },
@@ -1057,7 +1059,7 @@ describe("check pipeline", () => {
});
});
it("short-circuits on lint errors without launching a browser", async () => {
it("short-circuits on lint errors without launching a browser, and flags the skipped sections", async () => {
const lint = lintWith(
"error",
"root_missing_composition_id",
@@ -1069,6 +1071,42 @@ describe("check pipeline", () => {
expect(checkExitCode(report)).toBe(1);
expect(report.lint.findings).toHaveLength(1);
expect(browser).not.toHaveBeenCalled();
// The browser sections look clean either way; only browserSkipped tells them apart.
expect(report.browserSkipped).toBe(true);
expect(report.runtime).toMatchObject({ ok: true, errorCount: 0, findings: [] });
expect(report.layout).toMatchObject({ ok: true, errorCount: 0, findings: [], duration: 0 });
expect(report.motion).toMatchObject({ ok: true, errorCount: 0, findings: [] });
expect(report.contrast).toMatchObject({ ok: true, errorCount: 0, findings: [] });
});
it("marks browserSkipped true when the linter itself crashes", async () => {
const { deps } = dependencies(fakeDriver());
deps.lintProject = vi.fn(async () => {
throw new Error("unreadable index.html");
});
const report = await runCheckPipeline(PROJECT, DEFAULT_CHECK_OPTIONS, deps);
expect(report.ok).toBe(false);
expect(report.browserSkipped).toBe(true);
expect(report.runtime.findings[0]?.code).toBe("check_lint_failure");
});
it("marks browserSkipped false once a browser session actually runs", async () => {
const { report } = await runScenario(fakeDriver());
expect(report.browserSkipped).toBe(false);
});
it("marks browserSkipped true when the browser session throws before producing results", async () => {
const { deps } = dependencies(fakeDriver());
deps.runBrowserCheck = vi.fn(async () => {
throw new Error("Chrome launch failed");
});
const report = await runCheckPipeline(PROJECT, DEFAULT_CHECK_OPTIONS, deps);
expect(report.ok).toBe(false);
expect(report.browserSkipped).toBe(true);
expect(report.runtime.findings).toHaveLength(1);
expect(report.layout).toMatchObject({ ok: true, errorCount: 0, findings: [] });
});
it("gates AA contrast failures and --no-contrast skips the pass", async () => {
+6
View File
@@ -405,6 +405,12 @@ function nonNegativeNumber(value: unknown, fallback: number): number {
function printHumanReport(report: CheckReport): void {
printSection("Lint", report.lint);
if (report.browserSkipped) {
console.log();
console.log(
` ${c.warn("⚠")} Browser session never ran — layout, motion, and contrast below are empty placeholders, not a clean pass.`,
);
}
printSection("Runtime", report.runtime);
printLayoutSection("Layout", report.layout);
printSection("Motion", report.motion);
+3
View File
@@ -1130,6 +1130,7 @@ export async function runAuditGrid(
contrastPassed: contrast.passed,
screenshots: collected.screenshots,
timings: { launchSettleMs: 0, seekLoopMs, contrastMs: collected.contrastMs },
skipped: false,
};
}
@@ -1412,6 +1413,7 @@ function buildReport(
const report: CheckReport = {
ok: errorCount === 0 && (!options.strict || warningCount === 0),
strict: options.strict,
browserSkipped: browser.skipped,
lint,
runtime,
layout,
@@ -1537,6 +1539,7 @@ function emptyBrowserResult(): CheckBrowserResult {
contrastPassed: 0,
screenshots: [],
timings: { launchSettleMs: 0, seekLoopMs: 0, contrastMs: 0 },
skipped: true,
};
}
+2
View File
@@ -244,6 +244,7 @@ export interface CheckBrowserResult {
contrastPassed: number;
screenshots: CheckScreenshot[];
timings: CheckTimings;
skipped: boolean;
}
/** The seek-grid audit loop, injected into checkBrowser so it never imports checkPipeline back. */
@@ -264,6 +265,7 @@ export interface CheckSection<T extends CheckFinding = CheckFinding> {
export interface CheckReport {
ok: boolean;
strict: boolean;
browserSkipped: boolean;
lint: CheckSection & { filesScanned: number };
runtime: CheckSection;
layout: CheckSection<AnchoredLayoutIssue> & {
+1 -1
View File
@@ -30,7 +30,7 @@
"files": 7
},
"hyperframes-cli": {
"hash": "c1b0b3d02ad38edc",
"hash": "0103a548049ffd4e",
"files": 11
},
"hyperframes-core": {
@@ -30,7 +30,7 @@ Lints `index.html` and all files in `compositions/`. Reports errors (must fix),
```bash
npx hyperframes check # current directory: the full browser gate
npx hyperframes check ./my-project # specific project
npx hyperframes check --json # agent-readable envelope {ok, lint, runtime, layout, motion, contrast, hdr, snapshots}
npx hyperframes check --json # agent-readable envelope {ok, browserSkipped, lint, runtime, layout, motion, contrast, hdr, snapshots}
npx hyperframes check --snapshots # also write overview frames (annotated) + per-finding crops
npx hyperframes check --samples 15 # denser timeline sweep (default 9)
npx hyperframes check --at 1.5,4,7.25 # explicit hero-frame timestamps
@@ -41,7 +41,7 @@ npx hyperframes check --no-contrast # skip the WCAG audit while iterating
npx hyperframes check --strict # exit non-zero on warnings too (default: only errors)
```
One command, one Chrome boot. `check` runs the linter first and skips the browser entirely when lint reports errors. Then it loads the bundled composition once, wires runtime listeners before navigation, and sweeps one seek grid running every audit per sample:
One command, one Chrome boot. `check` runs the linter first and skips the browser entirely when lint reports errors. When the browser never ran (lint errors, a linter crash, or a browser launch failure), `browserSkipped` is `true` and the `layout`, `motion` and `contrast` sections are empty, not clean. Otherwise it loads the bundled composition once, wires runtime listeners before navigation, and sweeps one seek grid running every audit per sample:
- **Runtime**: JavaScript console errors, unhandled exceptions, failed network requests (media-file `ERR_ABORTED` filtered out), HTTP 4xx/5xx.
- **Layout**: text extending outside its container or the canvas, text clipped by its own box, held text overlaps and occlusion (with an approximate covered fraction), children escaping clipping containers.