fix(cli): respect timeline-free static compositions (#3533)

This commit is contained in:
Miguel Ángel
2026-08-29 02:43:33 +00:00
committed by GitHub
parent af1cb1c10d
commit b28747df0f
4 changed files with 26 additions and 0 deletions
+13
View File
@@ -145,6 +145,7 @@ function fakeDriver(overrides: Partial<CheckAuditDriver> = {}): CheckAuditDriver
return {
initialize: vi.fn(async (_contrast: boolean) => undefined),
getDuration: vi.fn(async () => 9),
hasNoTimelineDeclaration: vi.fn(async () => false),
getTransitionBoundaries: vi.fn(async () => []),
getCanvas: vi.fn(async () => ({ width: 1920, height: 1080 })),
findAmbiguousSelectors: vi.fn(async (_selectors: string[]) => []),
@@ -1222,6 +1223,18 @@ describe("check pipeline", () => {
).toBe(true);
});
it("does not flag intentional static content declared with data-no-timeline", async () => {
const driver = fakeDriver({
getDuration: vi.fn(async () => 6),
hasNoTimelineDeclaration: vi.fn(async () => true),
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 flag a 1.5s static title card — too short for the guard to apply", async () => {
const driver = fakeDriver({
getDuration: vi.fn(async () => 1.5),
+8
View File
@@ -395,6 +395,7 @@ function createPageDriver(page: Page, setTime: (time: number) => void): CheckAud
return {
initialize: (contrast) => injectAuditScripts(page, contrast),
getDuration: () => getCompositionDuration(page),
hasNoTimelineDeclaration: () => hasNoTimelineDeclaration(page),
getTransitionBoundaries: () => collectTweenBoundaries(page),
getCanvas: () =>
page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight })),
@@ -420,6 +421,13 @@ function createPageDriver(page: Page, setTime: (time: number) => void): CheckAud
};
}
async function hasNoTimelineDeclaration(page: Page): Promise<boolean> {
return page.evaluate(
() =>
document.querySelector("[data-composition-id]")?.hasAttribute("data-no-timeline") ?? false,
);
}
async function injectAuditScripts(page: Page, contrast: boolean): Promise<void> {
await page.addScriptTag({ content: loadBrowserScript("layout-audit.browser.js") });
await page.addScriptTag({ content: loadBrowserScript("motion-sample.browser.js") });
+3
View File
@@ -492,7 +492,9 @@ function detectSweepStatic(
duration: number,
geometrySignatures: string[],
motionIssues: AnchoredLayoutIssue[],
hasNoTimelineDeclaration: boolean,
): AnchoredLayoutIssue[] {
if (hasNoTimelineDeclaration) return [];
if (duration < SWEEP_STATIC_MIN_DURATION_SEC) return [];
if (geometrySignatures.length < 2) return [];
if (motionIssues.some((issue) => issue.code === "motion_frozen")) return [];
@@ -1071,6 +1073,7 @@ export async function runAuditGrid(
grid.duration,
collected.geometrySignatures,
motionIssues,
await driver.hasNoTimelineDeclaration(),
);
const rotationFindings = detectRotationPivotDrift(
collected.rotationSamples,
+2
View File
@@ -179,6 +179,8 @@ export type MotionSpecResolution =
export interface CheckAuditDriver {
initialize(contrast: boolean): Promise<void>;
getDuration(): Promise<number>;
/** True when the root composition explicitly declares that it is intentionally timeline-free. */
hasNoTimelineDeclaration(): Promise<boolean>;
getTransitionBoundaries(): Promise<number[]>;
getCanvas(): Promise<Canvas>;
findAmbiguousSelectors(selectors: string[]): Promise<AnchoredLayoutIssue[]>;