diff --git a/src/app/app-settings.spec.ts b/src/app/app-settings.spec.ts index a647928..80dbcfe 100644 --- a/src/app/app-settings.spec.ts +++ b/src/app/app-settings.spec.ts @@ -62,6 +62,7 @@ describe('Application Settings', () => { afterEach(() => { fixture?.destroy(); vi.restoreAllMocks(); + vi.useRealTimers(); localStorage.clear(); document.body.classList.remove('light-mode', 'dark-mode'); }); @@ -105,6 +106,111 @@ describe('Application Settings', () => { await createApp(); } + it('keeps random starting balances unsaved through calculation and restores the first edited list', async () => { + vi.useFakeTimers({ toFake: ['Date'] }); + vi.setSystemTime(new Date(2026, 9, 4, 12)); + const random = vi + .spyOn(Math, 'random') + .mockReturnValueOnce(0) + .mockReturnValueOnce(0.5) + .mockReturnValue(1 - Number.EPSILON); + localStorage.removeItem('leaveBalances'); + await revisitApp(true); + + const root = fixture.nativeElement as HTMLElement; + expect( + Array.from(root.querySelectorAll('app-day-input input')).map( + (input) => input.value, + ), + ).toEqual([ + '10', + '04/10/2026', + '04/10/2027', + '15', + '04/10/2026', + '04/10/2027', + '20', + '04/10/2026', + '04/10/2027', + ]); + const types = await loader.getAllHarnesses( + MatSelectHarness.with({ selector: 'app-day-input mat-select' }), + ); + expect(await Promise.all(types.map((type) => type.getValueText()))).toEqual([ + 'Paid Leave', + 'RTT', + 'Other', + ]); + expect(calculate).toHaveBeenCalledOnce(); + const initialRequest = calculate.mock.calls[0][0]; + expect(initialRequest.balances.map((balance) => balance.quantity)).toEqual([10, 15, 20]); + expect(calculatePlanningInternal(initialRequest).kind).toBe('Plan'); + await expect.poll(() => root.querySelector('.planning-status')).not.toBeNull(); + expect(localStorage.getItem('leaveBalances')).toBeNull(); + + root.querySelector('.calculate-button')!.click(); + await fixture.whenStable(); + expect(calculate).toHaveBeenCalledTimes(2); + expect(calculate.mock.calls[1][0].balances).toEqual(initialRequest.balances); + await expect.poll(() => root.querySelector('.planning-status')).not.toBeNull(); + expect(localStorage.getItem('leaveBalances')).toBeNull(); + + const quantity = root.querySelector( + 'app-day-input input[type="number"]', + )!; + quantity.value = '7'; + quantity.dispatchEvent(new Event('input')); + await fixture.whenStable(); + const saved = localStorage.getItem('leaveBalances'); + expect(JSON.parse(saved!)).toMatchObject([ + { type: 'cp', numberOfDays: 7, availableDate: '2026-10-04', expiryDate: '2027-10-04' }, + { + type: 'rtt', + numberOfDays: 15, + availableDate: '2026-10-04', + expiryDate: '2027-10-04', + }, + { + type: 'other', + numberOfDays: 20, + availableDate: '2026-10-04', + expiryDate: '2027-10-04', + }, + ]); + + random.mockReturnValue(0); + await revisitApp(true); + expect( + Array.from( + fixture.nativeElement.querySelectorAll('app-day-input input[type="number"]'), + (input) => (input as HTMLInputElement).value, + ), + ).toEqual(['7', '15', '20']); + expect(calculate).toHaveBeenCalledOnce(); + expect(calculate.mock.calls[0][0].balances).toEqual( + initialRequest.balances.map((balance, index) => + index === 0 ? { ...balance, quantity: 7 } : balance, + ), + ); + expect(localStorage.getItem('leaveBalances')).toBe(saved); + }); + + it('calculates with a saved empty balance list without generating replacement rows', async () => { + localStorage.setItem('leaveBalances', '[]'); + const random = vi.spyOn(Math, 'random'); + await revisitApp(true); + + expect(fixture.nativeElement.querySelector('app-day-input')).toBeNull(); + expect(calculate).toHaveBeenCalledOnce(); + expect(calculate.mock.calls[0][0].balances).toEqual([]); + expect(calculatePlanningInternal(calculate.mock.calls[0][0]).kind).toBe('Plan'); + await expect + .poll(() => fixture.nativeElement.querySelector('.planning-status')) + .not.toBeNull(); + expect(random).not.toHaveBeenCalled(); + expect(localStorage.getItem('leaveBalances')).toBe('[]'); + }); + it('passes restored expired balances to the initial calculation without rewriting storage', () => { const root = fixture.nativeElement as HTMLElement; expect(