this repo has no description
at react-mobile 592 lines 25 kB view raw
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});