diff --git a/packages/cli/src/commands/check.test.ts b/packages/cli/src/commands/check.test.ts index 8d9a07f97e..fcba738489 100644 --- a/packages/cli/src/commands/check.test.ts +++ b/packages/cli/src/commands/check.test.ts @@ -934,6 +934,48 @@ describe("check pipeline", () => { expect(browser).toHaveBeenCalledTimes(1); }); + // An unreadable sidecar used to return before the browser opened, so Layout/Runtime/Contrast + // printed "0 errors" without having looked — an all-clear the same composition contradicts. + it("still audits layout when the motion sidecar is invalid", async () => { + const motion: MotionSpecResolution = { + kind: "invalid", + path: "/project/index.motion.json", + message: 'assertions[0] (staysInFrame): "selector" must be a non-empty string', + }; + const driver = fakeDriver({ + collectLayout: vi.fn(async (time: number) => [layoutIssue("error", { time })]), + }); + const { report, browser } = await runScenario(driver, {}, { motion }); + + expect(browser).toHaveBeenCalledTimes(1); + expect(report.layout.findings.length).toBeGreaterThan(0); + expect(report.motion.findings).toEqual([ + expect.objectContaining({ code: "motion_spec_invalid", severity: "error" }), + ]); + // A JSON consumer sees the spec was found but produced nothing sampleable. + expect(report.motion).toMatchObject({ + enabled: true, + specPath: "/project/index.motion.json", + samples: 0, + }); + expect(report.ok).toBe(false); + }); + + // Guards the new path rather than the old bug: now that the browser opens for an invalid sidecar, + // deleting `planMotionSampling`'s `kind !== "valid"` guard would crash on `motion.spec`. + it("does not sample motion for an invalid sidecar", async () => { + const motion: MotionSpecResolution = { + kind: "invalid", + path: "/project/index.motion.json", + message: "version 2 is not supported", + }; + const driver = fakeDriver(); + const { report } = await runScenario(driver, {}, { motion }); + + expect(report.motion.samples).toBe(0); + expect(driver.collectMotionFrame).not.toHaveBeenCalled(); + }); + it("reports a failing appearsBy sidecar as motion_appears_late", async () => { const motion: MotionSpecResolution = { kind: "valid", diff --git a/packages/cli/src/utils/checkPipeline.ts b/packages/cli/src/utils/checkPipeline.ts index a02fb2fa5b..4d7bc03821 100644 --- a/packages/cli/src/utils/checkPipeline.ts +++ b/packages/cli/src/utils/checkPipeline.ts @@ -1063,15 +1063,20 @@ export async function runCheckPipeline( } const motion = dependencies.resolveMotionSpec(project.dir); - if (motion.kind === "invalid") { - const finding = findingAtRoot( - "motion_spec_invalid", - "error", - motion.message, - relative(project.dir, motion.path) || "index.motion.json", - ); - return buildReport(options, lint, emptyBrowserResult(), motion, [finding], []); - } + // An unreadable sidecar only invalidates the motion stage: layout, runtime and contrast never read + // it. `planMotionSampling` declines to sample anything but a valid spec, so the audit runs as usual + // and the parse error rides along as a motion finding. + const specFindings = + motion.kind === "invalid" + ? [ + findingAtRoot( + "motion_spec_invalid", + "error", + motion.message, + relative(project.dir, motion.path) || "index.motion.json", + ), + ] + : []; let browser: CheckBrowserResult; try { @@ -1084,7 +1089,7 @@ export async function runCheckPipeline( const snapshotFiles = options.snapshots ? await writeContrastSnapshots(dependencies, project.dir, browser) : []; - const report = buildReport(options, lint, browser, motion, [], snapshotFiles); + const report = buildReport(options, lint, browser, motion, specFindings, snapshotFiles); return options.snapshots ? await withFindingCrops(dependencies, project, options, report) : report;