diff --git a/packages/sdk/src/session.test.ts b/packages/sdk/src/session.test.ts index fd356a6ff..78fa70fff 100644 --- a/packages/sdk/src/session.test.ts +++ b/packages/sdk/src/session.test.ts @@ -4,6 +4,7 @@ import { describe, it, expect } from "vitest"; import { openComposition } from "./session.js"; +import type { DraftProps, ElementAtPointResult, PreviewAdapter } from "./adapters/types.js"; const BASE_HTML = `
@@ -13,6 +14,88 @@ const BASE_HTML = `
`.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 ─────────────────────────────────────────────────────── describe("history coalescing", () => { diff --git a/packages/sdk/src/session.ts b/packages/sdk/src/session.ts index 989992a48..3a0a3ab44 100644 --- a/packages/sdk/src/session.ts +++ b/packages/sdk/src/session.ts @@ -66,6 +66,7 @@ class CompositionImpl implements Composition { private selectionHandlers: Array<(ids: string[]) => void> = []; private patchHandlers: Array<(e: PatchEvent) => void> = []; private errorHandlers: Array<(e: PersistErrorEvent) => void> = []; + private previewSelectionUnsubscribe: (() => void) | null = null; /** Attached by openComposition() for standalone mode. */ private historyModule: HistoryModule | null = null; @@ -85,6 +86,8 @@ class CompositionImpl implements Composition { this.persist = opts.persist; this.preview = opts.preview; this.overrides = { ...(opts.overrides ?? {}) }; + this.previewSelectionUnsubscribe = + this.preview?.on("selection", (ids) => this.updateSelection(ids)) ?? null; } attachHistory(module: HistoryModule): void { @@ -206,6 +209,13 @@ class CompositionImpl implements Composition { return [...this.currentSelection]; } + private updateSelection(ids: readonly string[]): void { + this.currentSelection = [...ids]; + for (const handler of this.selectionHandlers) { + handler([...this.currentSelection]); + } + } + // ── Dispatch / batch ───────────────────────────────────────────────────────── // fallow-ignore-next-line complexity @@ -396,6 +406,8 @@ class CompositionImpl implements Composition { } dispose(): void { + this.previewSelectionUnsubscribe?.(); + this.previewSelectionUnsubscribe = null; this.persistQueueModule?.dispose(); this.historyModule?.dispose(); this.changeHandlers = [];