mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(sdk): stage 7 step 2 — setSelection API (#1442)
* feat(sdk): stage 7 step 2 — setSelection API Adds setSelection(ids: string[]) to Composition interface and CompositionImpl. Fires selectionchange; does not touch undo stack or patch stream. 11 contract tests: get/set/clear, event firing, copy semantics, no undo/patch side-effects. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(sdk): guard setSelection against same-id no-ops Skip event dispatch when ids are identical (same length, same order) to prevent double-firing selectionchange from callers that call setSelection with the same list. Two new tests (RED→GREEN verified). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(sdk): de-duplicate ids in setSelection Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com> * fix(sdk): document PreviewAdapter.on("selection") as stage 8 prep Reviewer noted it is dead surface in this stack — no caller uses it. Add comment explaining it is wired up in stage 8 when the preview host pushes selection events up to the SDK session. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
Miguel Ángel
parent
c19898e799
commit
30ce1f35a2
@@ -70,5 +70,7 @@ export interface PreviewAdapter {
|
||||
/** Set preview selection; fires selectionchange on the session */
|
||||
select(ids: string[], opts?: { additive?: boolean }): void;
|
||||
|
||||
// Stage 8 prep: fired when the preview host changes selection (e.g. user clicks an element).
|
||||
// Not wired up in stage 7 — callers listen to the session's own selectionchange event instead.
|
||||
on(event: "selection", handler: (ids: string[]) => void): () => void;
|
||||
}
|
||||
|
||||
@@ -297,3 +297,125 @@ describe("override-set orphan cleanup on removeElement", () => {
|
||||
expect(overrides["hf-sub.style.opacity"]).toBe("1");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── setSelection / getSelection / selectionchange ───────────────────────────
|
||||
|
||||
describe("setSelection", () => {
|
||||
it("getSelection returns empty array before any setSelection call", async () => {
|
||||
const comp = await openComposition(BASE_HTML);
|
||||
expect(comp.getSelection()).toEqual([]);
|
||||
});
|
||||
|
||||
it("setSelection updates getSelection", async () => {
|
||||
const comp = await openComposition(BASE_HTML);
|
||||
comp.setSelection(["hf-title"]);
|
||||
expect(comp.getSelection()).toEqual(["hf-title"]);
|
||||
});
|
||||
|
||||
it("setSelection with multiple ids", async () => {
|
||||
const comp = await openComposition(BASE_HTML);
|
||||
comp.setSelection(["hf-title", "hf-sub"]);
|
||||
expect(comp.getSelection()).toEqual(["hf-title", "hf-sub"]);
|
||||
});
|
||||
|
||||
it("setSelection([]) clears selection", async () => {
|
||||
const comp = await openComposition(BASE_HTML);
|
||||
comp.setSelection(["hf-title"]);
|
||||
comp.setSelection([]);
|
||||
expect(comp.getSelection()).toEqual([]);
|
||||
});
|
||||
|
||||
it("setSelection fires selectionchange with new ids", async () => {
|
||||
const comp = await openComposition(BASE_HTML);
|
||||
const calls: string[][] = [];
|
||||
comp.on("selectionchange", (ids) => calls.push(ids));
|
||||
comp.setSelection(["hf-title"]);
|
||||
expect(calls).toEqual([["hf-title"]]);
|
||||
});
|
||||
|
||||
it("setSelection fires selectionchange with empty array when clearing", async () => {
|
||||
const comp = await openComposition(BASE_HTML);
|
||||
comp.setSelection(["hf-title"]);
|
||||
const calls: string[][] = [];
|
||||
comp.on("selectionchange", (ids) => calls.push(ids));
|
||||
comp.setSelection([]);
|
||||
expect(calls).toEqual([[]]);
|
||||
});
|
||||
|
||||
it("selectionchange listener receives a fresh copy each call", async () => {
|
||||
const comp = await openComposition(BASE_HTML);
|
||||
const snapshots: string[][] = [];
|
||||
comp.on("selectionchange", (ids) => snapshots.push(ids));
|
||||
comp.setSelection(["hf-title"]);
|
||||
comp.setSelection(["hf-sub"]);
|
||||
expect(snapshots[0]).toEqual(["hf-title"]);
|
||||
expect(snapshots[1]).toEqual(["hf-sub"]);
|
||||
});
|
||||
|
||||
it("unsubscribed listener does not fire", async () => {
|
||||
const comp = await openComposition(BASE_HTML);
|
||||
const calls: string[][] = [];
|
||||
const off = comp.on("selectionchange", (ids) => calls.push(ids));
|
||||
off();
|
||||
comp.setSelection(["hf-title"]);
|
||||
expect(calls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("selection() proxy operates on ids at call time", async () => {
|
||||
const comp = await openComposition(BASE_HTML);
|
||||
comp.setSelection(["hf-title"]);
|
||||
const proxy = comp.selection();
|
||||
expect(proxy.ids).toEqual(["hf-title"]);
|
||||
});
|
||||
|
||||
it("setSelection does not affect undo stack", async () => {
|
||||
const comp = await openComposition(BASE_HTML);
|
||||
comp.setStyle("hf-title", { color: "#ff0000" });
|
||||
comp.setSelection(["hf-sub"]);
|
||||
expect(comp.canUndo()).toBe(true);
|
||||
comp.undo();
|
||||
// selection must not have been pushed to history
|
||||
expect(comp.canUndo()).toBe(false);
|
||||
});
|
||||
|
||||
it("setSelection does not emit a patch event", async () => {
|
||||
const comp = await openComposition(BASE_HTML);
|
||||
const patches: unknown[] = [];
|
||||
comp.on("patch", (e) => patches.push(e));
|
||||
comp.setSelection(["hf-title"]);
|
||||
expect(patches).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("setSelection with same ids does not fire selectionchange again", async () => {
|
||||
const comp = await openComposition(BASE_HTML);
|
||||
const calls: string[][] = [];
|
||||
comp.on("selectionchange", (ids) => calls.push(ids));
|
||||
comp.setSelection(["hf-title"]);
|
||||
comp.setSelection(["hf-title"]); // same ids — must be a no-op
|
||||
expect(calls).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("setSelection with same ids in different order fires selectionchange", async () => {
|
||||
const comp = await openComposition(BASE_HTML);
|
||||
const calls: string[][] = [];
|
||||
comp.on("selectionchange", (ids) => calls.push(ids));
|
||||
comp.setSelection(["hf-title", "hf-sub"]);
|
||||
comp.setSelection(["hf-sub", "hf-title"]); // order differs — must fire
|
||||
expect(calls).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("setSelection de-duplicates repeated ids", async () => {
|
||||
const comp = await openComposition(BASE_HTML);
|
||||
comp.setSelection(["hf-title", "hf-title", "hf-sub", "hf-title"]);
|
||||
expect(comp.getSelection()).toEqual(["hf-title", "hf-sub"]);
|
||||
});
|
||||
|
||||
it("setSelection with duplicates matching stored selection does not fire selectionchange", async () => {
|
||||
const comp = await openComposition(BASE_HTML);
|
||||
const calls: string[][] = [];
|
||||
comp.on("selectionchange", (ids) => calls.push(ids));
|
||||
comp.setSelection(["hf-title"]);
|
||||
comp.setSelection(["hf-title", "hf-title"]); // de-duped = ["hf-title"] — no change
|
||||
expect(calls).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -224,6 +224,17 @@ class CompositionImpl implements Composition {
|
||||
return [...this.currentSelection];
|
||||
}
|
||||
|
||||
setSelection(ids: string[]): void {
|
||||
const deduped = Array.from(new Set(ids));
|
||||
if (
|
||||
deduped.length === this.currentSelection.length &&
|
||||
deduped.every((id, i) => id === this.currentSelection[i])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.updateSelection(deduped);
|
||||
}
|
||||
|
||||
private updateSelection(ids: readonly string[]): void {
|
||||
this.currentSelection = [...ids];
|
||||
for (const handler of this.selectionHandlers) {
|
||||
|
||||
@@ -253,6 +253,8 @@ export interface Composition {
|
||||
/** Curried handle — holds only the id, no stale-ref hazard */
|
||||
element(id: HfId): ElementHandle;
|
||||
getSelection(): string[];
|
||||
/** Replace the current selection; fires selectionchange. Pass [] to clear. */
|
||||
setSelection(ids: string[]): void;
|
||||
|
||||
// ── Advanced / agent layer (F10 layer 2) ──────────────────────────────────
|
||||
dispatch(op: EditOp, opts?: { origin?: unknown }): void;
|
||||
|
||||
Reference in New Issue
Block a user