mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix(lint): stop overlapping_gsap_tweens flagging distinct unresolved targets (#1798)
* fix(lint): stop overlapping_gsap_tweens flagging distinct unresolved targets
The GSAP parser assigns the sentinel `__unresolved__` to any tween whose
target it cannot statically resolve to a concrete element (a computed
variable, a helper call, etc.). The overlap check compared tweens by that
target string, so two tweens aimed at completely different elements via
unresolvable selectors (e.g. `#s0 .hl .w` and `#s1 .hl .w` produced by a
helper) both collapsed to `__unresolved__` and were reported as
overlapping, a false positive.
An unresolved target is an unknown element: two of them are not provably
the same element, so an overlap between them cannot be asserted. Skip
overlap analysis when the target is the sentinel. Genuine overlaps on a
resolved element are still flagged.
* fix(lint): guard gsap_exit_missing_hard_kill against the unresolved-target sentinel
The overlap rule already skips tweens whose target collapses to the
__unresolved__ sentinel, but the sibling exit rule in the same file did not.
A scene-boundary exit on an unresolved target could emit a finding like
GSAP exit on "__unresolved__" ... with a meaningless tl.set("__unresolved__", ...)
fix hint. An unresolved target is an unknown element: you cannot assert a
missing hard kill on it, so skip the window early in the loop, mirroring the
overlap rule. Exits on resolved selectors are still flagged.
This commit is contained in:
@@ -952,6 +952,32 @@ describe("GSAP rules", () => {
|
||||
expect(finding).toBeDefined();
|
||||
});
|
||||
|
||||
it("does NOT report overlapping_gsap_tweens for distinct unresolved-target tweens", async () => {
|
||||
// Each tween targets a DIFFERENT element via a target the parser cannot resolve
|
||||
// statically (a helper call). Both collapse to the `__unresolved__` sentinel, but
|
||||
// they are not the same element, so an overlap must not be asserted between them.
|
||||
const html = `
|
||||
<html><body>
|
||||
<div data-composition-id="c1" data-width="1920" data-height="1080">
|
||||
<div id="s0"><div class="hl"><span class="w">a</span></div></div>
|
||||
<div id="s1"><div class="hl"><span class="w">b</span></div></div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
const a = pickWord(0);
|
||||
const b = pickWord(1);
|
||||
tl.to(a, { x: 100, duration: 1 }, 0);
|
||||
tl.to(b, { x: 100, duration: 1 }, 0);
|
||||
window.__timelines["c1"] = tl;
|
||||
</script>
|
||||
</body></html>`;
|
||||
const result = await lintHyperframeHtml(html);
|
||||
const finding = result.findings.find((f) => f.code === "overlapping_gsap_tweens");
|
||||
expect(finding).toBeUndefined();
|
||||
});
|
||||
|
||||
it("warns when an opacity exit ends at a clip start boundary without a hard kill", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
@@ -979,6 +1005,37 @@ describe("GSAP rules", () => {
|
||||
expect(finding?.message).toContain("3.00s");
|
||||
});
|
||||
|
||||
it("does NOT report gsap_exit_missing_hard_kill for an unresolved-target boundary exit", async () => {
|
||||
// The exit tween targets an element via a value the parser cannot resolve (a helper
|
||||
// call), so it collapses to the `__unresolved__` sentinel. You cannot assert a missing
|
||||
// hard kill on an unknown element, and a `tl.set("__unresolved__", ...)` hint is
|
||||
// meaningless. The resolved-target exit in the same timeline is still flagged.
|
||||
const html = `
|
||||
<html><body>
|
||||
<div data-composition-id="c1" data-width="1920" data-height="1080" data-start="0" data-duration="6">
|
||||
<div id="scene-a" class="clip" data-start="0" data-duration="3" data-track-index="0">
|
||||
<h1 id="headline">First beat</h1>
|
||||
</div>
|
||||
<div id="scene-b" class="clip" data-start="3" data-duration="3" data-track-index="0">
|
||||
<h1>Second beat</h1>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
const el = pickWord(0);
|
||||
tl.to(el, { opacity: 0, duration: 0.3 }, 2.7);
|
||||
tl.to("#headline", { opacity: 0, duration: 0.3 }, 2.7);
|
||||
window.__timelines["c1"] = tl;
|
||||
</script>
|
||||
</body></html>`;
|
||||
const result = await lintHyperframeHtml(html);
|
||||
const exitFindings = result.findings.filter((f) => f.code === "gsap_exit_missing_hard_kill");
|
||||
expect(exitFindings).toHaveLength(1);
|
||||
expect(exitFindings[0]?.selector).toBe("#headline");
|
||||
});
|
||||
|
||||
it("does not warn when a boundary exit has a matching hard kill", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
|
||||
@@ -50,6 +50,12 @@ type CompositionRange = {
|
||||
|
||||
const SCENE_BOUNDARY_EPSILON_SECONDS = 0.05;
|
||||
|
||||
// Sentinel the GSAP parser assigns to a tween whose target it cannot statically
|
||||
// resolve to a concrete element (a computed variable, a helper call, etc.). It is
|
||||
// NOT an identity: two distinct unresolved selectors are not the same element, so
|
||||
// overlap analysis must never treat them as one.
|
||||
const UNRESOLVED_TARGET = "__unresolved__";
|
||||
|
||||
// ── GSAP parsing utilities ─────────────────────────────────────────────────
|
||||
|
||||
function countClassUsage(tags: OpenTag[]): Map<string, number> {
|
||||
@@ -546,6 +552,9 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
const left = gsapWindows[i];
|
||||
if (!left) continue;
|
||||
if (left.end <= left.position) continue;
|
||||
// Unresolved targets are unknown elements: two of them are not provably
|
||||
// the same element, so an overlap between them cannot be asserted.
|
||||
if (left.targetSelector === UNRESOLVED_TARGET) continue;
|
||||
for (let j = i + 1; j < gsapWindows.length; j++) {
|
||||
const right = gsapWindows[j];
|
||||
if (!right) continue;
|
||||
@@ -573,6 +582,9 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
// gsap_exit_missing_hard_kill
|
||||
if (clipStartBoundaries.length > 0) {
|
||||
for (const win of gsapWindows) {
|
||||
// Unresolved targets are unknown elements: you cannot assert a missing
|
||||
// hard kill on one, and a `tl.set("__unresolved__", ...)` hint is meaningless.
|
||||
if (win.targetSelector === UNRESOLVED_TARGET) continue;
|
||||
if (!isSceneBoundaryExit(win)) continue;
|
||||
const boundary = findMatchingSceneBoundary(win.end, clipStartBoundaries);
|
||||
if (boundary == null) continue;
|
||||
|
||||
Reference in New Issue
Block a user