import { describe, expect, it } from 'vitest' import { useRequestSyncState } from '../../composables/useRequestSyncState' describe('useRequestSyncState', () => { it('starts with idle request state', () => { const state = useRequestSyncState() expect(state.loading.value).toBe(false) expect(state.error.value).toBeNull() expect(state.lastLoadedAt.value).toBeNull() expect(state.lastSavedAt.value).toBeNull() expect(state.lastRunAt.value).toBeNull() }) it('normalizes errors and supports explicit clearing', () => { const state = useRequestSyncState('Default failure') expect(state.setError(new Error('Backend rejected request'))).toBe( 'Backend rejected request', ) expect(state.error.value).toBe('Backend rejected request') expect(state.setError('String failure')).toBe('String failure') expect(state.error.value).toBe('String failure') expect(state.setError({ message: 'Validation failed' })).toBe('Validation failed') expect(state.error.value).toBe('Validation failed') expect(state.setError({ reason: 'unknown' })).toBe('Default failure') state.clearError() expect(state.error.value).toBeNull() }) it('marks load, save, and run timestamps', () => { const state = useRequestSyncState() const loadedAt = new Date('2026-05-07T09:00:00Z') const savedAt = new Date('2026-05-07T09:01:00Z') const runAt = new Date('2026-05-07T09:02:00Z') state.markLoaded(loadedAt) state.markSaved(savedAt) state.markRun(runAt) expect(state.lastLoadedAt.value).toBe(loadedAt) expect(state.lastSavedAt.value).toBe(savedAt) expect(state.lastRunAt.value).toBe(runAt) }) it('wraps successful operations and marks requested sync timestamps', async () => { const state = useRequestSyncState() const result = await state.run(async () => { expect(state.loading.value).toBe(true) return { ok: true } }, { success: 'load' }) expect(result).toEqual({ ok: true }) expect(state.loading.value).toBe(false) expect(state.error.value).toBeNull() expect(state.lastLoadedAt.value).toBeInstanceOf(Date) expect(state.lastSavedAt.value).toBeNull() expect(state.lastRunAt.value).toBeNull() }) it('wraps successful run operations and marks lastRunAt', async () => { const state = useRequestSyncState() const result = await state.run(async () => 'done', { success: 'run' }) expect(result).toBe('done') expect(state.lastLoadedAt.value).toBeNull() expect(state.lastSavedAt.value).toBeNull() expect(state.lastRunAt.value).toBeInstanceOf(Date) }) it('wraps failed operations, sets error, and rethrows', async () => { const state = useRequestSyncState() const failure = new Error('Network down') await expect(state.run(async () => { expect(state.loading.value).toBe(true) throw failure }, { success: 'save' })).rejects.toThrow('Network down') expect(state.loading.value).toBe(false) expect(state.error.value).toBe('Network down') expect(state.lastLoadedAt.value).toBeNull() expect(state.lastSavedAt.value).toBeNull() expect(state.lastRunAt.value).toBeNull() }) describe('stale-response guard', () => { function deferred() { let resolve!: (value: T) => void let reject!: (reason: unknown) => void const promise = new Promise((res, rej) => { resolve = res reject = rej }) return { promise, resolve, reject } } it('lets only the newest run own loading', async () => { const state = useRequestSyncState() const first = deferred() const second = deferred() const firstRun = state.run(() => first.promise, { success: 'load' }) const secondRun = state.run(() => second.promise, { success: 'load' }) first.resolve('stale') await expect(firstRun).resolves.toBe('stale') expect(state.loading.value).toBe(true) second.resolve('fresh') await expect(secondRun).resolves.toBe('fresh') expect(state.loading.value).toBe(false) }) it('does not let a stale success move the sync timestamps', async () => { const state = useRequestSyncState() const first = deferred() const second = deferred() const firstRun = state.run(() => first.promise, { success: 'save' }) const secondRun = state.run(() => second.promise, { success: 'load' }) first.resolve('stale') await firstRun expect(state.lastSavedAt.value).toBeNull() second.resolve('fresh') await secondRun expect(state.lastLoadedAt.value).toBeInstanceOf(Date) expect(state.lastSavedAt.value).toBeNull() }) it('does not let a stale failure write the error, but still rejects its caller', async () => { const state = useRequestSyncState() const first = deferred() const second = deferred() const firstRun = state.run(() => first.promise, { errorMessage: 'first failed' }) const secondRun = state.run(() => second.promise, { success: 'load' }) first.reject(new Error('stale failure')) await expect(firstRun).rejects.toThrow('stale failure') expect(state.error.value).toBeNull() second.resolve('fresh') await secondRun expect(state.error.value).toBeNull() expect(state.loading.value).toBe(false) }) it('aborts the superseded run signal by default', async () => { const state = useRequestSyncState() const first = deferred() const second = deferred() let firstSignal: AbortSignal | undefined const firstRun = state.run((signal) => { firstSignal = signal return first.promise }) expect(firstSignal?.aborted).toBe(false) const secondRun = state.run(() => second.promise) expect(firstSignal?.aborted).toBe(true) first.resolve('stale') second.resolve('fresh') await Promise.all([firstRun, secondRun]) }) it('leaves the previous signal alone when abortPrevious is false', async () => { const state = useRequestSyncState() const first = deferred() const second = deferred() let firstSignal: AbortSignal | undefined const firstRun = state.run((signal) => { firstSignal = signal return first.promise }) const secondRun = state.run(() => second.promise, { abortPrevious: false }) expect(firstSignal?.aborted).toBe(false) first.resolve('stale') second.resolve('fresh') await Promise.all([firstRun, secondRun]) }) it('forwards a caller signal onto the operation signal', async () => { const state = useRequestSyncState() const caller = new AbortController() const pending = deferred() let operationSignal: AbortSignal | undefined const run = state.run((signal) => { operationSignal = signal return pending.promise }, { signal: caller.signal }) caller.abort() expect(operationSignal?.aborted).toBe(true) pending.resolve('done') await run }) it('cancel() aborts the in-flight run and drops loading', async () => { const state = useRequestSyncState() const pending = deferred() let operationSignal: AbortSignal | undefined const run = state.run((signal) => { operationSignal = signal return pending.promise }, { success: 'load' }) state.cancel() expect(operationSignal?.aborted).toBe(true) expect(state.loading.value).toBe(false) pending.resolve('too late') await expect(run).resolves.toBe('too late') expect(state.lastLoadedAt.value).toBeNull() }) }) })