feat: entry editor at /content/:key/new and /content/:key/:id
This commit is contained in:
1 parent
91d916fb74
commit
fba20f83c5
94 files changed
+7116
-423
No files matched your search
@@ -0,0 +1,136 @@
|
||||
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')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user