diff --git a/packages/core/src/lint/rules/core.test.ts b/packages/core/src/lint/rules/core.test.ts index cd5525648..1a686271b 100644 --- a/packages/core/src/lint/rules/core.test.ts +++ b/packages/core/src/lint/rules/core.test.ts @@ -71,6 +71,24 @@ describe("core rules", () => { expect(finding?.message).toContain("without initializing"); }); + it("reports error when dot timeline registry is assigned without initializing", async () => { + const html = ` + +
+
+
+ +`; + const result = await lintHyperframeHtml(html); + const finding = result.findings.find((f) => f.code === "timeline_registry_missing_init"); + expect(finding).toBeDefined(); + expect(finding?.severity).toBe("error"); + }); + it("does not flag timeline assignment when init guard is present", async () => { const validComposition = ` @@ -92,6 +110,54 @@ describe("core rules", () => { expect(finding).toBeUndefined(); }); + describe("timeline_id_mismatch", () => { + it("accepts dot timeline registration", async () => { + const html = ` + +
+ +`; + const result = await lintHyperframeHtml(html); + const finding = result.findings.find((f) => f.code === "timeline_id_mismatch"); + expect(finding).toBeUndefined(); + }); + + it("reports mismatched dot timeline registration", async () => { + const html = ` + +
+ +`; + const result = await lintHyperframeHtml(html); + const finding = result.findings.find((f) => f.code === "timeline_id_mismatch"); + expect(finding).toBeDefined(); + expect(finding?.message).toContain('Timeline registered as "intro"'); + }); + + it("accepts bracket timeline registration for hyphenated ids", async () => { + const html = ` + +
+ +`; + const result = await lintHyperframeHtml(html); + const finding = result.findings.find((f) => f.code === "timeline_id_mismatch"); + expect(finding).toBeUndefined(); + }); + }); + it("warns when a timeline-visible element has no stable id for Studio editing", async () => { const html = ` diff --git a/packages/core/src/lint/rules/core.ts b/packages/core/src/lint/rules/core.ts index 273eb340b..f244bfc5e 100644 --- a/packages/core/src/lint/rules/core.ts +++ b/packages/core/src/lint/rules/core.ts @@ -4,6 +4,7 @@ import { readAttr, truncateSnippet, extractCompositionIdsFromCss, + extractTimelineRegistryKeys, getInlineScriptSyntaxError, TIMELINE_REGISTRY_INIT_PATTERN, TIMELINE_REGISTRY_ASSIGN_PATTERN, @@ -118,13 +119,12 @@ export const coreRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [ const htmlCompIds = new Set(); const timelineRegKeys = new Set(); const compIdRe = /data-composition-id\s*=\s*["']([^"']+)["']/gi; - const tlKeyRe = /window\.__timelines\[\s*["']([^"']+)["']\s*\]/g; let m: RegExpExecArray | null; while ((m = compIdRe.exec(source)) !== null) { if (m[1]) htmlCompIds.add(m[1]); } - while ((m = tlKeyRe.exec(source)) !== null) { - if (m[1]) timelineRegKeys.add(m[1]); + for (const key of extractTimelineRegistryKeys(source)) { + timelineRegKeys.add(key); } for (const key of timelineRegKeys) { if (!htmlCompIds.has(key)) { diff --git a/packages/core/src/lint/rules/gsap.test.ts b/packages/core/src/lint/rules/gsap.test.ts index cae448ead..6ef3c57ea 100644 --- a/packages/core/src/lint/rules/gsap.test.ts +++ b/packages/core/src/lint/rules/gsap.test.ts @@ -903,6 +903,25 @@ describe("GSAP rules", () => { expect(finding).toBeUndefined(); }); + it("does NOT warn when timeline is registered with dot property syntax", async () => { + const html = ` + +
+
Hello
+
+ + +`; + const result = await lintHyperframeHtml(html); + const finding = result.findings.find((f) => f.code === "gsap_timeline_not_registered"); + expect(finding).toBeUndefined(); + }); + it("does NOT warn for sub-compositions (template-based)", async () => { const html = `