Files
hyperframes/packages/lint/src/rules/captions.test.ts
T
Miguel Ángel 98d0bdd73c refactor: extract @hyperframes/lint from core (#1756)
* refactor: extract @hyperframes/lint package from core

Moves all lint rules, hyperframeLinter, lintProject, and related types
from packages/core/src/lint/ into a new standalone packages/lint package.

Core keeps a thin re-export stub at @hyperframes/core/lint for backward
compatibility. Consumer imports (cli lint command, producer hyperframeLint)
are updated to import from @hyperframes/lint directly.

Depends on @hyperframes/parsers (PR #1755).

* fix: restore postcss-selector-parser in core (sourceMutation.ts still uses it)

* fix(ci): add parsers+lint to Dockerfile and build before preview tests

* chore: update bun.lock after restoring postcss-selector-parser dep

* test(cli): update lintProject test for string-dir signature from @hyperframes/lint

* refactor(core): single-source the lint engine in @hyperframes/lint

Delete core's byte-identical copy of the lint rule engine and re-point
staticGuard at @hyperframes/lint, so the render-time render-gate and the
studio preview share one rule engine instead of two copies that could
silently diverge. Back-compat preserved via the @hyperframes/core/lint stub.

Addresses review feedback on the dual-copy footgun.
2026-06-27 01:16:40 -04:00

167 lines
5.8 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { lintHyperframeHtml } from "../hyperframeLinter.js";
describe("caption rules", () => {
it("warns when caption exit has no hard kill tl.set", async () => {
const html = `
<html><body>
<div data-composition-id="captions" data-width="1920" data-height="1080">
<div id="caption-container"></div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
GROUPS.forEach(function(group, gi) {
var groupEl = document.createElement("div");
groupEl.id = "cg-" + gi;
tl.set(groupEl, { opacity: 1 }, group.start);
tl.to(groupEl, { opacity: 0, duration: 0.12 }, group.end - 0.12);
});
window.__timelines["captions"] = tl;
</script>
</div>
</body></html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "caption_exit_missing_hard_kill");
expect(finding).toBeDefined();
expect(finding?.severity).toBe("error");
});
it("does not warn when caption exit has hard kill tl.set", async () => {
const html = `
<html><body>
<div data-composition-id="captions" data-width="1920" data-height="1080">
<div id="caption-container"></div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
GROUPS.forEach(function(group, gi) {
var groupEl = document.createElement("div");
groupEl.id = "cg-" + gi;
tl.set(groupEl, { opacity: 1 }, group.start);
tl.to(groupEl, { opacity: 0, duration: 0.12 }, group.end - 0.12);
tl.set(groupEl, { opacity: 0, visibility: "hidden" }, group.end);
});
window.__timelines["captions"] = tl;
</script>
</div>
</body></html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "caption_exit_missing_hard_kill");
expect(finding).toBeUndefined();
});
it("does not warn for generic GSAP opacity exits in non-caption loops", async () => {
const html = `
<html><body>
<div data-composition-id="main" data-width="1920" data-height="1080">
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
var sceneCaption = document.querySelector("#scene-caption");
CARDS.forEach(function(group, gi) {
var groupEl = document.createElement("div");
groupEl.id = "card-" + gi;
tl.to(groupEl, { opacity: 0, duration: 0.12 }, 2);
});
window.__timelines["main"] = tl;
</script>
</div>
</body></html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "caption_exit_missing_hard_kill");
expect(finding).toBeUndefined();
});
it("does not warn on a content frame that only mentions karaoke in a comment", async () => {
const html = `<template id="06-one-platform-template">
<div id="root" data-composition-id="06-one-platform" data-width="1920" data-height="1080">
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
// "Minutes, not weeks" lands with a karaoke-style keyword glow
SCREENS.forEach(function (s, i) {
var el = document.getElementById("screen-" + i);
tl.to(el, { y: -40, opacity: 0, duration: 0.3 }, i * 1.3);
});
window.__timelines["06-one-platform"] = tl;
</script>
</div>
</template>`;
const result = await lintHyperframeHtml(html, { isSubComposition: true });
const finding = result.findings.find((f) => f.code === "caption_exit_missing_hard_kill");
expect(finding).toBeUndefined();
});
it("warns when caption group has nowrap without max-width", async () => {
const html = `
<html><body>
<div data-composition-id="captions" data-width="1920" data-height="1080">
<style>
.caption-group {
position: absolute;
white-space: nowrap;
text-align: center;
}
</style>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
window.__timelines["captions"] = tl;
</script>
</div>
</body></html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "caption_text_overflow_risk");
expect(finding).toBeDefined();
expect(finding?.severity).toBe("warning");
});
it("does not warn when caption group has nowrap with max-width", async () => {
const html = `
<html><body>
<div data-composition-id="captions" data-width="1920" data-height="1080">
<style>
.caption-group {
position: absolute;
white-space: nowrap;
max-width: 1600px;
overflow: hidden;
}
</style>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
window.__timelines["captions"] = tl;
</script>
</div>
</body></html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find(
(f) => f.code === "caption_text_overflow_risk" && f.severity === "warning",
);
expect(finding).toBeUndefined();
});
it("warns when caption container uses position: relative", async () => {
const html = `
<html><body>
<div data-composition-id="captions" data-width="1920" data-height="1080">
<style>
.caption-group {
position: relative;
}
</style>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
window.__timelines["captions"] = tl;
</script>
</div>
</body></html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "caption_container_relative_position");
expect(finding).toBeDefined();
expect(finding?.severity).toBe("error");
});
});