mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-09 12:00:26 +00:00
* 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.
167 lines
5.8 KiB
TypeScript
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");
|
|
});
|
|
});
|