Files
hyperframes/packages/core/src/compiler/htmlDocument.test.ts
T
Miguel Ángel 078c493448 perf: the preview loads and decodes only the images of scenes on screen or due next (#4578)
* perf(core): skip hidden clips' content so the browser stops decoding their images

Hidden timed clips get content-visibility:hidden while their inline visibility is hidden,
from script evaluation on. A clip due within the look-ahead (2 s by default) keeps its
content live, so its images load before it shows.

* perf(studio-server): load preview images lazily so hidden scenes fetch nothing

* fix(core): stop the readiness scan waiting on and decoding later scenes' images

* perf(studio-server): keep first-frame images eager so the preview opens as fast as before

Only images inside a clip with a numeric start after 0 go lazy; a start the route
cannot read stays eager.

* perf(core): keep clips that start at 0 laid out before the first pass so the first frame is on time

* perf(core): open the preview on time by hiding only lazy images before the first pass

GSAP reads the computed style of the elements it animates while timelines build. Inside a
content-skipped clip every read forces a style update, which cost about 65 ms at open on the
bench composition. Before the first pass the rule now takes only lazy images out of layout, so
hidden later scenes still do not load theirs; after it, the skip rule keeps their content skipped.

* fix(core): hold a paused jump on the previous picture until the next scene's images decode

* fix(studio): swap in a reloaded preview only after its restore seek has painted

* fix(core): apply a held paused jump after 1 s when an image never decodes

* fix(core): keep hidden-scene image skipping and held jumps to Studio's preview, never renders

* fix(studio): reapply manual edits once a held paused jump lands

* fix(studio): copy a clip without the preview's lazy-loading marks

* fix(studio-server): mark preview images lazy without re-encoding the document's entities

* fix(core): share one preview-mark constant between the runtime and the preview route

* fix(studio): reapply manual edits a second time only after a seek that was held

* refactor(core): share the hidden-ancestor walk between clip visibility and image holds

* fix(core): import the studio preview mark from its own subpath

* fix(studio-server): find preview images with the linear html scanner

* chore(core): drop two doc comments the scanner's names already carry

* fix(studio-server): serve the preview mark as its own meta, apart from the GSAP fallback script

* fix(studio-server): capture thumbnails and frames from the preview without lazy images or the mark

* fix(core): let the full readiness scan wait on every image except lazy ones off the first frame

* fix(studio-server): keep an authored loading attribute in any letter case on preview images

* fix(core): leave template content out of findStartTags so preview images match the DOM

* fix(studio): send Ask agent the live markup without the preview loading marks

* chore(core): drop the template note from findStartTags, its test carries it

* perf(studio-server): splice lazy attributes into the preview in one pass

* fix(studio): keep authored image loading on copy and Ask agent by marking preview-made lazy images

* test(studio-server): follow the capture preview URL back to its sub-composition for every project id

* refactor: name the upcoming-clip and GSAP fallback marks once

* fix(studio-server): keep capture builds out of the on-disk preview store
2026-09-28 04:03:36 +00:00

267 lines
12 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
findStartTags,
injectScriptsAtHeadStart,
injectScriptsIntoHtml,
injectTagsAtHeadStart,
insertBeforeCloseTag,
isFullHtmlDocument,
parseHTMLContent,
stripEmbeddedRuntimeScripts,
} from "./htmlDocument.js";
describe("htmlDocument helpers", () => {
it("keeps a document's <html> attributes when a comment comes before the doctype", () => {
const doc = parseHTMLContent(
'<!-- hyperframes-registry-item: blk -->\n<!doctype html>\n<html lang="en" data-composition-variables="[]"><body></body></html>',
);
expect(doc.documentElement.getAttribute("lang")).toBe("en");
expect(doc.documentElement.hasAttribute("data-composition-variables")).toBe(true);
});
it("tells a document from a fragment past leading comments", () => {
expect(isFullHtmlDocument("<!-- marker -->\n<!doctype html><html></html>")).toBe(true);
expect(isFullHtmlDocument("<!-- a --><!-- b --><html lang='en'></html>")).toBe(true);
expect(isFullHtmlDocument("<!-- marker --><div data-composition-id='x'></div>")).toBe(false);
expect(isFullHtmlDocument("<!--><div></div><!-- --><html></html>")).toBe(false);
expect(isFullHtmlDocument("<html-card></html-card>")).toBe(false);
expect(isFullHtmlDocument("<!DOCTYPEhtml><html/ lang='en'></html>")).toBe(true);
});
it("wraps fragments before parsing", () => {
const doc = parseHTMLContent("<template><span>hello</span></template>");
expect(doc.body.querySelector("template")?.innerHTML).toContain("<span>hello</span>");
});
it("strips every known embedded HyperFrames runtime marker", () => {
const html = `
<script src="hyperframe.runtime.iife.js"></script>
<script src="hyperframes-runtime.modular.inline.js"></script >
<script src="hyperframe-runtime.modular-runtime.inline.js"></script>
<script data-hyperframes-preview-runtime="1"></script>
<script>window.__playerReady = true;</script >
<script>window.__renderReady = false;</script>
<script>window.authored = true;</script>`;
const stripped = stripEmbeddedRuntimeScripts(html);
expect(stripped).not.toContain("hyperframe.runtime.iife.js");
expect(stripped).not.toContain("hyperframes-runtime.modular.inline.js");
expect(stripped).not.toContain("hyperframe-runtime.modular-runtime.inline.js");
expect(stripped).not.toContain("data-hyperframes-preview-runtime");
expect(stripped).not.toContain("window.__playerReady");
expect(stripped).not.toContain("window.__renderReady");
expect(stripped).toContain("window.authored = true");
});
it("keeps authored scripts that reference runtime readiness flags", () => {
const html = `
<script>
window.__timelines = window.__timelines || {};
if (window.__renderReady) window.authoredReadySeen = true;
window.__timelines["main"] = {};
</script>`;
const stripped = stripEmbeddedRuntimeScripts(html);
expect(stripped).toContain('window.__timelines["main"]');
expect(stripped).toContain("window.__renderReady");
});
it("does not treat non-script tags as scripts when stripping runtimes", () => {
const html = "<scripture>window.__playerReady = true;</scripture>";
expect(stripEmbeddedRuntimeScripts(html)).toBe(html);
});
it("injects head and body scripts without replacement-token interpolation", () => {
const html = "<html><head></head><body></body></html>";
const injected = injectScriptsIntoHtml(html, ["window.x = '$&';"], ["window.y = '$&';"]);
expect(injected).toContain("<script>window.x = '$&';</script>\n</head>");
expect(injected).toContain("<script>window.y = '$&';</script>\n</body>");
});
it("injects early head scripts before authored head scripts", () => {
const html = '<html><head><script id="authored"></script></head><body></body></html>';
const injected = injectScriptsAtHeadStart(html, ["window.early = true;"]);
expect(injected.indexOf("window.early = true")).toBeLessThan(injected.indexOf('id="authored"'));
});
it("escapes inline scripts so authored script text cannot break out of the wrapper tag", () => {
const html = "<html><head></head><body></body></html>";
const injected = injectScriptsIntoHtml(
html,
['window.payload = "</script ><script>window.pwned = true;</script>";'],
["window.comment = '<!-- kept as script text';"],
);
expect(injected).toContain("<\\/script ><script>window.pwned = true;<\\/script>");
expect(injected).toContain("<\\!-- kept as script text");
expect(injected).not.toContain("</script ><script>window.pwned = true;");
});
it("injects at the document's own </head> and </body>, not at those strings inside script, style or comment text", () => {
const vendor = 'w.print("<head>"),w.print("</head>"),w.print("<body>"),w.print("</body>")';
const html = `<html><head><script>${vendor}</script><style>a::after{content:"</head>"}</style><!-- </body> --></HEAD ><body><script>${vendor}</script></body></html>`;
const injected = injectScriptsIntoHtml(html, ["window.h = 1;"], ["window.b = 1;"]);
expect(injected.split(`<script>${vendor}</script>`)).toHaveLength(3);
expect(injected).toContain("<script>window.h = 1;</script>\n</HEAD >");
expect(injected).toContain("<script>window.b = 1;</script>\n</body></html>");
});
it("falls back to the document's own <body> when </head> is omitted", () => {
const vendor = 'w.print("<head>"),w.print("<body>")';
const html = `<html><head><script>${vendor}</script><body><p>x</p></body></html>`;
const injected = injectScriptsIntoHtml(html, ["window.h = 1;"], []);
expect(injected).toContain(`<script>${vendor}</script><script>window.h = 1;</script>\n<body>`);
});
it("injects at head start past a script that prints <head>, and before <body> without a head", () => {
const vendor = 'w.print("<head>"),w.print("<body>")';
const noHead = `<html><body><script>${vendor}</script></body></html>`;
expect(injectTagsAtHeadStart(noHead, "<meta x>")).toBe(`<html><meta x>\n${noHead.slice(6)}`);
});
it("skips title text and an empty comment", () => {
const html = "<html><head><title>Intro to <script></title><!--></head><body></body></html>";
expect(insertBeforeCloseTag(html, "head", "X")).toBe(
"<html><head><title>Intro to <script></title><!-->X</head><body></body></html>",
);
});
it("keeps indexes right after a character that lowercases to two (İ)", () => {
const page = "<html><head><title>İzmir</title></head><body><h1>İstanbul</h1></body></html>";
const injected = injectScriptsIntoHtml(page, ["a=1"], ["b=2"]);
expect(injected).toContain("<script>a=1</script>\n</head>");
expect(injected).toContain("<script>b=2</script>\n</body></html>");
const stripped = stripEmbeddedRuntimeScripts(
'<p>İİ</p><script src="hyperframe.runtime.iife.js"></script><p>kept</p>',
);
expect(stripped).toBe("<p>İİ</p><p>kept</p>");
const escaped = injectScriptsIntoHtml(page, ['x="İİ</SCRIPT>"'], []);
expect(escaped).toContain('<script>x="İİ<\\/script>"</script>');
});
it("skips a script tag written inside an attribute value", () => {
const html =
'<html><head><meta content="<script>"></head><body><script>a</script></body></html>';
expect(insertBeforeCloseTag(html, "head", "X")).toBe(
'<html><head><meta content="<script>">X</head><body><script>a</script></body></html>',
);
});
it("reads past stray quotes, a self-closing SVG title and a look-alike close tag", () => {
expect(insertBeforeCloseTag("<body><img alt=it's><p>hi</p></body>", "body", "X")).toBe(
"<body><img alt=it's><p>hi</p>X</body>",
);
expect(insertBeforeCloseTag("<body><svg><title/></svg></body>", "body", "X")).toBe(
"<body><svg><title/></svg>X</body>",
);
const lookAlike = '<head><script>x="</scripts>";y="</head>"</script></head><body></body>';
expect(insertBeforeCloseTag(lookAlike, "head", "X")).toBe(
'<head><script>x="</scripts>";y="</head>"</script>X</head><body></body>',
);
});
it("treats a quote as a value only after =, like the browser", () => {
const page = "<html><head><meta name=it's></head><body><p>don't</p></body></html>";
expect(injectScriptsIntoHtml(page, ["H"], [])).toContain(
"<meta name=it's><script>H</script>\n</head>",
);
for (const meta of [
'<meta content=a=" x>',
'<meta b="c"="d>',
'<meta ="x>',
'<meta="b>',
'<meta b/="x>',
'</ a="x>',
]) {
const odd = `<html><head>${meta}</head><body class="y"></body></html>`;
expect(insertBeforeCloseTag(odd, "head", "X")).toBe(odd.replace("</head>", "X</head>"));
}
const headAttr = "<html><head data-x=it's></head><body>it's</body></html>";
expect(injectTagsAtHeadStart(headAttr, "T")).toBe(
"<html><head data-x=it's>\nT</head><body>it's</body></html>",
);
});
it("ends a comment at --!>, but not on its own opening dashes", () => {
expect(insertBeforeCloseTag("<head><!-- a --!></head>", "head", "X")).toBe(
"<head><!-- a --!>X</head>",
);
expect(insertBeforeCloseTag("<head><!--!></head>-->", "head", "X")).toBeNull();
expect(insertBeforeCloseTag("<head><!---></head>", "head", "X")).toBe("<head><!--->X</head>");
});
it("stays linear over many unclosed raw-text tags and an unfinished quoted attribute", () => {
const many = `<body>${"<script/>".repeat(40_000)}</body>`;
const unfinished = `<body>${'<p data-if="a>b" '.repeat(20_000)}`;
const started = performance.now();
expect(insertBeforeCloseTag(many, "body", "X")).toBe(many.replace("</body>", "X</body>"));
expect(insertBeforeCloseTag(unfinished, "body", "X")).toBeNull();
expect(performance.now() - started).toBeLessThan(500);
});
it("finds no head end past an unclosed script", () => {
expect(insertBeforeCloseTag("<html><head><script>a</head>", "head", "X")).toBeNull();
});
it("finds no close tag in a fragment", () => {
expect(insertBeforeCloseTag('<div><script>"</head>"</script></div>', "head", "x")).toBeNull();
});
});
describe("findStartTags", () => {
const at = (html: string, name: string) =>
findStartTags(html, name).map((i) => html.slice(i, html.indexOf(">", i) + 1));
it("finds each start tag in any case, and none in comments, raw text or longer names", () => {
const html =
'<!-- <img a> --><script>"<img b>"</script><textarea><img c></textarea>' +
'<img-card></img-card><IMG src=d><img\nsrc=e><div title="<img f>"></div>';
expect(at(html, "img")).toEqual(["<IMG src=d>", "<img\nsrc=e>"]);
});
it("leaves out template content, nested templates included", () => {
const html =
"<img a><template><img b><template><img c></template><img d></template>" +
'<div data-start="5"><template><img e></template><img f></div>';
expect(at(html, "img")).toEqual(["<img a>", "<img f>"]);
expect(at(html, "template")).toEqual(["<template>", "<template>"]);
});
});
describe("injectTagsAtHeadStart on long adversarial input", () => {
const M = 2_000_000;
it.each([
["an unclosed double quote", `<html data-x="${"a".repeat(M)}`],
["an unclosed single quote", `<html data-x='${"a".repeat(M)}`],
["many quoted values", `<html ${'"a" '.repeat(M / 4)}`],
["many <", "<".repeat(M)],
["many <html>", "<html>".repeat(M / 6)],
["many comments", `${"<!--x-->".repeat(M / 8)}<head>`],
["an unclosed comment", `<!--${"a".repeat(M)}`],
["an unclosed comment of <", `<!--${"<".repeat(M)}`],
["an unclosed comment of quotes", `<!--${'"'.repeat(M)}`],
["alternating quotes", `<html ${`"'`.repeat(M / 2)}`],
["leading spaces", `${" ".repeat(M)}x`],
["a tag that never closes", `<html ${"a ".repeat(M / 2)}`],
["an unclosed tag name", `<${"a".repeat(M)}`],
])("stays fast on %s", (_, html) => {
const started = performance.now();
injectTagsAtHeadStart(html, "<meta>");
expect(performance.now() - started).toBeLessThan(1500);
});
});