Files
hyperframes/packages/inspector/tests/timelineValues.ts
Xuanru Li 45825faa5d fix(inspector): grep timeline windows, continuation, candidates and timeline text from the Canary dogfood (#5595)
* fix(inspector): report nested hosts' authored window, one location per source line

The runtime strips data-duration/data-end from nested composition hosts after
boot and keeps them as data-hf-authored-*. Timeline inspection built its
resolver without authored attrs, so a host with data-duration="4" reported
its GSAP timeline length (3.72s) instead.

Timeline text and events[].sources listed the same file:line once per mapped
snippet (creation, caller, dependency), so a one-line script printed one
location up to nine times.

* refactor(inspector): carry the dedupe reason in the helper's name

* fix(inspector): end continuation on a page that fits no snippet; fill budget with candidates

* fix(inspector): timeline groups by component, names attr values, eases, film end

* fix(studio-server): stamp hf ids into a file without reformatting it

* fix(inspector): find an element's tag before its served hf id reaches the source

* fix(inspector): live element map shows authored classes and text, not the playhead's

* fix(inspector): collapse script-built text so the element map ignores the playhead

Class names stay in source order, a runtime-stripped id is shown as id="…"
rather than a selector, and a grouped row names which children it covers.

* fix(inspector): keep paging, stamping, and the live map still on the reported edges

A snippet larger than the page ended the walk, so later snippets could not
be reached. Raw text ended at a longer tag name, which could stamp an id
inside a script. An empty or mixed source element showed live text, and an
unknown duration read as a film that ends at 0s.

* fix(inspector): mint ids for an empty hf id and keep an unquoted class

An empty data-hf-id was treated as already assigned, so stamping left it
blank while the older stamper minted one. An unquoted class was read as no
class, so the live map dropped it.

* fix(inspector): narrow markup before reading a child tag

skipMarkup returns a boolean, so the build still sees Markup where a tag
is required. Narrow on the kind first so the inspector typechecks.

* fix: quiet check false positives and name the ancestor of an id-less target

Endpoint contact, disjoint fromTo pairs, and a negative z-index under a
stacking-context parent were warnings with bad advice. Layout and contrast
blamed a dot grid, a circular avatar's panel, and a visibility override.
A faint texture veil is no longer the click target.

* fix(lint): drop a history sentence and six gsap comments

The comments check treats "load-bearing" as history, and gsap.ts added six
comment lines. The names already carry what those lines said.

* fix(parsers): ignore an empty id written inside another attribute

The empty-id scan matched the first data-hf-id="" in the tag, including one
inside a quoted title. It now skips quoted text, so the real attribute is minted.

* fix(lint): keep real covers and a negative z-index under a card

A gradient scale-down treated every fill like a radial dot, so a panel
that starts transparent stopped counting as a cover. A stacking context
on any ancestor cleared a negative z-index, and a faint wrapper made
images inside it unpickable.

* fix: keep an emoji from shifting a stamped id

The quote mask replaced one emoji with one space, so the id landed before
the closing quote. A radial cover that turns opaque after a clear stop
stays fully painted.

* fix(lint): score a letter-only dot as a dot, not a color name

namesAColor saw #fff and rgba as words, so those dot grids counted as a
full cover. Hex and rgb colors are stripped before that word check.
2026-10-14 09:16:20 +00:00

70 lines
3.8 KiB
TypeScript

import assert from "node:assert/strict";
import { mkdtempSync, writeFileSync, copyFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Inspector } from "../src/service";
import { animationValues } from "../../core/src/runtime/timelineValues";
const unknown = { kind: "unknown", value: null };
assert.deepEqual(animationValues({ scale: () => 1 }, ["scale"]).properties[0].to, unknown);
assert.equal(animationValues({ x: "+=10" }, ["x"]).properties[0].to.kind, "relative");
assert.equal(animationValues({ x: "random(0,10)" }, ["x"]).properties[0].to.kind, "expression");
assert.deepEqual(animationValues({ x: NaN }, ["x"]).properties[0].to, unknown);
const project = mkdtempSync(join(tmpdir(), "hf-timeline-values-"));
copyFileSync(process.argv[2], join(project, "gsap.js"));
writeFileSync(
join(project, "index.html"),
`<html><head><script src="gsap.js"></script></head><body><div data-composition-id="main" data-duration="10" data-width="1920" data-height="1080"><div id="bubble"></div><div id="from"></div><div id="rel"></div><div class="member"></div><div class="member"></div><svg><rect id="attr"></rect></svg><div id="fn"></div><div id="kf"></div><div id="round"></div></div><script>
const tl=gsap.timeline({paused:true});
tl.fromTo('#bubble',{scale:0},{scale:1.18,duration:.16,ease:'power2.out'},1.8);
tl.to('#bubble',{scale:.95,duration:.11,ease:'sine.inOut'},1.96);
tl.to('#bubble',{scale:1.04,duration:.09,ease:'sine.inOut'},2.07);
tl.to('#bubble',{scale:1,duration:.09,ease:'sine.out'},2.16);
tl.from('#from',{x:100,duration:1},3);
tl.to('#rel',{x:'+=10',duration:1},3);
tl.to('.member',{x:i=>100+i,duration:1,stagger:.2},4);
tl.fromTo('#attr',{attr:{width:0}},{attr:{width:50},duration:1},6);
tl.to('#fn',{x:10,duration:1,ease:t=>t*t},6);
tl.to('#kf',{keyframes:{x:[0,10,20]},ease:'steps(8)',duration:1},7);
tl.to('#round',{rotation:2.6999999999999997,duration:1},8);
window.__timelines={main:tl};
</script></body></html>`,
);
const inspector = new Inspector(project);
try {
const result = await inspector.timeline({
component: { selector: "#bubble" },
range: [1.8, 2.3],
});
assert.equal(result.events.length, 4);
assert.deepEqual(
result.events.map((e) => e.values?.properties[0].to.value),
[1.18, 0.95, 1.04, 1],
);
assert.equal(result.events[0].values?.properties[0].from.value, 0);
assert.deepEqual(result.events[1].values?.properties[0].from, unknown);
assert.equal(result.events[1].values?.ease.value, "sine.inOut");
assert(result.text.includes("scale: ? → 0.95"));
const from = await inspector.timeline({ component: { selector: "#from" } });
assert.equal(from.events[0].values?.properties[0].from.value, 100);
assert.deepEqual(from.events[0].values?.properties[0].to, unknown);
const rel = await inspector.timeline({ component: { selector: "#rel" } });
assert.equal(rel.events[0].values?.properties[0].to.kind, "relative");
const members = await inspector.timeline({ range: [4, 6] });
const animations = members.events.filter((e) => e.kind === "animation");
assert.equal(animations.length, 2);
assert(animations.every((e) => e.values?.properties[0].to.kind === "unknown"));
const textOf = async (selector: string) =>
(await inspector.timeline({ component: { selector } })).text;
assert.match(await textOf("#attr"), /attr\.width: 0 → 50/);
assert.match(await textOf("#fn"), /ease: custom function/);
assert.match(await textOf("#kf"), /ease: steps\(8\) across keyframes/);
assert.match(await textOf("#round"), /rotation: \? → 2\.7;/);
console.log(result.text);
console.log(
"PASS authored endpoints, unknown starts, from direction, ease, relative values, function values, stagger, attr values, custom and keyframe eases and rounding",
);
} finally {
await inspector.dispose();
rmSync(project, { recursive: true, force: true });
}