import { beforeEach, describe, expect, it, vi } from 'vitest' import { ApiError } from '@/composables/useApi' import { useEntryLoad } from '@/composables/useEntryLoad' import type { ContentEntry } from '@/lib/contentTypes' const getEntry = vi.fn() vi.mock('@/composables/useEntries', () => ({ useEntries: () => ({ getEntry }) })) function entry(id: number, title: string): ContentEntry { return { id, content_type: 'events', data: { title }, created_by: 1, updated_by: 1, created_at: '2026-10-12T09:14:00', updated_at: '2026-10-12T09:14:00', } } function gate() { let resolve!: (value: ContentEntry) => void const promise = new Promise((done) => (resolve = done)) return { promise, resolve } } beforeEach(() => { getEntry.mockReset() }) describe('initial status', () => { it.each([ ['new', 'idle'], ['invalid', 'missing'], [42, 'loading'], ] as const)('starts a %s target as %s', (target, status) => { expect(useEntryLoad('events', target).status.value).toBe(status) }) it('requests nothing for a new or invalid target', async () => { await useEntryLoad('events', 'new').start() await useEntryLoad('events', 'invalid').start() expect(getEntry).not.toHaveBeenCalled() }) }) describe('stale answers', () => { it('keeps the answer of the latest request', async () => { const first = gate() const second = gate() getEntry.mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise) const load = useEntryLoad('events', 1) const started = load.start() const retried = load.retry(1) second.resolve(entry(1, 'second')) await retried first.resolve(entry(1, 'first')) await started expect(load.entry.value?.data.title).toBe('second') expect(load.status.value).toBe('ready') }) it('drops a stale failure', async () => { let fail!: (error: unknown) => void const first = new Promise((_resolve, reject) => (fail = reject)) const second = gate() getEntry.mockReturnValueOnce(first).mockReturnValueOnce(second.promise) const load = useEntryLoad('events', 1) const started = load.start() const retried = load.retry(1) second.resolve(entry(1, 'second')) await retried fail(new ApiError(500)) await started expect(load.status.value).toBe('ready') expect(load.entry.value?.data.title).toBe('second') }) it('ignores an in-flight load once an entry is adopted', async () => { const pendingLoad = gate() getEntry.mockReturnValueOnce(pendingLoad.promise) const load = useEntryLoad('events', 1) const started = load.start() load.adopt(entry(1, 'saved')) pendingLoad.resolve(entry(1, 'loaded')) await started expect(load.entry.value?.data.title).toBe('saved') }) it('ignores an in-flight load once the entry is marked gone', async () => { const pendingLoad = gate() getEntry.mockReturnValueOnce(pendingLoad.promise) const load = useEntryLoad('events', 1) const started = load.start() load.markGone(new ApiError(404, undefined, 'entry_not_found')) pendingLoad.resolve(entry(1, 'loaded')) await started expect(load.status.value).toBe('missing') expect(load.entry.value).toBeNull() }) }) describe('failures', () => { it.each([ [403, undefined, 'forbidden'], [404, 'entry_not_found', 'missing'], [404, 'content_type_not_found', 'typeMissing'], [500, undefined, 'failed'], ])('maps %i %s to %s', async (status, body, expected) => { getEntry.mockRejectedValueOnce(new ApiError(status, undefined, body)) const load = useEntryLoad('events', 1) await load.start() expect(load.status.value).toBe(expected) }) it('leaves the status alone on 401', async () => { getEntry.mockRejectedValueOnce(new ApiError(401)) const load = useEntryLoad('events', 1) await load.start() expect(load.status.value).toBe('loading') }) })