Skip to content

Commit 88f067e

Browse files
vanceingallsclaude
andcommitted
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>
1 parent db5032f commit 88f067e

3 files changed

Lines changed: 96 additions & 0 deletions

File tree

packages/sdk/src/session.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,92 @@ describe("override-set orphan cleanup on removeElement", () => {
214214
expect(overrides["hf-sub.style.opacity"]).toBe("1");
215215
});
216216
});
217+
218+
// ─── setSelection / getSelection / selectionchange ───────────────────────────
219+
220+
describe("setSelection", () => {
221+
it("getSelection returns empty array before any setSelection call", async () => {
222+
const comp = await openComposition(BASE_HTML);
223+
expect(comp.getSelection()).toEqual([]);
224+
});
225+
226+
it("setSelection updates getSelection", async () => {
227+
const comp = await openComposition(BASE_HTML);
228+
comp.setSelection(["hf-title"]);
229+
expect(comp.getSelection()).toEqual(["hf-title"]);
230+
});
231+
232+
it("setSelection with multiple ids", async () => {
233+
const comp = await openComposition(BASE_HTML);
234+
comp.setSelection(["hf-title", "hf-sub"]);
235+
expect(comp.getSelection()).toEqual(["hf-title", "hf-sub"]);
236+
});
237+
238+
it("setSelection([]) clears selection", async () => {
239+
const comp = await openComposition(BASE_HTML);
240+
comp.setSelection(["hf-title"]);
241+
comp.setSelection([]);
242+
expect(comp.getSelection()).toEqual([]);
243+
});
244+
245+
it("setSelection fires selectionchange with new ids", async () => {
246+
const comp = await openComposition(BASE_HTML);
247+
const calls: string[][] = [];
248+
comp.on("selectionchange", (ids) => calls.push(ids));
249+
comp.setSelection(["hf-title"]);
250+
expect(calls).toEqual([["hf-title"]]);
251+
});
252+
253+
it("setSelection fires selectionchange with empty array when clearing", async () => {
254+
const comp = await openComposition(BASE_HTML);
255+
comp.setSelection(["hf-title"]);
256+
const calls: string[][] = [];
257+
comp.on("selectionchange", (ids) => calls.push(ids));
258+
comp.setSelection([]);
259+
expect(calls).toEqual([[]]);
260+
});
261+
262+
it("selectionchange listener receives a fresh copy each call", async () => {
263+
const comp = await openComposition(BASE_HTML);
264+
const snapshots: string[][] = [];
265+
comp.on("selectionchange", (ids) => snapshots.push(ids));
266+
comp.setSelection(["hf-title"]);
267+
comp.setSelection(["hf-sub"]);
268+
expect(snapshots[0]).toEqual(["hf-title"]);
269+
expect(snapshots[1]).toEqual(["hf-sub"]);
270+
});
271+
272+
it("unsubscribed listener does not fire", async () => {
273+
const comp = await openComposition(BASE_HTML);
274+
const calls: string[][] = [];
275+
const off = comp.on("selectionchange", (ids) => calls.push(ids));
276+
off();
277+
comp.setSelection(["hf-title"]);
278+
expect(calls).toHaveLength(0);
279+
});
280+
281+
it("selection() proxy operates on ids at call time", async () => {
282+
const comp = await openComposition(BASE_HTML);
283+
comp.setSelection(["hf-title"]);
284+
const proxy = comp.selection();
285+
expect(proxy.ids).toEqual(["hf-title"]);
286+
});
287+
288+
it("setSelection does not affect undo stack", async () => {
289+
const comp = await openComposition(BASE_HTML);
290+
comp.setStyle("hf-title", { color: "#ff0000" });
291+
comp.setSelection(["hf-sub"]);
292+
expect(comp.canUndo()).toBe(true);
293+
comp.undo();
294+
// selection must not have been pushed to history
295+
expect(comp.canUndo()).toBe(false);
296+
});
297+
298+
it("setSelection does not emit a patch event", async () => {
299+
const comp = await openComposition(BASE_HTML);
300+
const patches: unknown[] = [];
301+
comp.on("patch", (e) => patches.push(e));
302+
comp.setSelection(["hf-title"]);
303+
expect(patches).toHaveLength(0);
304+
});
305+
});

packages/sdk/src/session.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ class CompositionImpl implements Composition {
221221
return [...this.currentSelection];
222222
}
223223

224+
setSelection(ids: string[]): void {
225+
this.currentSelection = [...ids];
226+
this.selectionHandlers.forEach((h) => h([...this.currentSelection]));
227+
}
228+
224229
// ── Dispatch / batch ─────────────────────────────────────────────────────────
225230

226231
// fallow-ignore-next-line complexity

packages/sdk/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,8 @@ export interface Composition {
253253
/** Curried handle — holds only the id, no stale-ref hazard */
254254
element(id: HfId): ElementHandle;
255255
getSelection(): string[];
256+
/** Replace the current selection; fires selectionchange. Pass [] to clear. */
257+
setSelection(ids: string[]): void;
256258

257259
// ── Advanced / agent layer (F10 layer 2) ──────────────────────────────────
258260
dispatch(op: EditOp, opts?: { origin?: unknown }): void;

0 commit comments

Comments
 (0)