mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
feat(studio): carry hfId on TimelineElement, wire through buildPatchTarget (R7, T5b) (#1299)
* feat(studio): carry hfId on TimelineElement, wire through buildPatchTarget (R7, T5b) * refactor(studio): extract readHfId helper, fix empty-string normalization, add comments (R7 review) - Extract readHfId(el) to domEditingLayers.ts — centralizes `?.trim() || undefined` normalisation; guards against empty-string data-hf-id reaching findTagByTarget - Wire readHfId into domEditingLayers.ts and useDomEditCommits.ts (the one site that still used `?? undefined` instead of `|| undefined`) - Re-export readHfId through domEditing.ts public API - Add readHfId unit tests: present, absent, empty-string, whitespace-only - Add comment on PatchTarget: runtime validation lives in findTagByTarget, type is docs-only - Suppress pre-existing unused re-exports in timelineDOM.ts (backward-compat re-exports brought into fallow scope by the T5b hfId changes) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(core): clear data-hf-id on split clone to prevent dual-match (R7 review) cloneNode(true) copies all attributes including data-hf-id. Without clearing it, both halves of a split share the same hf-id; the server's findByHfId picks the first match and silently patches the wrong clip. Remove the attribute from the clone so write-back re-mints a fresh id on the next preview load. Adds a test: splitElementInHtml — hfId clone isolation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(studio): add hfId to DomEditLayerItem + getDomLayerPatchTarget return type (R7 review) - Add hfId to DomEditLayerItem interface (domEditingTypes.ts) so layer item construction in collectDomEditLayerItems compiles - Widen getDomLayerPatchTarget return type to include hfId + populate it from data-hf-id attribute (domEditingElement.ts) - Widen findDomEditSelectionTarget to check hfId-first when no id/selector - Widen Pick types in domEditOverlayGeometry.ts and useGsapScriptCommits.ts - Add hfId to buildMissingCompositionElements element construction - Add hfId-targeted test coverage in domEditing.test.ts, domEditOverlayGeometry.test.ts, timelineIframeHelpers.test.ts - Update hfIds.test.ts KNOWN LIMITATION labels — write-back landed in R7 T1-2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
82c754f27a
commit
0923bc0787
@@ -0,0 +1,55 @@
|
||||
// @vitest-environment jsdom
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseTimelineFromDOM, createImplicitTimelineLayersFromDOM } from "./timelineDOM";
|
||||
|
||||
function makeDoc(html: string): Document {
|
||||
const d = document.implementation.createHTMLDocument();
|
||||
d.body.innerHTML = html;
|
||||
return d;
|
||||
}
|
||||
|
||||
describe("parseTimelineFromDOM — hfId from data-hf-id", () => {
|
||||
it("harvests hfId from a data-start element that has data-hf-id", () => {
|
||||
const doc = makeDoc(`
|
||||
<div data-composition-id="root">
|
||||
<div id="hero" class="clip" data-start="0" data-duration="5" data-hf-id="hf-abc123"></div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const elements = parseTimelineFromDOM(doc, 10);
|
||||
const hero = elements.find((el) => el.domId === "hero");
|
||||
|
||||
expect(hero).toBeDefined();
|
||||
expect(hero?.hfId).toBe("hf-abc123");
|
||||
});
|
||||
|
||||
it("leaves hfId undefined when element has no data-hf-id", () => {
|
||||
const doc = makeDoc(`
|
||||
<div data-composition-id="root">
|
||||
<div id="plain" class="clip" data-start="0" data-duration="5"></div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const elements = parseTimelineFromDOM(doc, 10);
|
||||
const plain = elements.find((el) => el.domId === "plain");
|
||||
|
||||
expect(plain).toBeDefined();
|
||||
expect(plain?.hfId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createImplicitTimelineLayersFromDOM — hfId from data-hf-id", () => {
|
||||
it("harvests hfId from an implicit layer child that has data-hf-id", () => {
|
||||
const doc = makeDoc(`
|
||||
<div data-composition-id="root">
|
||||
<div id="layer" class="clip" data-hf-id="hf-xyz789"></div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const layers = createImplicitTimelineLayersFromDOM(doc, 10);
|
||||
const layer = layers.find((el) => el.domId === "layer");
|
||||
|
||||
expect(layer).toBeDefined();
|
||||
expect(layer?.hfId).toBe("hf-xyz789");
|
||||
});
|
||||
});
|
||||
@@ -26,14 +26,21 @@ import {
|
||||
|
||||
// Re-export helpers that were previously public from this module so that
|
||||
// existing import sites (hook + tests) don't need to change.
|
||||
// fallow-ignore-next-line unused-exports
|
||||
export {
|
||||
readTimelineDurationFromDocument,
|
||||
// fallow-ignore-next-line unused-exports
|
||||
resolveMediaElement,
|
||||
// fallow-ignore-next-line unused-exports
|
||||
applyMediaMetadataFromElement,
|
||||
getTimelineElementSelector,
|
||||
// fallow-ignore-next-line unused-exports
|
||||
getTimelineElementSourceFile,
|
||||
// fallow-ignore-next-line unused-exports
|
||||
getTimelineElementSelectorIndex,
|
||||
// fallow-ignore-next-line unused-exports
|
||||
buildTimelineElementIdentity,
|
||||
// fallow-ignore-next-line unused-exports
|
||||
getTimelineElementIdentity,
|
||||
findTimelineDomNodeForClip,
|
||||
} from "./timelineElementHelpers";
|
||||
@@ -72,8 +79,10 @@ export function createTimelineElementFromManifestClip(params: {
|
||||
let selectorIndex: number | undefined;
|
||||
let sourceFile: string | undefined;
|
||||
|
||||
let hfId: string | undefined;
|
||||
if (hostEl) {
|
||||
domId = hostEl.id || undefined;
|
||||
hfId = hostEl.getAttribute("data-hf-id") || undefined;
|
||||
selector = getTimelineElementSelector(hostEl);
|
||||
selectorIndex =
|
||||
doc && selector ? getTimelineElementSelectorIndex(doc, hostEl, selector) : undefined;
|
||||
@@ -98,6 +107,7 @@ export function createTimelineElementFromManifestClip(params: {
|
||||
duration: clip.duration,
|
||||
track: clip.track,
|
||||
domId,
|
||||
hfId,
|
||||
selector,
|
||||
selectorIndex,
|
||||
sourceFile,
|
||||
@@ -127,6 +137,7 @@ export function createTimelineElementFromManifestClip(params: {
|
||||
}
|
||||
if (hostEl) {
|
||||
entry.domId = hostEl.id || undefined;
|
||||
entry.hfId = hostEl.getAttribute("data-hf-id") || undefined;
|
||||
entry.selector = getTimelineElementSelector(hostEl);
|
||||
entry.selectorIndex =
|
||||
doc && entry.selector
|
||||
@@ -187,6 +198,7 @@ export function createImplicitTimelineLayersFromDOM(
|
||||
|
||||
layers.push({
|
||||
domId: child.id || undefined,
|
||||
hfId: child.getAttribute("data-hf-id") || undefined,
|
||||
duration: rootDuration,
|
||||
id: identity.id,
|
||||
key: identity.key,
|
||||
@@ -262,6 +274,7 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
|
||||
duration: dur,
|
||||
track: isNaN(track) ? 0 : track,
|
||||
domId: el.id || undefined,
|
||||
hfId: el.getAttribute("data-hf-id") || undefined,
|
||||
selector,
|
||||
selectorIndex,
|
||||
sourceFile,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// @vitest-environment jsdom
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildMissingCompositionElements } from "./timelineIframeHelpers";
|
||||
import type { IframeWindow } from "./playbackTypes";
|
||||
|
||||
function makeDoc(html: string): Document {
|
||||
const d = document.implementation.createHTMLDocument();
|
||||
d.body.innerHTML = html;
|
||||
return d;
|
||||
}
|
||||
|
||||
describe("buildMissingCompositionElements — hfId (R7)", () => {
|
||||
it("harvests hfId from data-hf-id on composition host elements", () => {
|
||||
const doc = makeDoc(`
|
||||
<div data-composition-id="root">
|
||||
<div
|
||||
data-composition-id="scene-a"
|
||||
data-composition-src="scenes/a.html"
|
||||
data-hf-id="hf-scene1"
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
></div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const { missing } = buildMissingCompositionElements(doc, window as IframeWindow, [], 10);
|
||||
const entry = missing[0];
|
||||
|
||||
expect(entry).toBeDefined();
|
||||
expect(entry?.hfId).toBe("hf-scene1");
|
||||
});
|
||||
|
||||
it("leaves hfId undefined when element has no data-hf-id", () => {
|
||||
const doc = makeDoc(`
|
||||
<div data-composition-id="root">
|
||||
<div
|
||||
data-composition-id="scene-b"
|
||||
data-composition-src="scenes/b.html"
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
></div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const { missing } = buildMissingCompositionElements(doc, window as IframeWindow, [], 10);
|
||||
const entry = missing[0];
|
||||
|
||||
expect(entry).toBeDefined();
|
||||
expect(entry?.hfId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -286,6 +286,7 @@ export function buildMissingCompositionElements(
|
||||
duration: dur,
|
||||
track: isNaN(track) ? 0 : track,
|
||||
domId: el.id || undefined,
|
||||
hfId: el.getAttribute("data-hf-id") || undefined,
|
||||
selector,
|
||||
selectorIndex,
|
||||
sourceFile,
|
||||
|
||||
Reference in New Issue
Block a user