1
2
3
4
14import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
15
16const { batchMock } = vi.hoisted(() => ({ batchMock: vi.fn().mockResolvedValue({ applied: 0 }) }));
17
18vi.mock("../api", () => ({
19 blockApi: { batch: batchMock },
20}));
21
22import { BlockBatchQueue } from "../batch-queue";
23import type { BatchBlockOp } from "../api";
24
25const UPDATE = (id: string, content: unknown): BatchBlockOp => ({
26 op: "update",
27 blockId: id,
28 patch: { content },
29});
30
31describe("BlockBatchQueue.dropBlock", () => {
32 beforeEach(() => {
33 vi.useFakeTimers();
34 batchMock.mockClear();
35
36
37
38 batchMock.mockResolvedValue({ applied: 0 });
39 });
40
41 afterEach(() => {
42 vi.useRealTimers();
43 });
44
45 it("drops every pending op that touches the block (both op shapes)", () => {
46 const q = new BlockBatchQueue("page-1", 5000);
47 q.queue([
48 UPDATE("a", "<p>keep</p>"),
49 { op: "upsert", blockId: "b", block: { id: "b", parentId: null, type: "paragraph", content: "" } },
50 UPDATE("b", "<p>gone</p>"),
51 UPDATE("c", "<p>keep</p>"),
52 ]);
53 expect(q.pendingCount).toBe(4);
54
55 q.dropBlock("b");
56
57
58
59 expect(q.pendingCount).toBe(2);
60 vi.advanceTimersByTime(5000);
61 const ops = batchMock.mock.calls[0]?.[1] as BatchBlockOp[];
62 expect(ops.map((o) => o.blockId)).toEqual(["a", "c"]);
63 });
64
65 it("keeps a sibling's pending ops so the flush still lands them", () => {
66 const q = new BlockBatchQueue("page-1", 5000);
67 q.queue(UPDATE("keep", "<p>x</p>"));
68 q.dropBlock("deleted");
69 expect(q.pendingCount).toBe(1);
70 q.dispose();
71 expect(q.pendingCount).toBe(1);
72 });
73
74 it("is a no-op on an empty queue (no throw, no timer change)", () => {
75 const q = new BlockBatchQueue("page-1", 5000);
76 expect(() => q.dropBlock("ghost")).not.toThrow();
77 expect(q.pendingCount).toBe(0);
78 q.dispose();
79 });
80
81 it("a flush after dropBlock never carries the deleted id (the race that 400s the batch)", () => {
82 const q = new BlockBatchQueue("page-1", 5000);
83
84
85 q.queue(UPDATE("victim", ""));
86 q.dropBlock("victim");
87
88
89 q.queue(UPDATE("other", "<p>real</p>"));
90 vi.advanceTimersByTime(5000);
91
92 expect(batchMock).toHaveBeenCalledTimes(1);
93 const ops = batchMock.mock.calls[0]?.[1] as BatchBlockOp[];
94 expect(ops).toEqual([{ op: "update", blockId: "other", patch: { content: "<p>real</p>" } }]);
95 });
96});
97