mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
fix(sdk): bridge preview selection into session state (#1362)
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { openComposition } from "./session.js";
|
import { openComposition } from "./session.js";
|
||||||
|
import type { DraftProps, ElementAtPointResult, PreviewAdapter } from "./adapters/types.js";
|
||||||
|
|
||||||
const BASE_HTML = `
|
const BASE_HTML = `
|
||||||
<div data-hf-id="hf-stage" data-hf-root style="width: 1280px; height: 720px" data-duration="5">
|
<div data-hf-id="hf-stage" data-hf-root style="width: 1280px; height: 720px" data-duration="5">
|
||||||
@@ -13,6 +14,88 @@ const BASE_HTML = `
|
|||||||
</div>
|
</div>
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
|
class TestPreviewAdapter implements PreviewAdapter {
|
||||||
|
private selectionHandlers: Array<(ids: string[]) => void> = [];
|
||||||
|
|
||||||
|
elementAtPoint(_x: number, _y: number, _opts?: { atTime?: number }): ElementAtPointResult | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyDraft(_id: string, _props: DraftProps): void {
|
||||||
|
// Test adapter tracks selection only.
|
||||||
|
}
|
||||||
|
|
||||||
|
commitPreview(): void {
|
||||||
|
// Test adapter tracks selection only.
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelPreview(): void {
|
||||||
|
// Test adapter tracks selection only.
|
||||||
|
}
|
||||||
|
|
||||||
|
select(ids: string[], _opts?: { additive?: boolean }): void {
|
||||||
|
this.emitSelection(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
on(_event: "selection", handler: (ids: string[]) => void): () => void {
|
||||||
|
this.selectionHandlers.push(handler);
|
||||||
|
return () => {
|
||||||
|
this.selectionHandlers = this.selectionHandlers.filter((h) => h !== handler);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
emitSelection(ids: readonly string[]): void {
|
||||||
|
const snapshot = [...ids];
|
||||||
|
for (const handler of this.selectionHandlers) {
|
||||||
|
handler([...snapshot]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listenerCount(): number {
|
||||||
|
return this.selectionHandlers.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Preview selection bridge ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("preview selection bridge", () => {
|
||||||
|
it("mirrors preview selection into session state and notifies subscribers", async () => {
|
||||||
|
const preview = new TestPreviewAdapter();
|
||||||
|
const comp = await openComposition(BASE_HTML, { preview });
|
||||||
|
const events: string[][] = [];
|
||||||
|
|
||||||
|
comp.on("selectionchange", (ids) => events.push([...ids]));
|
||||||
|
preview.select(["hf-title"]);
|
||||||
|
|
||||||
|
expect(comp.getSelection()).toEqual(["hf-title"]);
|
||||||
|
expect(comp.selection().ids).toEqual(["hf-title"]);
|
||||||
|
expect(events).toEqual([["hf-title"]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("selection proxy applies edits to ids selected by the preview", async () => {
|
||||||
|
const preview = new TestPreviewAdapter();
|
||||||
|
const comp = await openComposition(BASE_HTML, { preview });
|
||||||
|
|
||||||
|
preview.select(["hf-title", "hf-sub"]);
|
||||||
|
comp.selection().setStyle({ color: "#123456" });
|
||||||
|
|
||||||
|
expect(comp.getElement("hf-title")?.inlineStyles["color"]).toBe("#123456");
|
||||||
|
expect(comp.getElement("hf-sub")?.inlineStyles["color"]).toBe("#123456");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dispose unsubscribes from preview selection events", async () => {
|
||||||
|
const preview = new TestPreviewAdapter();
|
||||||
|
const comp = await openComposition(BASE_HTML, { preview });
|
||||||
|
|
||||||
|
expect(preview.listenerCount()).toBe(1);
|
||||||
|
comp.dispose();
|
||||||
|
expect(preview.listenerCount()).toBe(0);
|
||||||
|
|
||||||
|
preview.select(["hf-title"]);
|
||||||
|
expect(comp.getSelection()).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ─── History coalescing ───────────────────────────────────────────────────────
|
// ─── History coalescing ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe("history coalescing", () => {
|
describe("history coalescing", () => {
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ class CompositionImpl implements Composition {
|
|||||||
private selectionHandlers: Array<(ids: string[]) => void> = [];
|
private selectionHandlers: Array<(ids: string[]) => void> = [];
|
||||||
private patchHandlers: Array<(e: PatchEvent) => void> = [];
|
private patchHandlers: Array<(e: PatchEvent) => void> = [];
|
||||||
private errorHandlers: Array<(e: PersistErrorEvent) => void> = [];
|
private errorHandlers: Array<(e: PersistErrorEvent) => void> = [];
|
||||||
|
private previewSelectionUnsubscribe: (() => void) | null = null;
|
||||||
|
|
||||||
/** Attached by openComposition() for standalone mode. */
|
/** Attached by openComposition() for standalone mode. */
|
||||||
private historyModule: HistoryModule | null = null;
|
private historyModule: HistoryModule | null = null;
|
||||||
@@ -85,6 +86,8 @@ class CompositionImpl implements Composition {
|
|||||||
this.persist = opts.persist;
|
this.persist = opts.persist;
|
||||||
this.preview = opts.preview;
|
this.preview = opts.preview;
|
||||||
this.overrides = { ...(opts.overrides ?? {}) };
|
this.overrides = { ...(opts.overrides ?? {}) };
|
||||||
|
this.previewSelectionUnsubscribe =
|
||||||
|
this.preview?.on("selection", (ids) => this.updateSelection(ids)) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
attachHistory(module: HistoryModule): void {
|
attachHistory(module: HistoryModule): void {
|
||||||
@@ -206,6 +209,13 @@ class CompositionImpl implements Composition {
|
|||||||
return [...this.currentSelection];
|
return [...this.currentSelection];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private updateSelection(ids: readonly string[]): void {
|
||||||
|
this.currentSelection = [...ids];
|
||||||
|
for (const handler of this.selectionHandlers) {
|
||||||
|
handler([...this.currentSelection]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Dispatch / batch ─────────────────────────────────────────────────────────
|
// ── Dispatch / batch ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// fallow-ignore-next-line complexity
|
// fallow-ignore-next-line complexity
|
||||||
@@ -396,6 +406,8 @@ class CompositionImpl implements Composition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
|
this.previewSelectionUnsubscribe?.();
|
||||||
|
this.previewSelectionUnsubscribe = null;
|
||||||
this.persistQueueModule?.dispose();
|
this.persistQueueModule?.dispose();
|
||||||
this.historyModule?.dispose();
|
this.historyModule?.dispose();
|
||||||
this.changeHandlers = [];
|
this.changeHandlers = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user