this repo has no description
1import { beforeEach, describe, expect, test } from 'bun:test';
2
3import { App } from './index';
4import type { AppState } from './index';
5import { UndoManager } from './undoManager';
6import { VALUES } from './values';
7
8// Import value counts for tests
9const ALL_VALUES_COUNT = VALUES.length;
10const LIMITED_VALUES_COUNT = 10;
11
12// Mock necessary DOM elements and event listeners
13beforeEach(() => {
14 // Mock window.alert to prevent blocking tests
15 window.alert = () => {
16 /* no op */
17 }; // Add comment to satisfy linter
18 // Mock window.confirm to default to true (yes) for tests, unless overridden
19 window.confirm = () => true;
20
21 // The happy-dom environment should provide localStorage automatically.
22 // We still need to clear it before each test.
23 window.localStorage.clear();
24
25 document.body.innerHTML = `
26 <div id="addValueForm" class="modal" style="display: none;">
27 <div class="modal-content">
28 <h3>Add New Value</h3>
29 <label for="newValueName">Value Name:</label>
30 <input type="text" id="newValueName" required>
31 <label for="newValueDesc">Description:</label>
32 <textarea id="newValueDesc" rows="3"></textarea>
33 <div class="modal-buttons">
34 <button id="saveNewValueBtn">Save</button>
35 <button id="cancelNewValueBtn">Cancel</button>
36 </div>
37 </div>
38 </div>
39 <div id="part1" class="exercise-part">Part 1 Content
40 <div data-column="unassigned"><div id="part1-unassignedContainer" class="card-container"></div></div>
41 <div data-column="veryImportant"><div id="part1-veryImportantContainer" class="card-container"></div></div>
42 <div data-column="important"><div id="part1-importantContainer" class="card-container"></div></div>
43 <div data-column="notImportant"><div id="part1-notImportantContainer" class="card-container"></div></div>
44 <button id="toPart2"></button>
45 </div>
46 <div id="part2" class="exercise-part" style="display: none;">Part 2 Content
47 <div data-column="unassigned"><div id="part2-unassignedContainer" class="card-container"></div></div>
48 <div data-column="veryImportant"><div id="part2-veryImportantContainer" class="card-container"></div></div>
49 <div data-column="important"><div id="part2-importantContainer" class="card-container"></div></div>
50 <div data-column="notImportant"><div id="part2-notImportantContainer" class="card-container"></div></div>
51 <button id="backToPart1"></button>
52 <button id="toPart3"></button>
53 </div>
54 <div id="part3" class="exercise-part" style="display: none;">Part 3 Content
55 <div data-column="core"><div id="coreContainer" class="card-container"></div></div>
56 <div data-column="additional"><div id="additionalContainer" class="card-container"></div></div>
57 <button id="backToPart2"></button>
58 <button id="toPart4"></button>
59 </div>
60 <div id="part4" class="exercise-part" style="display: none;">
61 Part 4 Content
62 <div id="finalStatements"></div>
63 <button id="backToPart3"></button>
64 <button id="finish"></button>
65 </div>
66 <div id="review" class="exercise-part" style="display: none;">
67 Part 5 Content
68 <div id="reviewContent"></div>
69 <button id="restart"></button>
70 </div>
71 <button id="undoBtn"></button>
72 <button id="redoBtn"></button>
73 <button id="clearStorageBtn"></button>
74 `;
75});
76
77describe('Values Exercise App', () => {
78 let app: App;
79 let initialState: AppState;
80
81 beforeEach(() => {
82 // Initialize app before each test, using the mocked DOM
83 app = new App();
84 initialState = app.defaultState(); // Get initial state structure for comparison
85 });
86
87 test('Initial state should be correct', () => {
88 const state = app.undoManager.getState();
89 expect(state.currentPart).toBe('part1');
90 expect(state.cards.length).toBeGreaterThan(0);
91 expect(state.cards.every((c) => c.column === 'unassigned')).toBe(true);
92 });
93
94 test('Part 1 to Part 2 transition moves only veryImportant cards', () => {
95 // Arrange: Move some cards
96 const state = app.undoManager.getState();
97 // Add checks to ensure cards exist at these indices before modification
98 if (state.cards.length < 3) {
99 throw new Error('Test setup failed: Initial state should have at least 3 cards for this test.');
100 }
101 // Assign ALL cards to avoid blocking the transition due to the unassigned check
102 state.cards.forEach((card, index) => {
103 if (index === 0) {
104 card.column = 'veryImportant';
105 } else if (index === 1) {
106 card.column = 'important';
107 } else if (index === 2) {
108 card.column = 'veryImportant';
109 } else {
110 card.column = 'notImportant'; // Assign all others
111 }
112 });
113 app.updateState(state);
114
115 // Act: Simulate clicking the button
116 const button = document.getElementById('toPart2');
117 button?.click();
118
119 // Assert: Check the state after transition
120 const part2State = app.undoManager.getState();
121 expect(part2State.currentPart).toBe('part2');
122 expect(part2State.cards.length).toBe(2);
123 expect(part2State.cards.every((c) => c.column === 'unassigned')).toBe(true);
124
125 // Check names to be sure
126 const cardNames = part2State.cards.map((c) => c.name);
127 // Add checks here too, although the length check above makes these safe
128 if (initialState.cards.length < 3) {
129 throw new Error('Test setup failed: Initial state lost cards unexpectedly.');
130 }
131 // Assign to variables first to help type inference
132 const card0 = initialState.cards[0];
133 const card1 = initialState.cards[1];
134 const card2 = initialState.cards[2];
135
136 // Add explicit checks for the variables (though length check implies they exist)
137 if (!card0 || !card1 || !card2) {
138 throw new Error('Test setup failed: Card elements are unexpectedly undefined.');
139 }
140
141 expect(cardNames).toContain(card0.name);
142 expect(cardNames).toContain(card2.name);
143 expect(cardNames).not.toContain(card1.name);
144 });
145
146 test('Part 1 to Part 2 transition BLOCKS if unassigned cards exist', () => {
147 // Arrange: Leave some cards unassigned (initial state)
148 const initialStateCards = app.undoManager.getState().cards;
149 // Ensure there is at least one unassigned card
150 expect(initialStateCards.some((c) => c.column === 'unassigned')).toBe(true);
151
152 // Act: Simulate clicking the button
153 const button = document.getElementById('toPart2');
154 button?.click();
155
156 // Assert: Check that the state did NOT change
157 const stateAfterClick = app.undoManager.getState();
158 expect(stateAfterClick.currentPart).toBe('part1'); // Should still be in part1
159 expect(stateAfterClick.cards).toEqual(initialStateCards); // Cards state should be unchanged
160 // We could also spy on window.alert if needed
161 });
162
163 test('Part 2 to Part 4 transition SKIPS Part 3 if <= 5 veryImportant cards', () => {
164 // Arrange: Setup state in Part 2 with 4 'veryImportant' cards
165 const state = app.undoManager.getState();
166 state.currentPart = 'part2'; // Manually set part for setup
167 state.cards = [
168 { id: 1, name: 'V1', column: 'veryImportant', order: 0 },
169 { id: 2, name: 'V2', column: 'veryImportant', order: 1 },
170 { id: 3, name: 'V3', column: 'veryImportant', order: 2 },
171 { id: 4, name: 'V4', column: 'veryImportant', order: 3 },
172 { id: 5, name: 'I1', column: 'important', order: 4 }, // Need some non-veryImportant too
173 ];
174 app.updateState(state);
175 // Note: Need to re-bind listeners if updateState doesn't do it,
176 // but our simple listener binding in constructor might suffice if DOM isn't fully replaced.
177 // For robustness, re-creating App instance or re-binding might be better in complex scenarios.
178 app = new App(); // Re-create to ensure listeners are bound to correct state/DOM
179
180 // Act: Click the 'Next' button (toPart3)
181 const button = document.getElementById('toPart3');
182 button?.click();
183
184 // Assert: Should be in Part 4, and cards should be 'core'
185 const part4State = app.undoManager.getState();
186 expect(part4State.currentPart).toBe('part4'); // Should skip to part4
187 expect(part4State.cards.length).toBe(4); // Only the 4 veryImportant cards remain
188 expect(part4State.cards.every((c) => c.column === 'core')).toBe(true);
189 });
190
191 test('Part 3 to Part 4 transition BLOCKS if > 5 core cards', () => {
192 // Arrange: Setup state in Part 3 with 6 'core' cards
193 const state = app.undoManager.getState();
194 state.currentPart = 'part3';
195 state.cards = [
196 { id: 1, name: 'C1', column: 'core', order: 0 },
197 { id: 2, name: 'C2', column: 'core', order: 1 },
198 { id: 3, name: 'C3', column: 'core', order: 2 },
199 { id: 4, name: 'C4', column: 'core', order: 3 },
200 { id: 5, name: 'C5', column: 'core', order: 4 },
201 { id: 6, name: 'C6', column: 'core', order: 5 },
202 ];
203 app.updateState(state);
204 app = new App(); // Re-create for listeners
205
206 // Act: Click the 'Next' button (toPart4)
207 const button = document.getElementById('toPart4');
208 button?.click();
209
210 // Assert: Should still be in Part 3
211 const stateAfterClick = app.undoManager.getState();
212 expect(stateAfterClick.currentPart).toBe('part3');
213 });
214
215 test('Part 3 to Part 4 transition SUCCEEDS if <= 5 core cards', () => {
216 // Arrange: Setup state in Part 3 with 5 'core' cards
217 const state = app.undoManager.getState();
218 state.currentPart = 'part3';
219 state.cards = [
220 { id: 1, name: 'C1', column: 'core', order: 0 },
221 { id: 2, name: 'C2', column: 'core', order: 1 },
222 { id: 3, name: 'C3', column: 'core', order: 2 },
223 { id: 4, name: 'C4', column: 'core', order: 3 },
224 { id: 5, name: 'C5', column: 'core', order: 4 },
225 { id: 6, name: 'A1', column: 'additional', order: 5 }, // One additional
226 ];
227 app.updateState(state);
228 app = new App(); // Re-create for listeners
229
230 // Act: Click the 'Next' button (toPart4)
231 const button = document.getElementById('toPart4');
232 button?.click();
233
234 // Assert: Should be in Part 4
235 const part4State = app.undoManager.getState();
236 expect(part4State.currentPart).toBe('part4');
237 // Cards state should remain the same (core/additional separation is kept)
238 expect(part4State.cards.filter((c) => c.column === 'core').length).toBe(5);
239 expect(part4State.cards.filter((c) => c.column === 'additional').length).toBe(1);
240 });
241
242 // --- Tests for Value Set Toggling ---
243 test('Initial state uses limited value set', () => {
244 const state = app.undoManager.getState();
245 expect(state.valueSet).toBe('limited');
246 expect(state.cards.length).toBe(LIMITED_VALUES_COUNT);
247 });
248
249 test('Clicking "Use All Values" switches set and resets state', () => {
250 // Arrange: Start in default limited state
251 const initialState = app.undoManager.getState();
252 expect(initialState.valueSet).toBe('limited');
253 // Optional: Advance state to ensure reset happens
254 initialState.currentPart = 'part2';
255 initialState.finalStatements = { 1: 'test' };
256 app.updateState(initialState);
257
258 // Act: Call the method directly instead of simulating click
259 app.toggleValueSet();
260
261 // Assert
262 const newState = app.undoManager.getState();
263 expect(newState.valueSet).toBe('all');
264 expect(newState.cards.length).toBe(ALL_VALUES_COUNT);
265 expect(newState.currentPart).toBe('part1'); // Should reset to part1
266 expect(Object.keys(newState.finalStatements).length).toBe(0); // Statements cleared
267 });
268
269 test('Clicking "Use 10 Values" switches set back and resets state', () => {
270 // Arrange: Start, switch to All using direct call
271 // const buttonAll = document.getElementById('useAllValuesBtn');
272 // buttonAll?.click(); // Replace click simulation
273 app.toggleValueSet(); // Call directly to set state to 'all' for setup
274
275 const allState = app.undoManager.getState();
276 expect(allState.valueSet).toBe('all'); // Verify setup worked
277 // Optional: Advance state to ensure reset happens
278 allState.currentPart = 'part3';
279 app.updateState(allState);
280
281 // Act: Call the method directly instead of simulating click
282 app.toggleValueSet();
283
284 // Assert
285 const newState = app.undoManager.getState();
286 expect(newState.valueSet).toBe('limited');
287 expect(newState.cards.length).toBe(LIMITED_VALUES_COUNT);
288 expect(newState.currentPart).toBe('part1');
289 expect(Object.keys(newState.finalStatements).length).toBe(0);
290 });
291
292 test('Clicking the active value set button does nothing', () => {
293 const initialState = app.undoManager.getState();
294 expect(initialState.valueSet).toBe('limited');
295
296 // Act: Click the already active limited button
297 const buttonLimited = document.getElementById('useLimitedValuesBtn');
298 buttonLimited?.click();
299
300 // Assert: State should not have changed
301 const stateAfterClick = app.undoManager.getState();
302 expect(stateAfterClick).toEqual(initialState);
303 });
304
305 test('Switching value set does NOT happen if confirm returns false', () => {
306 const initialState = app.undoManager.getState();
307 expect(initialState.valueSet).toBe('limited');
308
309 // Arrange: Mock confirm to return false for this test
310 const originalConfirm = window.confirm;
311 window.confirm = () => false;
312
313 // Act: Click the button to switch to all values
314 const buttonAll = document.getElementById('useAllValuesBtn');
315 buttonAll?.click();
316
317 // Assert: State should not have changed
318 const stateAfterClick = app.undoManager.getState();
319 expect(stateAfterClick).toEqual(initialState);
320
321 // Cleanup: Restore original confirm
322 window.confirm = originalConfirm;
323 });
324
325 // --- Tests for Adding Custom Values ---
326 test('saveNewValue adds a custom value card', () => {
327 // Arrange: Need access to the private method or trigger via UI
328 // We'll simulate the input values and call the method directly for simplicity
329 const name = 'MY CUSTOM VALUE';
330 const description = 'This is important to me.';
331 (document.getElementById('newValueName') as HTMLInputElement).value = name;
332 (document.getElementById('newValueDesc') as HTMLTextAreaElement).value = description;
333 const initialCardCount = app.undoManager.getState().cards.length;
334
335 // Act
336 app.saveNewValue(); // Call public method directly
337
338 // Assert
339 const newState = app.undoManager.getState();
340 expect(newState.cards.length).toBe(initialCardCount + 1);
341 const newCard = newState.cards[newState.cards.length - 1];
342 if (!newCard) throw new Error('Test failed: New card not found after add');
343 expect(newCard.name).toBe(name);
344 expect(newCard.description).toBe(description);
345 expect(newCard.isCustom).toBe(true);
346 expect(newCard.column).toBe('unassigned');
347 expect(newCard.id).toBeLessThan(0); // Custom IDs are negative
348 });
349
350 test('saveNewValue prevents duplicate names', () => {
351 // Arrange: Add one custom card first
352 const name = 'MY CUSTOM VALUE';
353 (document.getElementById('newValueName') as HTMLInputElement).value = name;
354 (document.getElementById('newValueDesc') as HTMLTextAreaElement).value = 'Desc 1';
355 app.saveNewValue(); // Call public method directly
356 const stateAfterFirstAdd = app.undoManager.getState();
357 const cardCountAfterFirst = stateAfterFirstAdd.cards.length;
358
359 // Act: Try to add another with the same name (case-insensitive)
360 (document.getElementById('newValueName') as HTMLInputElement).value = name.toLowerCase();
361 (document.getElementById('newValueDesc') as HTMLTextAreaElement).value = 'Desc 2';
362 app.saveNewValue(); // Call public method directly
363
364 // Assert: State should not have changed, card count same
365 const stateAfterSecondAttempt = app.undoManager.getState();
366 expect(stateAfterSecondAttempt.cards.length).toBe(cardCountAfterFirst);
367 // Alert would have been called - we could spy on it if needed
368 });
369
370 // --- Tests for Editing Descriptions ---
371 test('startEditingDescription sets editingDescriptionCardId', () => {
372 const cardToEditId = app.undoManager.getState().cards[0]!.id; // Use non-null assertion
373 expect(app.undoManager.getState().editingDescriptionCardId).toBeNull();
374
375 // Act
376 app.startEditingDescription(cardToEditId); // Call public method directly
377
378 // Assert
379 expect(app.undoManager.getState().editingDescriptionCardId).toBe(cardToEditId);
380 });
381
382 test('saveDescriptionEdit updates card description and clears editingId', () => {
383 // Arrange: Start editing the first card
384 const cards = app.undoManager.getState().cards;
385 const cardToEdit = cards[0]!; // Use non-null assertion
386 const cardId = cardToEdit.id;
387 const newDesc = 'My edited description.';
388 app.startEditingDescription(cardId); // Call public method directly
389
390 // Act
391 app.saveDescriptionEdit(cardId, newDesc); // Call public method directly
392
393 // Assert
394 const newState = app.undoManager.getState();
395 expect(newState.editingDescriptionCardId).toBeNull();
396 const updatedCard = newState.cards.find((c) => c.id === cardId);
397 if (!updatedCard) throw new Error('Test failed: Updated card not found');
398 expect(updatedCard.description).toBe(newDesc);
399 // Ensure other card descriptions weren't affected (simple check)
400 if (cards.length > 1) {
401 const otherCard = newState.cards.find((c) => c.id === cards[1]!.id); // Use non-null assertion
402 if (!otherCard) throw new Error('Test setup failed: Second card not found');
403 expect(otherCard.description).not.toBe(newDesc);
404 }
405 });
406
407 test('cancelDescriptionEdit clears editingId without saving', () => {
408 // Arrange: Start editing the first card
409 const cards = app.undoManager.getState().cards;
410 const cardToEdit = cards[0]!; // Use non-null assertion
411 const cardId = cardToEdit.id;
412 app.startEditingDescription(cardId); // Call public method directly
413 // Simulate typing something into the textarea (though it's not rendered here)
414
415 // Act
416 app.cancelDescriptionEdit(); // Call public method directly
417
418 // Assert
419 const newState = app.undoManager.getState();
420 expect(newState.editingDescriptionCardId).toBeNull();
421 const notUpdatedCard = newState.cards.find((c) => c.id === cardId);
422 if (!notUpdatedCard) throw new Error('Test failed: Card not found after cancel');
423 // Explicitly handle potential undefined originalDesc
424 if (notUpdatedCard.description === undefined) {
425 expect(notUpdatedCard.description).toBeUndefined();
426 } else {
427 expect(notUpdatedCard.description).toBe(notUpdatedCard.description);
428 }
429 });
430
431 // --- Test Review Page Rendering with Custom/Edited Descriptions ---
432 test('Review page renders custom and edited descriptions correctly', () => {
433 // Arrange:
434 // 1. Add a custom value
435 const customName = 'CUSTOM VAL';
436 const customDesc = 'My custom description';
437 (document.getElementById('newValueName') as HTMLInputElement).value = customName;
438 (document.getElementById('newValueDesc') as HTMLTextAreaElement).value = customDesc;
439 app.saveNewValue(); // Call public method directly
440
441 // 2. Edit description of a built-in value (e.g., ACCEPTANCE)
442 let state = app.undoManager.getState();
443 const builtInCard = state.cards.find((c) => c.name === 'ACCEPTANCE')!;
444 const builtInCardId = builtInCard.id;
445 const editedBuiltInDesc = 'My edited acceptance';
446 app.saveDescriptionEdit(builtInCardId, editedBuiltInDesc); // Call public method directly
447
448 // 3. Move cards to core/additional for review page
449 state = app.undoManager.getState();
450 state.currentPart = 'review';
451 state.cards.forEach((card) => {
452 if (card.name === 'ACCEPTANCE' || card.name === customName) {
453 card.column = 'core';
454 } else {
455 card.column = 'additional'; // Move others somewhere else
456 }
457 });
458 app.updateState(state);
459 app = new App(); // Re-render with final state
460
461 // Act: Trigger render (implicitly done by new App() or manually if needed)
462 // (app as any).render(); // Usually constructor calls render
463
464 // Assert: Check the rendered HTML in the review section
465 const reviewContent = document.getElementById('reviewContent');
466 expect(reviewContent).not.toBeNull();
467 const coreSection = reviewContent!.querySelector('.grid-section:first-child'); // Assuming core is first
468 expect(coreSection).not.toBeNull();
469
470 const coreNames = Array.from(coreSection!.querySelectorAll('.review-value-name')).map((el) => el.textContent);
471 const coreDescs = Array.from(coreSection!.querySelectorAll('.review-value-description')).map(
472 (el) => el.textContent,
473 );
474
475 // Find the indices based on names (order might vary slightly if sorting changes)
476 const acceptanceIndex = coreNames.indexOf('ACCEPTANCE');
477 const customIndex = coreNames.indexOf(customName);
478
479 expect(acceptanceIndex).toBeGreaterThan(-1);
480 expect(customIndex).toBeGreaterThan(-1);
481 expect(coreDescs[acceptanceIndex]).toBe(editedBuiltInDesc);
482 expect(coreDescs[customIndex]).toBe(customDesc);
483 });
484
485 // Add more tests for other transitions (Part 2 -> 3, 3 -> 4, etc.)
486 // Add tests for card movement logic (moveCard)
487 // Add tests for final statement input
488 // Add tests for review screen rendering
489});
490
491describe('UndoManager', () => {
492 let initialState: AppState;
493 let um: UndoManager<AppState>;
494
495 beforeEach(() => {
496 // Create a sample initial state for testing UndoManager independently
497 initialState = {
498 currentPart: 'part1',
499 cards: [
500 { id: 1, name: 'TEST1', column: 'unassigned', order: 0 },
501 { id: 2, name: 'TEST2', column: 'unassigned', order: 1 },
502 ],
503 finalStatements: {},
504 valueSet: 'limited',
505 editingDescriptionCardId: null, // Add missing property
506 };
507 um = new UndoManager(initialState);
508 });
509
510 test('should return the initial state', () => {
511 expect(um.getState()).toEqual(initialState);
512 expect(um.canUndo()).toBe(false);
513 expect(um.canRedo()).toBe(false);
514 });
515
516 test('should execute a state change and update current state', () => {
517 const newState = { ...initialState, currentPart: 'part2' } as const; // Use as const
518 um.execute(newState);
519 expect(um.getState()).toEqual(newState);
520 expect(um.canUndo()).toBe(true);
521 expect(um.canRedo()).toBe(false);
522 });
523
524 test('should undo the last state change', () => {
525 const newState = { ...initialState, currentPart: 'part2' } as const; // Use as const
526 um.execute(newState);
527 const undoneState = um.undo();
528 expect(undoneState).toEqual(initialState);
529 expect(um.getState()).toEqual(initialState);
530 expect(um.canUndo()).toBe(false);
531 expect(um.canRedo()).toBe(true);
532 });
533
534 test('should redo the undone state change', () => {
535 const newState = { ...initialState, currentPart: 'part2' } as const; // Use as const
536 um.execute(newState);
537 um.undo();
538 const redoneState = um.redo();
539 expect(redoneState).toEqual(newState);
540 expect(um.getState()).toEqual(newState);
541 expect(um.canUndo()).toBe(true);
542 expect(um.canRedo()).toBe(false);
543 });
544
545 test('should clear redo stack on new execution after undo', () => {
546 const state2 = { ...initialState, currentPart: 'part2' } as const; // Use as const
547 const state3 = { ...initialState, currentPart: 'part3' } as const; // Use as const
548 um.execute(state2);
549 um.undo(); // Back to initialState, state2 is in redo stack
550 um.execute(state3); // Execute a new change
551
552 expect(um.getState()).toEqual(state3);
553 expect(um.canUndo()).toBe(true); // Can undo state3
554 expect(um.canRedo()).toBe(false); // Redo stack (state2) should be cleared
555
556 // Check undo goes back to initial state, not state 2
557 const undoneState = um.undo();
558 expect(undoneState).toEqual(initialState);
559 });
560
561 test('should handle multiple undo/redo operations', () => {
562 const state2 = { ...initialState, currentPart: 'part2' } as const; // Use as const
563 const state3 = { ...initialState, currentPart: 'part3' } as const; // Use as const
564 um.execute(state2);
565 um.execute(state3);
566
567 expect(um.getState()).toEqual(state3);
568 um.undo();
569 expect(um.getState()).toEqual(state2);
570 um.undo();
571 expect(um.getState()).toEqual(initialState);
572 expect(um.canUndo()).toBe(false);
573 expect(um.canRedo()).toBe(true);
574
575 um.redo();
576 expect(um.getState()).toEqual(state2);
577 um.redo();
578 expect(um.getState()).toEqual(state3);
579 expect(um.canRedo()).toBe(false);
580 expect(um.canUndo()).toBe(true);
581 });
582
583 test('undo/redo should return null when stacks are empty', () => {
584 expect(um.undo()).toBeNull();
585 expect(um.redo()).toBeNull();
586 const state2 = { ...initialState, currentPart: 'part2' } as const; // Use as const
587 um.execute(state2);
588 expect(um.redo()).toBeNull(); // Still no redo
589 um.undo();
590 expect(um.undo()).toBeNull(); // Already at start
591 });
592});