Files
RenewCMS/web/src/composables/__tests__/useEntryLoad.spec.ts
T

137 lines
3.7 KiB
TypeScript

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<ContentEntry>((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<ContentEntry>((_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')
})
})