From 9aebc8db70cf373875b9b685a68722b78eb33c03 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Sat, 6 Jun 2026 18:27:26 -0700 Subject: [PATCH] test(studio): add T11 history coalescing and cross-prop separation suite (#1242) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Extends `editHistory.test.ts` with T11 from the SDK migration test plan: history coalescing gaps and origin guard stubs. ## Tests **New passing test (1):** - **cross-prop coalescing separation** — two edits within the coalesce window but with *different* `coalesceKey` values produce two separate undo entries, not one coalesced entry. Fills the gap left by the existing same-file coalescing tests (lines 176–243). **`.todo` stubs (2):** - `gesture-start/commit collapses intermediate drag steps into one undo entry` — requires gesture lifecycle API not yet built - `origin:applyPatches edits excluded from undo stack` — requires SDK session object (`session.on("patch", ...)`, `session.dispatch(...)`) which doesn't exist yet; needed to prevent undo loops when SDK patches are applied ## Stack Stacked on T8 (#1241). Prerequisite for T4 (#1243). --- packages/studio/src/utils/editHistory.test.ts | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/packages/studio/src/utils/editHistory.test.ts b/packages/studio/src/utils/editHistory.test.ts index e4f2abc3e..193bd05fe 100644 --- a/packages/studio/src/utils/editHistory.test.ts +++ b/packages/studio/src/utils/editHistory.test.ts @@ -209,6 +209,75 @@ describe("edit history", () => { expect(state.undo[0].files["index.html"].after).toBe("c"); }); + it("coalesces entries with the same coalesceKey within the window (prop: format)", () => { + const first = buildEditHistoryEntry({ + projectId: "project-1", + label: "Edit title color", + kind: "source", + coalesceKey: "prop:title.color", + files: { + "index.html": { before: "a", after: "b" }, + }, + now: 100, + id: "entry-1", + }); + const second = buildEditHistoryEntry({ + projectId: "project-1", + label: "Edit title color", + kind: "source", + coalesceKey: "prop:title.color", + files: { + "index.html": { before: "b", after: "c" }, + }, + now: 200, + id: "entry-2", + }); + + const state = pushEditHistoryEntry( + pushEditHistoryEntry(createEmptyEditHistory(), first), + second, + { coalesceMs: 1000 }, + ); + + expect(state.undo).toHaveLength(1); + expect(state.undo[0].id).toBe("entry-2"); + expect(state.undo[0].files["index.html"].before).toBe("a"); + expect(state.undo[0].files["index.html"].after).toBe("c"); + }); + + it("does not coalesce entries with different coalesceKeys (cross-prop separation)", () => { + const titleEdit = buildEditHistoryEntry({ + projectId: "project-1", + label: "Edit title color", + kind: "source", + coalesceKey: "prop:title.color", + files: { + "index.html": { before: "a", after: "b" }, + }, + now: 100, + id: "entry-title", + }); + const bodyEdit = buildEditHistoryEntry({ + projectId: "project-1", + label: "Edit body color", + kind: "source", + coalesceKey: "prop:body.color", + files: { + "index.html": { before: "b", after: "c" }, + }, + now: 200, + id: "entry-body", + }); + + const state = pushEditHistoryEntry( + pushEditHistoryEntry(createEmptyEditHistory(), titleEdit), + bodyEdit, + { coalesceMs: 1000 }, + ); + + expect(state.undo.map((e) => e.id)).toEqual(["entry-title", "entry-body"]); + }); + it("does not coalesce source editor edits outside the coalesce window", () => { const first = buildEditHistoryEntry({ projectId: "project-1", @@ -241,4 +310,47 @@ describe("edit history", () => { expect(state.undo.map((entry) => entry.id)).toEqual(["entry-1", "entry-2"]); }); + + it("coalesces entries exactly at the coalesce boundary (delta === coalesceMs is inclusive)", () => { + const first = buildEditHistoryEntry({ + projectId: "project-1", + label: "Edit source", + kind: "source", + coalesceKey: "source:index.html", + files: { + "index.html": { before: "a", after: "b" }, + }, + now: 100, + id: "entry-1", + }); + const second = buildEditHistoryEntry({ + projectId: "project-1", + label: "Edit source", + kind: "source", + coalesceKey: "source:index.html", + files: { + "index.html": { before: "b", after: "c" }, + }, + now: 1100, // exactly coalesceMs=1000ms after first + id: "entry-2", + }); + + const state = pushEditHistoryEntry( + pushEditHistoryEntry(createEmptyEditHistory(), first), + second, + { coalesceMs: 1000 }, + ); + + // Boundary is <=: delta of exactly 1000ms coalesces into one entry. + expect(state.undo).toHaveLength(1); + expect(state.undo[0].id).toBe("entry-2"); + expect(state.undo[0].files["index.html"].before).toBe("a"); + expect(state.undo[0].files["index.html"].after).toBe("c"); + }); + + it.todo("gesture-start/commit collapses intermediate drag steps into one undo entry"); + + it.todo( + "origin:applyPatches edits are excluded from undo stack to prevent undo loops (requires SDK session)", + ); });