|
|
@@ -256,6 +256,76 @@ describe("content-change-op coalescing", () => {
|
|
|
expect(history.canUndo).toBe(true)
|
|
|
expect(history.undo()).not.toBeNull()
|
|
|
})
|
|
|
+
|
|
|
+ it("typing → split → typing → undo cycles through operations correctly", () => {
|
|
|
+ const history = createOperationHistory()
|
|
|
+ const COALESCE_MS = 500
|
|
|
+
|
|
|
+ function handleContentOp(op: {
|
|
|
+ blockId: string
|
|
|
+ previousContent: string
|
|
|
+ newContent: string
|
|
|
+ timestamp: number
|
|
|
+ }) {
|
|
|
+ const last = history.peek()
|
|
|
+ if (
|
|
|
+ last?.type === "set-block-content" &&
|
|
|
+ last.blockId === op.blockId &&
|
|
|
+ op.timestamp - last.timestamp < COALESCE_MS
|
|
|
+ ) {
|
|
|
+ ;(last as SetBlockContentOp).newContent = op.newContent
|
|
|
+ last.timestamp = op.timestamp
|
|
|
+ } else {
|
|
|
+ void history.execute({
|
|
|
+ type: "set-block-content",
|
|
|
+ blockId: op.blockId,
|
|
|
+ previousContent: op.previousContent,
|
|
|
+ newContent: op.newContent,
|
|
|
+ timestamp: op.timestamp,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. Type "hello" in block A
|
|
|
+ handleContentOp({ blockId: "a", previousContent: "", newContent: "hello", timestamp: 1000 })
|
|
|
+
|
|
|
+ // 2. Split at index 0 (creates block B with "world")
|
|
|
+ history.execute({
|
|
|
+ type: "split-block",
|
|
|
+ index: 0,
|
|
|
+ splitBlockId: "b",
|
|
|
+ depth: 0,
|
|
|
+ beforeContent: "hello",
|
|
|
+ afterContent: "world",
|
|
|
+ timestamp: 2000,
|
|
|
+ })
|
|
|
+
|
|
|
+ // 3. Type " test" in block B
|
|
|
+ handleContentOp({ blockId: "b", previousContent: "", newContent: " test", timestamp: 2100 })
|
|
|
+
|
|
|
+ // Step 3 undo — revert block B typing
|
|
|
+ const undo1 = history.undo()
|
|
|
+ expect(undo1).not.toBeNull()
|
|
|
+ expect(undo1!.type).toBe("set-block-content")
|
|
|
+ expect((undo1 as SetBlockContentOp).blockId).toBe("b")
|
|
|
+ expect((undo1 as SetBlockContentOp).newContent).toBe("")
|
|
|
+ expect(history.canUndo).toBe(true)
|
|
|
+
|
|
|
+ // Step 2 undo — revert the split (merge block B back into block A)
|
|
|
+ const undo2 = history.undo()
|
|
|
+ expect(undo2).not.toBeNull()
|
|
|
+ expect(undo2!.type).toBe("merge-block")
|
|
|
+ expect(history.canUndo).toBe(true)
|
|
|
+
|
|
|
+ // Step 1 undo — revert block A typing
|
|
|
+ const undo3 = history.undo()
|
|
|
+ expect(undo3).not.toBeNull()
|
|
|
+ expect(undo3!.type).toBe("set-block-content")
|
|
|
+ expect((undo3 as SetBlockContentOp).blockId).toBe("a")
|
|
|
+ expect((undo3 as SetBlockContentOp).newContent).toBe("")
|
|
|
+ expect(history.canUndo).toBe(false)
|
|
|
+ expect(history.undo()).toBeNull()
|
|
|
+ })
|
|
|
})
|
|
|
|
|
|
describe("paste handling", () => {
|