mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
Merge pull request #1001 from heygen-com/fix/sub-comp-t0-baseline-regen
test(producer): regenerate sub-comp-t0 baseline + add gsap_from_opacity_noop lint rule
This commit is contained in:
@@ -746,4 +746,99 @@ describe("GSAP rules", () => {
|
||||
const finding = result.findings.find((f) => f.code === "gsap_infinite_repeat");
|
||||
expect(finding).toBeUndefined();
|
||||
});
|
||||
|
||||
it("errors when CSS opacity:0 + gsap.from({opacity:0}) — invisible forever", () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div data-composition-id="c1" data-width="1920" data-height="1080">
|
||||
<div id="title" style="opacity: 0; font-size: 120px;">Hello</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
tl.from("#title", { opacity: 0, y: 30, duration: 0.5 }, 0.2);
|
||||
window.__timelines["c1"] = tl;
|
||||
</script>
|
||||
</body></html>`;
|
||||
const result = lintHyperframeHtml(html);
|
||||
const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
|
||||
expect(finding).toBeDefined();
|
||||
expect(finding!.severity).toBe("error");
|
||||
expect(finding!.selector).toBe("#title");
|
||||
});
|
||||
|
||||
it("errors when style block has opacity:0 + gsap.from({opacity:0})", () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div data-composition-id="c1" data-width="1920" data-height="1080">
|
||||
<div id="hero">Hello</div>
|
||||
</div>
|
||||
<style>
|
||||
#hero { font-size: 200px; color: #fff; opacity: 0; }
|
||||
</style>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
tl.from("#hero", { opacity: 0, scale: 3.5, duration: 0.25, ease: "expo.out" }, 0.1);
|
||||
window.__timelines["c1"] = tl;
|
||||
</script>
|
||||
</body></html>`;
|
||||
const result = lintHyperframeHtml(html);
|
||||
const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
|
||||
expect(finding).toBeDefined();
|
||||
});
|
||||
|
||||
it("does NOT error when gsap.from({opacity:0}) and CSS has no opacity:0", () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div data-composition-id="c1" data-width="1920" data-height="1080">
|
||||
<div id="title" style="font-size: 120px; color: #fff;">Hello</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
tl.from("#title", { opacity: 0, y: 30, duration: 0.5 }, 0.2);
|
||||
window.__timelines["c1"] = tl;
|
||||
</script>
|
||||
</body></html>`;
|
||||
const result = lintHyperframeHtml(html);
|
||||
const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
|
||||
expect(finding).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does NOT error when gsap.fromTo({opacity:0}, {opacity:1}) — destination overrides CSS", () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div data-composition-id="c1" data-width="1920" data-height="1080">
|
||||
<div id="title" style="opacity: 0; font-size: 120px;">Hello</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
tl.fromTo("#title", { opacity: 0, y: 30 }, { opacity: 1, y: 0, duration: 0.5 }, 0.2);
|
||||
window.__timelines["c1"] = tl;
|
||||
</script>
|
||||
</body></html>`;
|
||||
const result = lintHyperframeHtml(html);
|
||||
const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
|
||||
expect(finding).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does NOT error when gsap.to() uses opacity:0 (exit animation)", () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div data-composition-id="c1" data-width="1920" data-height="1080">
|
||||
<div id="title" style="opacity: 0;">Hello</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
tl.to("#title", { opacity: 0, duration: 0.5 }, 4.0);
|
||||
window.__timelines["c1"] = tl;
|
||||
</script>
|
||||
</body></html>`;
|
||||
const result = lintHyperframeHtml(html);
|
||||
const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
|
||||
expect(finding).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -843,4 +843,60 @@ export const gsapRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
|
||||
}
|
||||
return findings;
|
||||
},
|
||||
|
||||
// gsap_from_opacity_noop — CSS opacity:0 + gsap.from({opacity:0}) = invisible forever
|
||||
({ styles, scripts, tags }) => {
|
||||
const findings: HyperframeLintFinding[] = [];
|
||||
const cssOpacityZeroSelectors = new Set<string>();
|
||||
|
||||
for (const style of styles) {
|
||||
for (const [, selector, body] of style.content.matchAll(
|
||||
/([#.][a-zA-Z0-9_-]+)\s*\{([^}]+)\}/g,
|
||||
)) {
|
||||
if (body && /opacity\s*:\s*0\s*[;}]/.test(body)) {
|
||||
cssOpacityZeroSelectors.add((selector ?? "").trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const tag of tags) {
|
||||
const inlineStyle = readAttr(tag.raw, "style");
|
||||
if (!inlineStyle || !/opacity\s*:\s*0/.test(inlineStyle)) continue;
|
||||
const id = readAttr(tag.raw, "id");
|
||||
const classes = readAttr(tag.raw, "class")?.split(/\s+/).filter(Boolean) ?? [];
|
||||
if (id) cssOpacityZeroSelectors.add(`#${id}`);
|
||||
for (const cls of classes) cssOpacityZeroSelectors.add(`.${cls}`);
|
||||
}
|
||||
|
||||
if (cssOpacityZeroSelectors.size === 0) return findings;
|
||||
|
||||
for (const script of scripts) {
|
||||
if (!/gsap\.timeline/.test(script.content)) continue;
|
||||
const windows = extractGsapWindows(script.content);
|
||||
|
||||
for (const win of windows) {
|
||||
if (win.method !== "from") continue;
|
||||
if (!win.properties.includes("opacity")) continue;
|
||||
const sel = win.targetSelector;
|
||||
const cssKey = sel.startsWith("#") || sel.startsWith(".") ? sel : `#${sel}`;
|
||||
if (!cssOpacityZeroSelectors.has(cssKey)) continue;
|
||||
|
||||
findings.push({
|
||||
code: "gsap_from_opacity_noop",
|
||||
severity: "error",
|
||||
message:
|
||||
`"${sel}" has CSS \`opacity: 0\` and a gsap.${win.method}() that also sets opacity to 0. ` +
|
||||
`gsap.from() animates FROM the specified value TO the current CSS value — ` +
|
||||
`since CSS is already 0, the element animates from 0→0 and never becomes visible.`,
|
||||
selector: sel,
|
||||
fixHint:
|
||||
`Remove \`opacity: 0\` from the CSS/inline style on "${sel}". ` +
|
||||
`Let gsap.from({opacity: 0}) handle the initial hidden state — ` +
|
||||
`it will animate FROM 0 TO the CSS value (1 by default).`,
|
||||
snippet: truncateSnippet(win.raw),
|
||||
});
|
||||
}
|
||||
}
|
||||
return findings;
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user