@voidhash/mimic-react 1.0.0-beta.16 → 1.0.0-beta.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/zustand/useDraft.d.cts +3 -3
- package/dist/zustand/useDraft.d.cts.map +1 -1
- package/dist/zustand/useDraft.d.mts +3 -3
- package/dist/zustand/useDraft.d.mts.map +1 -1
- package/dist/zustand/useDraft.mjs.map +1 -1
- package/package.json +15 -8
- package/src/zustand/useDraft.ts +2 -2
- package/.turbo/turbo-build.log +0 -76
- package/tests/zustand/middleware.test.ts +0 -588
- package/tests/zustand-commander/commander.test.ts +0 -1082
- package/tsconfig.build.json +0 -24
- package/tsconfig.json +0 -8
- package/tsdown.config.ts +0 -18
- package/vitest.mts +0 -11
|
@@ -1,1082 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { createStore, type StoreApi } from "zustand";
|
|
3
|
-
import {
|
|
4
|
-
createCommander,
|
|
5
|
-
performUndo,
|
|
6
|
-
performRedo,
|
|
7
|
-
clearUndoHistory,
|
|
8
|
-
isCommand,
|
|
9
|
-
isUndoableCommand,
|
|
10
|
-
COMMAND_SYMBOL,
|
|
11
|
-
UNDOABLE_COMMAND_SYMBOL,
|
|
12
|
-
type CommanderSlice,
|
|
13
|
-
type Command,
|
|
14
|
-
type CommandContext,
|
|
15
|
-
type UndoableCommand,
|
|
16
|
-
} from "../../src/zustand-commander/index";
|
|
17
|
-
|
|
18
|
-
// =============================================================================
|
|
19
|
-
// Test Store Types
|
|
20
|
-
// =============================================================================
|
|
21
|
-
|
|
22
|
-
interface TestState {
|
|
23
|
-
count: number;
|
|
24
|
-
items: string[];
|
|
25
|
-
selectedId: string | null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
type TestStore = TestState & CommanderSlice;
|
|
29
|
-
|
|
30
|
-
// =============================================================================
|
|
31
|
-
// Helper Functions
|
|
32
|
-
// =============================================================================
|
|
33
|
-
|
|
34
|
-
function createTestStore(
|
|
35
|
-
commander: ReturnType<typeof createCommander<TestState>>,
|
|
36
|
-
initial?: Partial<TestState>
|
|
37
|
-
) {
|
|
38
|
-
return createStore<TestStore>(
|
|
39
|
-
commander.middleware(() => ({
|
|
40
|
-
count: 0,
|
|
41
|
-
items: [],
|
|
42
|
-
selectedId: null,
|
|
43
|
-
...initial,
|
|
44
|
-
}))
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// =============================================================================
|
|
49
|
-
// Tests
|
|
50
|
-
// =============================================================================
|
|
51
|
-
|
|
52
|
-
describe("createCommander", () => {
|
|
53
|
-
it("should create a commander instance with action and undoableAction methods", () => {
|
|
54
|
-
const commander = createCommander<TestState>();
|
|
55
|
-
|
|
56
|
-
expect(commander).toBeDefined();
|
|
57
|
-
expect(typeof commander.action).toBe("function");
|
|
58
|
-
expect(typeof commander.undoableAction).toBe("function");
|
|
59
|
-
expect(typeof commander.middleware).toBe("function");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("should respect maxUndoStackSize option", () => {
|
|
63
|
-
const commander = createCommander<TestState>({ maxUndoStackSize: 2 });
|
|
64
|
-
const store = createTestStore(commander);
|
|
65
|
-
|
|
66
|
-
// Create an undoable action
|
|
67
|
-
const increment = commander.undoableAction(
|
|
68
|
-
(ctx) => {
|
|
69
|
-
const current = ctx.getState().count;
|
|
70
|
-
ctx.setState({ count: current + 1 });
|
|
71
|
-
return { previousCount: current };
|
|
72
|
-
},
|
|
73
|
-
(ctx, _params, result) => {
|
|
74
|
-
ctx.setState({ count: result.previousCount });
|
|
75
|
-
}
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
// Get dispatch via middleware context
|
|
79
|
-
const storeApi = store as unknown as StoreApi<TestStore>;
|
|
80
|
-
const ctx = {
|
|
81
|
-
getState: () => storeApi.getState(),
|
|
82
|
-
setState: (partial: Partial<TestStore>) => storeApi.setState(partial),
|
|
83
|
-
dispatch: <TParams, TReturn>(cmd: Command<TestStore, TParams, TReturn>) => {
|
|
84
|
-
return (params: TParams): TReturn => cmd.fn(ctx, params);
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
// Execute 5 times - stack should only keep last 2
|
|
89
|
-
for (let i = 0; i < 5; i++) {
|
|
90
|
-
increment.fn(ctx, undefined);
|
|
91
|
-
// Manually add to undo stack as we're bypassing the full dispatch
|
|
92
|
-
storeApi.setState((state: TestStore) => ({
|
|
93
|
-
...state,
|
|
94
|
-
_commander: {
|
|
95
|
-
undoStack: [...state._commander.undoStack, {
|
|
96
|
-
command: increment,
|
|
97
|
-
params: undefined,
|
|
98
|
-
result: { previousCount: i },
|
|
99
|
-
timestamp: Date.now(),
|
|
100
|
-
}].slice(-2),
|
|
101
|
-
redoStack: [],
|
|
102
|
-
},
|
|
103
|
-
}));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
expect(store.getState()._commander.undoStack.length).toBe(2);
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
describe("action (regular commands)", () => {
|
|
111
|
-
let commander: ReturnType<typeof createCommander<TestState>>;
|
|
112
|
-
let store: ReturnType<typeof createTestStore>;
|
|
113
|
-
|
|
114
|
-
beforeEach(() => {
|
|
115
|
-
commander = createCommander<TestState>();
|
|
116
|
-
store = createTestStore(commander);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("should create command with typed params", () => {
|
|
120
|
-
const setCount = commander.action<{ value: number }>(
|
|
121
|
-
(ctx, params) => {
|
|
122
|
-
ctx.setState({ count: params.value });
|
|
123
|
-
}
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
expect(setCount).toBeDefined();
|
|
127
|
-
expect(setCount[COMMAND_SYMBOL]).toBe(true);
|
|
128
|
-
expect(typeof setCount.fn).toBe("function");
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("should create command without params (void)", () => {
|
|
132
|
-
const reset = commander.action((ctx) => {
|
|
133
|
-
ctx.setState({ count: 0, items: [], selectedId: null });
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
expect(reset).toBeDefined();
|
|
137
|
-
expect(reset[COMMAND_SYMBOL]).toBe(true);
|
|
138
|
-
expect(typeof reset.fn).toBe("function");
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("should execute command and modify state", () => {
|
|
142
|
-
const setCount = commander.action<{ value: number }>(
|
|
143
|
-
(ctx, params) => {
|
|
144
|
-
ctx.setState({ count: params.value });
|
|
145
|
-
}
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
const storeApi = store as unknown as StoreApi<TestStore>;
|
|
149
|
-
const ctx = {
|
|
150
|
-
getState: () => storeApi.getState(),
|
|
151
|
-
setState: (partial: Partial<TestStore>) => storeApi.setState(partial),
|
|
152
|
-
dispatch: <TParams, TReturn>(cmd: Command<TestStore, TParams, TReturn>) => {
|
|
153
|
-
return (params: TParams): TReturn => cmd.fn(ctx, params);
|
|
154
|
-
},
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
expect(store.getState().count).toBe(0);
|
|
158
|
-
|
|
159
|
-
setCount.fn(ctx, { value: 42 });
|
|
160
|
-
|
|
161
|
-
expect(store.getState().count).toBe(42);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it("should NOT add to undo stack for regular actions", () => {
|
|
165
|
-
const setCount = commander.action<{ value: number }>(
|
|
166
|
-
(ctx, params) => {
|
|
167
|
-
ctx.setState({ count: params.value });
|
|
168
|
-
}
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
const storeApi = store as unknown as StoreApi<TestStore>;
|
|
172
|
-
const ctx = {
|
|
173
|
-
getState: () => storeApi.getState(),
|
|
174
|
-
setState: (partial: Partial<TestStore>) => storeApi.setState(partial),
|
|
175
|
-
dispatch: <TParams, TReturn>(cmd: Command<TestStore, TParams, TReturn>) => {
|
|
176
|
-
return (params: TParams): TReturn => cmd.fn(ctx, params);
|
|
177
|
-
},
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
// Execute the action
|
|
181
|
-
setCount.fn(ctx, { value: 100 });
|
|
182
|
-
|
|
183
|
-
// Undo stack should remain empty (regular action doesn't add to stack)
|
|
184
|
-
expect(store.getState()._commander.undoStack.length).toBe(0);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("should return value from action", () => {
|
|
188
|
-
const getDoubled = commander.action<{ value: number }, number>(
|
|
189
|
-
(_ctx, params) => {
|
|
190
|
-
return params.value * 2;
|
|
191
|
-
}
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
const storeApi = store as unknown as StoreApi<TestStore>;
|
|
195
|
-
const ctx = {
|
|
196
|
-
getState: () => storeApi.getState(),
|
|
197
|
-
setState: (partial: Partial<TestStore>) => storeApi.setState(partial),
|
|
198
|
-
dispatch: <TParams, TReturn>(cmd: Command<TestStore, TParams, TReturn>) => {
|
|
199
|
-
return (params: TParams): TReturn => cmd.fn(ctx, params);
|
|
200
|
-
},
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
const result = getDoubled.fn(ctx, { value: 21 });
|
|
204
|
-
expect(result).toBe(42);
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
describe("undoableAction", () => {
|
|
209
|
-
let commander: ReturnType<typeof createCommander<TestState>>;
|
|
210
|
-
let store: ReturnType<typeof createTestStore>;
|
|
211
|
-
|
|
212
|
-
beforeEach(() => {
|
|
213
|
-
commander = createCommander<TestState>();
|
|
214
|
-
store = createTestStore(commander);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it("should create undoable command with typed params", () => {
|
|
218
|
-
const addItem = commander.undoableAction<{ item: string }, { index: number }>(
|
|
219
|
-
(ctx, params) => {
|
|
220
|
-
const items = [...ctx.getState().items, params.item];
|
|
221
|
-
ctx.setState({ items });
|
|
222
|
-
return { index: items.length - 1 };
|
|
223
|
-
},
|
|
224
|
-
(ctx, _params, result) => {
|
|
225
|
-
const items = ctx.getState().items.filter((_, i) => i !== result.index);
|
|
226
|
-
ctx.setState({ items });
|
|
227
|
-
}
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
expect(addItem).toBeDefined();
|
|
231
|
-
expect(addItem[COMMAND_SYMBOL]).toBe(true);
|
|
232
|
-
expect(addItem[UNDOABLE_COMMAND_SYMBOL]).toBe(true);
|
|
233
|
-
expect(typeof addItem.fn).toBe("function");
|
|
234
|
-
expect(typeof addItem.revert).toBe("function");
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("should create undoable command without params", () => {
|
|
238
|
-
const increment = commander.undoableAction<void, { previousCount: number }>(
|
|
239
|
-
(ctx) => {
|
|
240
|
-
const current = ctx.getState().count;
|
|
241
|
-
ctx.setState({ count: current + 1 });
|
|
242
|
-
return { previousCount: current };
|
|
243
|
-
},
|
|
244
|
-
(ctx, _params, result) => {
|
|
245
|
-
ctx.setState({ count: result.previousCount });
|
|
246
|
-
}
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
expect(increment).toBeDefined();
|
|
250
|
-
expect(increment[COMMAND_SYMBOL]).toBe(true);
|
|
251
|
-
expect(increment[UNDOABLE_COMMAND_SYMBOL]).toBe(true);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it("should execute undoable action and modify state", () => {
|
|
255
|
-
const setCount = commander.undoableAction<{ value: number }, { previousValue: number }>(
|
|
256
|
-
(ctx, params) => {
|
|
257
|
-
const prev = ctx.getState().count;
|
|
258
|
-
ctx.setState({ count: params.value });
|
|
259
|
-
return { previousValue: prev };
|
|
260
|
-
},
|
|
261
|
-
(ctx, _params, result) => {
|
|
262
|
-
ctx.setState({ count: result.previousValue });
|
|
263
|
-
}
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
const storeApi = store as unknown as StoreApi<TestStore>;
|
|
267
|
-
const ctx = {
|
|
268
|
-
getState: () => storeApi.getState(),
|
|
269
|
-
setState: (partial: Partial<TestStore>) => storeApi.setState(partial),
|
|
270
|
-
dispatch: <TParams, TReturn>(cmd: Command<TestStore, TParams, TReturn>) => {
|
|
271
|
-
return (params: TParams): TReturn => cmd.fn(ctx, params);
|
|
272
|
-
},
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
expect(store.getState().count).toBe(0);
|
|
276
|
-
|
|
277
|
-
setCount.fn(ctx, { value: 50 });
|
|
278
|
-
|
|
279
|
-
expect(store.getState().count).toBe(50);
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
describe("performUndo", () => {
|
|
284
|
-
let commander: ReturnType<typeof createCommander<TestState>>;
|
|
285
|
-
let store: ReturnType<typeof createTestStore>;
|
|
286
|
-
|
|
287
|
-
beforeEach(() => {
|
|
288
|
-
commander = createCommander<TestState>();
|
|
289
|
-
store = createTestStore(commander);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it("should revert last action", () => {
|
|
293
|
-
const increment = commander.undoableAction(
|
|
294
|
-
(ctx) => {
|
|
295
|
-
const current = ctx.getState().count;
|
|
296
|
-
ctx.setState({ count: current + 1 });
|
|
297
|
-
return { previousCount: current };
|
|
298
|
-
},
|
|
299
|
-
(ctx, _params, result) => {
|
|
300
|
-
ctx.setState({ count: result.previousCount });
|
|
301
|
-
}
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
const storeApi = store as unknown as StoreApi<TestStore>;
|
|
305
|
-
|
|
306
|
-
// Manually set up undo stack with an entry
|
|
307
|
-
store.setState((state: TestStore) => ({
|
|
308
|
-
...state,
|
|
309
|
-
count: 5,
|
|
310
|
-
_commander: {
|
|
311
|
-
undoStack: [{
|
|
312
|
-
command: increment,
|
|
313
|
-
params: undefined,
|
|
314
|
-
result: { previousCount: 4 },
|
|
315
|
-
timestamp: Date.now(),
|
|
316
|
-
}],
|
|
317
|
-
redoStack: [],
|
|
318
|
-
},
|
|
319
|
-
}));
|
|
320
|
-
|
|
321
|
-
expect(store.getState().count).toBe(5);
|
|
322
|
-
|
|
323
|
-
const result = performUndo(storeApi);
|
|
324
|
-
|
|
325
|
-
expect(result).toBe(true);
|
|
326
|
-
expect(store.getState().count).toBe(4);
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
it("should move entry to redo stack", () => {
|
|
330
|
-
const increment = commander.undoableAction(
|
|
331
|
-
(ctx) => {
|
|
332
|
-
const current = ctx.getState().count;
|
|
333
|
-
ctx.setState({ count: current + 1 });
|
|
334
|
-
return { previousCount: current };
|
|
335
|
-
},
|
|
336
|
-
(ctx, _params, result) => {
|
|
337
|
-
ctx.setState({ count: result.previousCount });
|
|
338
|
-
}
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
const storeApi = store as unknown as StoreApi<TestStore>;
|
|
342
|
-
|
|
343
|
-
// Set up with one entry in undo stack
|
|
344
|
-
store.setState((state: TestStore) => ({
|
|
345
|
-
...state,
|
|
346
|
-
count: 10,
|
|
347
|
-
_commander: {
|
|
348
|
-
undoStack: [{
|
|
349
|
-
command: increment,
|
|
350
|
-
params: undefined,
|
|
351
|
-
result: { previousCount: 9 },
|
|
352
|
-
timestamp: Date.now(),
|
|
353
|
-
}],
|
|
354
|
-
redoStack: [],
|
|
355
|
-
},
|
|
356
|
-
}));
|
|
357
|
-
|
|
358
|
-
expect(store.getState()._commander.undoStack.length).toBe(1);
|
|
359
|
-
expect(store.getState()._commander.redoStack.length).toBe(0);
|
|
360
|
-
|
|
361
|
-
performUndo(storeApi);
|
|
362
|
-
|
|
363
|
-
expect(store.getState()._commander.undoStack.length).toBe(0);
|
|
364
|
-
expect(store.getState()._commander.redoStack.length).toBe(1);
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
it("should return false when undo stack is empty", () => {
|
|
368
|
-
const storeApi = store as unknown as StoreApi<TestStore>;
|
|
369
|
-
|
|
370
|
-
expect(store.getState()._commander.undoStack.length).toBe(0);
|
|
371
|
-
|
|
372
|
-
const result = performUndo(storeApi);
|
|
373
|
-
|
|
374
|
-
expect(result).toBe(false);
|
|
375
|
-
});
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
describe("performRedo", () => {
|
|
379
|
-
let commander: ReturnType<typeof createCommander<TestState>>;
|
|
380
|
-
let store: ReturnType<typeof createTestStore>;
|
|
381
|
-
|
|
382
|
-
beforeEach(() => {
|
|
383
|
-
commander = createCommander<TestState>();
|
|
384
|
-
store = createTestStore(commander);
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
it("should re-execute undone action", () => {
|
|
388
|
-
const increment = commander.undoableAction(
|
|
389
|
-
(ctx) => {
|
|
390
|
-
const current = ctx.getState().count;
|
|
391
|
-
ctx.setState({ count: current + 1 });
|
|
392
|
-
return { previousCount: current };
|
|
393
|
-
},
|
|
394
|
-
(ctx, _params, result) => {
|
|
395
|
-
ctx.setState({ count: result.previousCount });
|
|
396
|
-
}
|
|
397
|
-
);
|
|
398
|
-
|
|
399
|
-
const storeApi = store as unknown as StoreApi<TestStore>;
|
|
400
|
-
|
|
401
|
-
// Set up with an entry in redo stack
|
|
402
|
-
store.setState((state: TestStore) => ({
|
|
403
|
-
...state,
|
|
404
|
-
count: 4,
|
|
405
|
-
_commander: {
|
|
406
|
-
undoStack: [],
|
|
407
|
-
redoStack: [{
|
|
408
|
-
command: increment,
|
|
409
|
-
params: undefined,
|
|
410
|
-
result: { previousCount: 4 },
|
|
411
|
-
timestamp: Date.now(),
|
|
412
|
-
}],
|
|
413
|
-
},
|
|
414
|
-
}));
|
|
415
|
-
|
|
416
|
-
expect(store.getState().count).toBe(4);
|
|
417
|
-
|
|
418
|
-
const result = performRedo(storeApi);
|
|
419
|
-
|
|
420
|
-
expect(result).toBe(true);
|
|
421
|
-
expect(store.getState().count).toBe(5);
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it("should move entry back to undo stack", () => {
|
|
425
|
-
const increment = commander.undoableAction(
|
|
426
|
-
(ctx) => {
|
|
427
|
-
const current = ctx.getState().count;
|
|
428
|
-
ctx.setState({ count: current + 1 });
|
|
429
|
-
return { previousCount: current };
|
|
430
|
-
},
|
|
431
|
-
(ctx, _params, result) => {
|
|
432
|
-
ctx.setState({ count: result.previousCount });
|
|
433
|
-
}
|
|
434
|
-
);
|
|
435
|
-
|
|
436
|
-
const storeApi = store as unknown as StoreApi<TestStore>;
|
|
437
|
-
|
|
438
|
-
// Set up with an entry in redo stack
|
|
439
|
-
store.setState((state: TestStore) => ({
|
|
440
|
-
...state,
|
|
441
|
-
count: 0,
|
|
442
|
-
_commander: {
|
|
443
|
-
undoStack: [],
|
|
444
|
-
redoStack: [{
|
|
445
|
-
command: increment,
|
|
446
|
-
params: undefined,
|
|
447
|
-
result: { previousCount: 0 },
|
|
448
|
-
timestamp: Date.now(),
|
|
449
|
-
}],
|
|
450
|
-
},
|
|
451
|
-
}));
|
|
452
|
-
|
|
453
|
-
expect(store.getState()._commander.undoStack.length).toBe(0);
|
|
454
|
-
expect(store.getState()._commander.redoStack.length).toBe(1);
|
|
455
|
-
|
|
456
|
-
performRedo(storeApi);
|
|
457
|
-
|
|
458
|
-
expect(store.getState()._commander.undoStack.length).toBe(1);
|
|
459
|
-
expect(store.getState()._commander.redoStack.length).toBe(0);
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
it("should return false when redo stack is empty", () => {
|
|
463
|
-
const storeApi = store as unknown as StoreApi<TestStore>;
|
|
464
|
-
|
|
465
|
-
expect(store.getState()._commander.redoStack.length).toBe(0);
|
|
466
|
-
|
|
467
|
-
const result = performRedo(storeApi);
|
|
468
|
-
|
|
469
|
-
expect(result).toBe(false);
|
|
470
|
-
});
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
describe("clearUndoHistory", () => {
|
|
474
|
-
it("should clear both stacks", () => {
|
|
475
|
-
const commander = createCommander<TestState>();
|
|
476
|
-
const store = createTestStore(commander);
|
|
477
|
-
|
|
478
|
-
const increment = commander.undoableAction(
|
|
479
|
-
(ctx) => {
|
|
480
|
-
const current = ctx.getState().count;
|
|
481
|
-
ctx.setState({ count: current + 1 });
|
|
482
|
-
return { previousCount: current };
|
|
483
|
-
},
|
|
484
|
-
(ctx, _params, result) => {
|
|
485
|
-
ctx.setState({ count: result.previousCount });
|
|
486
|
-
}
|
|
487
|
-
);
|
|
488
|
-
|
|
489
|
-
const storeApi = store as unknown as StoreApi<TestStore>;
|
|
490
|
-
|
|
491
|
-
// Add entries to both stacks
|
|
492
|
-
store.setState((state: TestStore) => ({
|
|
493
|
-
...state,
|
|
494
|
-
_commander: {
|
|
495
|
-
undoStack: [{
|
|
496
|
-
command: increment,
|
|
497
|
-
params: undefined,
|
|
498
|
-
result: { previousCount: 0 },
|
|
499
|
-
timestamp: Date.now(),
|
|
500
|
-
}],
|
|
501
|
-
redoStack: [{
|
|
502
|
-
command: increment,
|
|
503
|
-
params: undefined,
|
|
504
|
-
result: { previousCount: 1 },
|
|
505
|
-
timestamp: Date.now(),
|
|
506
|
-
}],
|
|
507
|
-
},
|
|
508
|
-
}));
|
|
509
|
-
|
|
510
|
-
expect(store.getState()._commander.undoStack.length).toBe(1);
|
|
511
|
-
expect(store.getState()._commander.redoStack.length).toBe(1);
|
|
512
|
-
|
|
513
|
-
clearUndoHistory(storeApi);
|
|
514
|
-
|
|
515
|
-
expect(store.getState()._commander.undoStack.length).toBe(0);
|
|
516
|
-
expect(store.getState()._commander.redoStack.length).toBe(0);
|
|
517
|
-
});
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
describe("Type Guards", () => {
|
|
521
|
-
let commander: ReturnType<typeof createCommander<TestState>>;
|
|
522
|
-
|
|
523
|
-
beforeEach(() => {
|
|
524
|
-
commander = createCommander<TestState>();
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
describe("isCommand", () => {
|
|
528
|
-
it("should return true for regular commands", () => {
|
|
529
|
-
const cmd = commander.action((ctx) => {
|
|
530
|
-
ctx.setState({ count: 0 });
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
expect(isCommand(cmd)).toBe(true);
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
it("should return true for undoable commands", () => {
|
|
537
|
-
const cmd = commander.undoableAction(
|
|
538
|
-
(ctx) => {
|
|
539
|
-
const prev = ctx.getState().count;
|
|
540
|
-
ctx.setState({ count: 0 });
|
|
541
|
-
return { prev };
|
|
542
|
-
},
|
|
543
|
-
(ctx, _params, result) => {
|
|
544
|
-
ctx.setState({ count: result.prev });
|
|
545
|
-
}
|
|
546
|
-
);
|
|
547
|
-
|
|
548
|
-
expect(isCommand(cmd)).toBe(true);
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
it("should return false for non-commands", () => {
|
|
552
|
-
expect(isCommand(null)).toBe(false);
|
|
553
|
-
expect(isCommand(undefined)).toBe(false);
|
|
554
|
-
expect(isCommand({})).toBe(false);
|
|
555
|
-
expect(isCommand({ fn: () => {} })).toBe(false);
|
|
556
|
-
expect(isCommand("string")).toBe(false);
|
|
557
|
-
expect(isCommand(42)).toBe(false);
|
|
558
|
-
});
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
describe("isUndoableCommand", () => {
|
|
562
|
-
it("should return true for undoable commands", () => {
|
|
563
|
-
const cmd = commander.undoableAction(
|
|
564
|
-
(ctx) => {
|
|
565
|
-
const prev = ctx.getState().count;
|
|
566
|
-
ctx.setState({ count: 0 });
|
|
567
|
-
return { prev };
|
|
568
|
-
},
|
|
569
|
-
(ctx, _params, result) => {
|
|
570
|
-
ctx.setState({ count: result.prev });
|
|
571
|
-
}
|
|
572
|
-
);
|
|
573
|
-
|
|
574
|
-
expect(isUndoableCommand(cmd)).toBe(true);
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
it("should return false for regular commands", () => {
|
|
578
|
-
const cmd = commander.action((ctx) => {
|
|
579
|
-
ctx.setState({ count: 0 });
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
expect(isUndoableCommand(cmd)).toBe(false);
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
it("should return false for non-commands", () => {
|
|
586
|
-
expect(isUndoableCommand(null)).toBe(false);
|
|
587
|
-
expect(isUndoableCommand(undefined)).toBe(false);
|
|
588
|
-
expect(isUndoableCommand({})).toBe(false);
|
|
589
|
-
});
|
|
590
|
-
});
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
describe("Middleware", () => {
|
|
594
|
-
it("should add _commander slice to store", () => {
|
|
595
|
-
const commander = createCommander<TestState>();
|
|
596
|
-
const store = createTestStore(commander);
|
|
597
|
-
|
|
598
|
-
const state = store.getState();
|
|
599
|
-
|
|
600
|
-
expect(state._commander).toBeDefined();
|
|
601
|
-
expect(state._commander.undoStack).toBeDefined();
|
|
602
|
-
expect(state._commander.redoStack).toBeDefined();
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
it("should initialize with empty stacks", () => {
|
|
606
|
-
const commander = createCommander<TestState>();
|
|
607
|
-
const store = createTestStore(commander);
|
|
608
|
-
|
|
609
|
-
const state = store.getState();
|
|
610
|
-
|
|
611
|
-
expect(state._commander.undoStack).toEqual([]);
|
|
612
|
-
expect(state._commander.redoStack).toEqual([]);
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
it("should preserve user state", () => {
|
|
616
|
-
const commander = createCommander<TestState>();
|
|
617
|
-
const store = createTestStore(commander, {
|
|
618
|
-
count: 100,
|
|
619
|
-
items: ["a", "b"],
|
|
620
|
-
selectedId: "test-id",
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
const state = store.getState();
|
|
624
|
-
|
|
625
|
-
expect(state.count).toBe(100);
|
|
626
|
-
expect(state.items).toEqual(["a", "b"]);
|
|
627
|
-
expect(state.selectedId).toBe("test-id");
|
|
628
|
-
expect(state._commander).toBeDefined();
|
|
629
|
-
});
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
describe("Command Dispatch in Context", () => {
|
|
633
|
-
it("should allow dispatching commands from within commands", () => {
|
|
634
|
-
const commander = createCommander<TestState>();
|
|
635
|
-
const store = createTestStore(commander);
|
|
636
|
-
|
|
637
|
-
const addItem = commander.action<{ item: string }>(
|
|
638
|
-
(ctx, params) => {
|
|
639
|
-
const items = [...ctx.getState().items, params.item];
|
|
640
|
-
ctx.setState({ items });
|
|
641
|
-
}
|
|
642
|
-
);
|
|
643
|
-
|
|
644
|
-
const addMultiple = commander.action<{ items: string[] }>(
|
|
645
|
-
(ctx, params) => {
|
|
646
|
-
for (const item of params.items) {
|
|
647
|
-
ctx.dispatch(addItem)({ item });
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
);
|
|
651
|
-
|
|
652
|
-
const storeApi = store as unknown as StoreApi<TestStore>;
|
|
653
|
-
|
|
654
|
-
// Create a proper dispatch function
|
|
655
|
-
const createCtx = (): CommandContext<TestStore> => ({
|
|
656
|
-
getState: () => storeApi.getState(),
|
|
657
|
-
setState: (partial: Partial<TestStore>) => storeApi.setState(partial),
|
|
658
|
-
dispatch: <TParams, TReturn>(cmd: Command<TestStore, TParams, TReturn>) => {
|
|
659
|
-
return (params: TParams): TReturn => cmd.fn(createCtx(), params);
|
|
660
|
-
},
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
const ctx: CommandContext<TestStore> = createCtx();
|
|
664
|
-
|
|
665
|
-
addMultiple.fn(ctx, { items: ["x", "y", "z"] });
|
|
666
|
-
|
|
667
|
-
expect(store.getState().items).toEqual(["x", "y", "z"]);
|
|
668
|
-
});
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
describe("ctx.transaction routing (draft vs document)", () => {
|
|
672
|
-
it("should route ctx.transaction to document.transaction when no draft is active", () => {
|
|
673
|
-
const commander = createCommander<TestState>();
|
|
674
|
-
|
|
675
|
-
// Track calls to document.transaction
|
|
676
|
-
const transactionCalls: Array<(root: any) => void> = [];
|
|
677
|
-
const mockDocument = {
|
|
678
|
-
transaction: (fn: (root: any) => void) => {
|
|
679
|
-
transactionCalls.push(fn);
|
|
680
|
-
},
|
|
681
|
-
};
|
|
682
|
-
|
|
683
|
-
// Create store with mimic slice containing the mock document
|
|
684
|
-
const store = createStore<TestStore & { mimic: { document: typeof mockDocument } }>(
|
|
685
|
-
commander.middleware(() => ({
|
|
686
|
-
count: 0,
|
|
687
|
-
items: [],
|
|
688
|
-
selectedId: null,
|
|
689
|
-
mimic: { document: mockDocument },
|
|
690
|
-
}))
|
|
691
|
-
);
|
|
692
|
-
|
|
693
|
-
// Create a command that uses ctx.transaction
|
|
694
|
-
const updateViaTransaction = commander.action<{ value: number }>(
|
|
695
|
-
(ctx, params) => {
|
|
696
|
-
ctx.transaction((root: any) => {
|
|
697
|
-
root.count.set(params.value);
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
);
|
|
701
|
-
|
|
702
|
-
const storeApi = store as unknown as StoreApi<TestStore & { mimic: { document: typeof mockDocument } }>;
|
|
703
|
-
|
|
704
|
-
// Build a proper context with transaction routing
|
|
705
|
-
const ctx: CommandContext<TestStore> = {
|
|
706
|
-
getState: () => storeApi.getState(),
|
|
707
|
-
setState: (partial: Partial<TestStore>) => storeApi.setState(partial as any),
|
|
708
|
-
dispatch: <TParams, TReturn>(cmd: Command<TestStore, TParams, TReturn>) => {
|
|
709
|
-
return (params: TParams): TReturn => cmd.fn(ctx, params);
|
|
710
|
-
},
|
|
711
|
-
transaction: (fn: (root: any) => void) => {
|
|
712
|
-
const state = storeApi.getState();
|
|
713
|
-
const draft = state._commander.activeDraft;
|
|
714
|
-
if (draft) {
|
|
715
|
-
draft.update(fn);
|
|
716
|
-
} else {
|
|
717
|
-
(state as any).mimic.document.transaction(fn);
|
|
718
|
-
}
|
|
719
|
-
},
|
|
720
|
-
};
|
|
721
|
-
|
|
722
|
-
// Execute command - should route to document.transaction
|
|
723
|
-
updateViaTransaction.fn(ctx, { value: 42 });
|
|
724
|
-
|
|
725
|
-
expect(transactionCalls.length).toBe(1);
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
it("should route ctx.transaction to draft.update when draft is active", () => {
|
|
729
|
-
const commander = createCommander<TestState>();
|
|
730
|
-
|
|
731
|
-
// Track calls to both document.transaction and draft.update
|
|
732
|
-
const documentTransactionCalls: Array<(root: any) => void> = [];
|
|
733
|
-
const draftUpdateCalls: Array<(root: any) => void> = [];
|
|
734
|
-
|
|
735
|
-
const mockDocument = {
|
|
736
|
-
transaction: (fn: (root: any) => void) => {
|
|
737
|
-
documentTransactionCalls.push(fn);
|
|
738
|
-
},
|
|
739
|
-
};
|
|
740
|
-
|
|
741
|
-
const mockDraft = {
|
|
742
|
-
update: (fn: (root: any) => void) => {
|
|
743
|
-
draftUpdateCalls.push(fn);
|
|
744
|
-
},
|
|
745
|
-
commit: () => {},
|
|
746
|
-
discard: () => {},
|
|
747
|
-
id: "mock-draft-id",
|
|
748
|
-
};
|
|
749
|
-
|
|
750
|
-
// Create store with mimic slice
|
|
751
|
-
const store = createStore<TestStore & { mimic: { document: typeof mockDocument } }>(
|
|
752
|
-
commander.middleware(() => ({
|
|
753
|
-
count: 0,
|
|
754
|
-
items: [],
|
|
755
|
-
selectedId: null,
|
|
756
|
-
mimic: { document: mockDocument },
|
|
757
|
-
}))
|
|
758
|
-
);
|
|
759
|
-
|
|
760
|
-
// Set the active draft
|
|
761
|
-
store.setState((state) => ({
|
|
762
|
-
...state,
|
|
763
|
-
_commander: {
|
|
764
|
-
...state._commander,
|
|
765
|
-
activeDraft: mockDraft as any,
|
|
766
|
-
},
|
|
767
|
-
}));
|
|
768
|
-
|
|
769
|
-
// Create a command that uses ctx.transaction
|
|
770
|
-
const updateViaTransaction = commander.action<{ value: number }>(
|
|
771
|
-
(ctx, params) => {
|
|
772
|
-
ctx.transaction((root: any) => {
|
|
773
|
-
root.count.set(params.value);
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
);
|
|
777
|
-
|
|
778
|
-
const storeApi = store as unknown as StoreApi<TestStore & { mimic: { document: typeof mockDocument } }>;
|
|
779
|
-
|
|
780
|
-
// Build a proper context with transaction routing
|
|
781
|
-
const ctx: CommandContext<TestStore> = {
|
|
782
|
-
getState: () => storeApi.getState(),
|
|
783
|
-
setState: (partial: Partial<TestStore>) => storeApi.setState(partial as any),
|
|
784
|
-
dispatch: <TParams, TReturn>(cmd: Command<TestStore, TParams, TReturn>) => {
|
|
785
|
-
return (params: TParams): TReturn => cmd.fn(ctx, params);
|
|
786
|
-
},
|
|
787
|
-
transaction: (fn: (root: any) => void) => {
|
|
788
|
-
const state = storeApi.getState();
|
|
789
|
-
const draft = state._commander.activeDraft;
|
|
790
|
-
if (draft) {
|
|
791
|
-
draft.update(fn);
|
|
792
|
-
} else {
|
|
793
|
-
(state as any).mimic.document.transaction(fn);
|
|
794
|
-
}
|
|
795
|
-
},
|
|
796
|
-
};
|
|
797
|
-
|
|
798
|
-
// Execute command - should route to draft.update, NOT document.transaction
|
|
799
|
-
updateViaTransaction.fn(ctx, { value: 42 });
|
|
800
|
-
|
|
801
|
-
expect(draftUpdateCalls.length).toBe(1);
|
|
802
|
-
expect(documentTransactionCalls.length).toBe(0);
|
|
803
|
-
});
|
|
804
|
-
|
|
805
|
-
it("should never call document.transaction while draft is active (explicit verification)", () => {
|
|
806
|
-
const commander = createCommander<TestState>();
|
|
807
|
-
|
|
808
|
-
// Track ALL calls
|
|
809
|
-
const documentTransactionCalls: Array<{ fn: (root: any) => void; timestamp: number }> = [];
|
|
810
|
-
const draftUpdateCalls: Array<{ fn: (root: any) => void; timestamp: number }> = [];
|
|
811
|
-
|
|
812
|
-
const mockDocument = {
|
|
813
|
-
transaction: (fn: (root: any) => void) => {
|
|
814
|
-
documentTransactionCalls.push({ fn, timestamp: Date.now() });
|
|
815
|
-
},
|
|
816
|
-
};
|
|
817
|
-
|
|
818
|
-
const mockDraft = {
|
|
819
|
-
update: (fn: (root: any) => void) => {
|
|
820
|
-
draftUpdateCalls.push({ fn, timestamp: Date.now() });
|
|
821
|
-
},
|
|
822
|
-
commit: () => {},
|
|
823
|
-
discard: () => {},
|
|
824
|
-
id: "mock-draft-id",
|
|
825
|
-
};
|
|
826
|
-
|
|
827
|
-
// Create store
|
|
828
|
-
const store = createStore<TestStore & { mimic: { document: typeof mockDocument } }>(
|
|
829
|
-
commander.middleware(() => ({
|
|
830
|
-
count: 0,
|
|
831
|
-
items: [],
|
|
832
|
-
selectedId: null,
|
|
833
|
-
mimic: { document: mockDocument },
|
|
834
|
-
}))
|
|
835
|
-
);
|
|
836
|
-
|
|
837
|
-
const storeApi = store as unknown as StoreApi<TestStore & { mimic: { document: typeof mockDocument } }>;
|
|
838
|
-
|
|
839
|
-
// Helper to build context
|
|
840
|
-
const buildCtx = (): CommandContext<TestStore> => ({
|
|
841
|
-
getState: () => storeApi.getState(),
|
|
842
|
-
setState: (partial: Partial<TestStore>) => storeApi.setState(partial as any),
|
|
843
|
-
dispatch: <TParams, TReturn>(cmd: Command<TestStore, TParams, TReturn>) => {
|
|
844
|
-
return (params: TParams): TReturn => cmd.fn(buildCtx(), params);
|
|
845
|
-
},
|
|
846
|
-
transaction: (fn: (root: any) => void) => {
|
|
847
|
-
const state = storeApi.getState();
|
|
848
|
-
const draft = state._commander.activeDraft;
|
|
849
|
-
if (draft) {
|
|
850
|
-
draft.update(fn);
|
|
851
|
-
} else {
|
|
852
|
-
(state as any).mimic.document.transaction(fn);
|
|
853
|
-
}
|
|
854
|
-
},
|
|
855
|
-
});
|
|
856
|
-
|
|
857
|
-
// Command that uses transaction
|
|
858
|
-
const doUpdate = commander.action<{ value: number }>(
|
|
859
|
-
(ctx, params) => {
|
|
860
|
-
ctx.transaction((root: any) => {
|
|
861
|
-
root.count.set(params.value);
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
);
|
|
865
|
-
|
|
866
|
-
// Test 1: No draft - should go to document.transaction
|
|
867
|
-
doUpdate.fn(buildCtx(), { value: 1 });
|
|
868
|
-
expect(documentTransactionCalls.length).toBe(1);
|
|
869
|
-
expect(draftUpdateCalls.length).toBe(0);
|
|
870
|
-
|
|
871
|
-
// Test 2: Set active draft
|
|
872
|
-
store.setState((state) => ({
|
|
873
|
-
...state,
|
|
874
|
-
_commander: {
|
|
875
|
-
...state._commander,
|
|
876
|
-
activeDraft: mockDraft as any,
|
|
877
|
-
},
|
|
878
|
-
}));
|
|
879
|
-
|
|
880
|
-
// Test 3: With draft active - should go to draft.update
|
|
881
|
-
doUpdate.fn(buildCtx(), { value: 2 });
|
|
882
|
-
expect(documentTransactionCalls.length).toBe(1); // Still 1 - no new calls
|
|
883
|
-
expect(draftUpdateCalls.length).toBe(1);
|
|
884
|
-
|
|
885
|
-
// Test 4: Multiple updates while draft is active
|
|
886
|
-
doUpdate.fn(buildCtx(), { value: 3 });
|
|
887
|
-
doUpdate.fn(buildCtx(), { value: 4 });
|
|
888
|
-
doUpdate.fn(buildCtx(), { value: 5 });
|
|
889
|
-
|
|
890
|
-
expect(documentTransactionCalls.length).toBe(1); // Still 1 - no new calls
|
|
891
|
-
expect(draftUpdateCalls.length).toBe(4); // 4 draft updates
|
|
892
|
-
|
|
893
|
-
// Test 5: Clear draft
|
|
894
|
-
store.setState((state) => ({
|
|
895
|
-
...state,
|
|
896
|
-
_commander: {
|
|
897
|
-
...state._commander,
|
|
898
|
-
activeDraft: null,
|
|
899
|
-
},
|
|
900
|
-
}));
|
|
901
|
-
|
|
902
|
-
// Test 6: Without draft - should go back to document.transaction
|
|
903
|
-
doUpdate.fn(buildCtx(), { value: 6 });
|
|
904
|
-
expect(documentTransactionCalls.length).toBe(2);
|
|
905
|
-
expect(draftUpdateCalls.length).toBe(4);
|
|
906
|
-
});
|
|
907
|
-
|
|
908
|
-
it("should route nested command dispatches to draft when active", () => {
|
|
909
|
-
const commander = createCommander<TestState>();
|
|
910
|
-
|
|
911
|
-
const documentTransactionCalls: Array<(root: any) => void> = [];
|
|
912
|
-
const draftUpdateCalls: Array<(root: any) => void> = [];
|
|
913
|
-
|
|
914
|
-
const mockDocument = {
|
|
915
|
-
transaction: (fn: (root: any) => void) => {
|
|
916
|
-
documentTransactionCalls.push(fn);
|
|
917
|
-
},
|
|
918
|
-
};
|
|
919
|
-
|
|
920
|
-
const mockDraft = {
|
|
921
|
-
update: (fn: (root: any) => void) => {
|
|
922
|
-
draftUpdateCalls.push(fn);
|
|
923
|
-
},
|
|
924
|
-
commit: () => {},
|
|
925
|
-
discard: () => {},
|
|
926
|
-
id: "mock-draft-id",
|
|
927
|
-
};
|
|
928
|
-
|
|
929
|
-
const store = createStore<TestStore & { mimic: { document: typeof mockDocument } }>(
|
|
930
|
-
commander.middleware(() => ({
|
|
931
|
-
count: 0,
|
|
932
|
-
items: [],
|
|
933
|
-
selectedId: null,
|
|
934
|
-
mimic: { document: mockDocument },
|
|
935
|
-
}))
|
|
936
|
-
);
|
|
937
|
-
|
|
938
|
-
// Set active draft
|
|
939
|
-
store.setState((state) => ({
|
|
940
|
-
...state,
|
|
941
|
-
_commander: {
|
|
942
|
-
...state._commander,
|
|
943
|
-
activeDraft: mockDraft as any,
|
|
944
|
-
},
|
|
945
|
-
}));
|
|
946
|
-
|
|
947
|
-
const storeApi = store as unknown as StoreApi<TestStore & { mimic: { document: typeof mockDocument } }>;
|
|
948
|
-
|
|
949
|
-
const buildCtx = (): CommandContext<TestStore> => ({
|
|
950
|
-
getState: () => storeApi.getState(),
|
|
951
|
-
setState: (partial: Partial<TestStore>) => storeApi.setState(partial as any),
|
|
952
|
-
dispatch: <TParams, TReturn>(cmd: Command<TestStore, TParams, TReturn>) => {
|
|
953
|
-
return (params: TParams): TReturn => cmd.fn(buildCtx(), params);
|
|
954
|
-
},
|
|
955
|
-
transaction: (fn: (root: any) => void) => {
|
|
956
|
-
const state = storeApi.getState();
|
|
957
|
-
const draft = state._commander.activeDraft;
|
|
958
|
-
if (draft) {
|
|
959
|
-
draft.update(fn);
|
|
960
|
-
} else {
|
|
961
|
-
(state as any).mimic.document.transaction(fn);
|
|
962
|
-
}
|
|
963
|
-
},
|
|
964
|
-
});
|
|
965
|
-
|
|
966
|
-
// Inner command that uses transaction
|
|
967
|
-
const setCount = commander.action<{ value: number }>(
|
|
968
|
-
(ctx, params) => {
|
|
969
|
-
ctx.transaction((root: any) => {
|
|
970
|
-
root.count.set(params.value);
|
|
971
|
-
});
|
|
972
|
-
}
|
|
973
|
-
);
|
|
974
|
-
|
|
975
|
-
// Outer command that dispatches inner command
|
|
976
|
-
const setMultiple = commander.action<{ values: number[] }>(
|
|
977
|
-
(ctx, params) => {
|
|
978
|
-
for (const value of params.values) {
|
|
979
|
-
ctx.dispatch(setCount)({ value });
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
);
|
|
983
|
-
|
|
984
|
-
// Execute outer command - all nested transactions should go to draft
|
|
985
|
-
setMultiple.fn(buildCtx(), { values: [1, 2, 3, 4, 5] });
|
|
986
|
-
|
|
987
|
-
expect(draftUpdateCalls.length).toBe(5);
|
|
988
|
-
expect(documentTransactionCalls.length).toBe(0);
|
|
989
|
-
});
|
|
990
|
-
});
|
|
991
|
-
|
|
992
|
-
describe("Undo/Redo Integration", () => {
|
|
993
|
-
it("should handle multiple undo operations", () => {
|
|
994
|
-
const commander = createCommander<TestState>();
|
|
995
|
-
const store = createTestStore(commander);
|
|
996
|
-
|
|
997
|
-
const setCount = commander.undoableAction<{ value: number }, { previousValue: number }>(
|
|
998
|
-
(ctx, params) => {
|
|
999
|
-
const prev = ctx.getState().count;
|
|
1000
|
-
ctx.setState({ count: params.value });
|
|
1001
|
-
return { previousValue: prev };
|
|
1002
|
-
},
|
|
1003
|
-
(ctx, _params, result) => {
|
|
1004
|
-
ctx.setState({ count: result.previousValue });
|
|
1005
|
-
}
|
|
1006
|
-
);
|
|
1007
|
-
|
|
1008
|
-
const storeApi = store as unknown as StoreApi<TestStore>;
|
|
1009
|
-
|
|
1010
|
-
// Set up stack with multiple entries: 0 -> 10 -> 20 -> 30
|
|
1011
|
-
store.setState((state: TestStore) => ({
|
|
1012
|
-
...state,
|
|
1013
|
-
count: 30,
|
|
1014
|
-
_commander: {
|
|
1015
|
-
undoStack: [
|
|
1016
|
-
{ command: setCount, params: { value: 10 }, result: { previousValue: 0 }, timestamp: 1 },
|
|
1017
|
-
{ command: setCount, params: { value: 20 }, result: { previousValue: 10 }, timestamp: 2 },
|
|
1018
|
-
{ command: setCount, params: { value: 30 }, result: { previousValue: 20 }, timestamp: 3 },
|
|
1019
|
-
],
|
|
1020
|
-
redoStack: [],
|
|
1021
|
-
},
|
|
1022
|
-
}));
|
|
1023
|
-
|
|
1024
|
-
// Undo 30 -> 20
|
|
1025
|
-
performUndo(storeApi);
|
|
1026
|
-
expect(store.getState().count).toBe(20);
|
|
1027
|
-
|
|
1028
|
-
// Undo 20 -> 10
|
|
1029
|
-
performUndo(storeApi);
|
|
1030
|
-
expect(store.getState().count).toBe(10);
|
|
1031
|
-
|
|
1032
|
-
// Undo 10 -> 0
|
|
1033
|
-
performUndo(storeApi);
|
|
1034
|
-
expect(store.getState().count).toBe(0);
|
|
1035
|
-
|
|
1036
|
-
// Redo stack should have all 3 entries
|
|
1037
|
-
expect(store.getState()._commander.redoStack.length).toBe(3);
|
|
1038
|
-
expect(store.getState()._commander.undoStack.length).toBe(0);
|
|
1039
|
-
});
|
|
1040
|
-
|
|
1041
|
-
it("should handle undo then redo", () => {
|
|
1042
|
-
const commander = createCommander<TestState>();
|
|
1043
|
-
const store = createTestStore(commander);
|
|
1044
|
-
|
|
1045
|
-
const increment = commander.undoableAction(
|
|
1046
|
-
(ctx) => {
|
|
1047
|
-
const current = ctx.getState().count;
|
|
1048
|
-
ctx.setState({ count: current + 1 });
|
|
1049
|
-
return { previousCount: current };
|
|
1050
|
-
},
|
|
1051
|
-
(ctx, _params, result) => {
|
|
1052
|
-
ctx.setState({ count: result.previousCount });
|
|
1053
|
-
}
|
|
1054
|
-
);
|
|
1055
|
-
|
|
1056
|
-
const storeApi = store as unknown as StoreApi<TestStore>;
|
|
1057
|
-
|
|
1058
|
-
// Start at count 5
|
|
1059
|
-
store.setState((state: TestStore) => ({
|
|
1060
|
-
...state,
|
|
1061
|
-
count: 5,
|
|
1062
|
-
_commander: {
|
|
1063
|
-
undoStack: [{
|
|
1064
|
-
command: increment,
|
|
1065
|
-
params: undefined,
|
|
1066
|
-
result: { previousCount: 4 },
|
|
1067
|
-
timestamp: Date.now(),
|
|
1068
|
-
}],
|
|
1069
|
-
redoStack: [],
|
|
1070
|
-
},
|
|
1071
|
-
}));
|
|
1072
|
-
|
|
1073
|
-
// Undo: 5 -> 4
|
|
1074
|
-
performUndo(storeApi);
|
|
1075
|
-
expect(store.getState().count).toBe(4);
|
|
1076
|
-
|
|
1077
|
-
// Redo: 4 -> 5
|
|
1078
|
-
performRedo(storeApi);
|
|
1079
|
-
expect(store.getState().count).toBe(5);
|
|
1080
|
-
});
|
|
1081
|
-
});
|
|
1082
|
-
|