test(admin): import the untested SPA modules directly for check-phase10 hygiene
- mlLocale: type predicates, localeRecord, the ML_LOCALE_CHANGE broadcast - MLField: locale switch, whole-map emit, broadcast follow, content-locale - FileCaptionModal: prefill, save via FileRoutes.update, 422 stays, danger toast - ApiTokensPanel and ApiTokenCreateDialog mounted directly next to the app tests
This commit is contained in:
138
admin/tests/form/FileCaptionModal.test.ts
Normal file
138
admin/tests/form/FileCaptionModal.test.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
// FileCaptionModal (Phase 12.2, D-09; UI-SPEC section 3, caption modal)
|
||||
// mounted on its own over a stub FileRoutes, so no HTTP is involved. Save
|
||||
// sends title and description through routes.update; a 422 stays in the
|
||||
// dialog under its inputs; any other failure keeps the input and posts a
|
||||
// danger toast. The language bundle has no fileupload strings, so the test
|
||||
// asserts on data attributes and events rather than texts.
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import type { FileCallResult, FileRoutes } from '../../src/api/files'
|
||||
import type { FileItem } from '../../src/api/types'
|
||||
import FileCaptionModal from '../../src/components/form/fields/FileCaptionModal.vue'
|
||||
import { toasts } from '../../src/state/useToasts'
|
||||
import { resetState } from '../helpers'
|
||||
|
||||
const FILE: FileItem = {
|
||||
id: 41,
|
||||
file_name: 'front.png',
|
||||
file_size: 120400,
|
||||
content_type: 'image/png',
|
||||
title: 'Front',
|
||||
description: 'The front side',
|
||||
sort_order: 41,
|
||||
pending: false,
|
||||
created_at: '2026-10-02T10:00:00Z',
|
||||
}
|
||||
|
||||
function routesStub(update: FileRoutes['update']): FileRoutes {
|
||||
return {
|
||||
list: vi.fn(),
|
||||
upload: vi.fn(),
|
||||
update: vi.fn(update),
|
||||
remove: vi.fn(),
|
||||
reorder: vi.fn(),
|
||||
download: vi.fn(),
|
||||
thumb: vi.fn(),
|
||||
}
|
||||
}
|
||||
|
||||
function ok(item: FileItem): FileCallResult<FileItem> {
|
||||
return { data: item, status: 200, error: null }
|
||||
}
|
||||
|
||||
async function mountModal(routes: FileRoutes) {
|
||||
const wrapper = mount(FileCaptionModal, {
|
||||
props: { open: true, file: FILE, routes, preview: null, idPrefix: 'gadget' },
|
||||
attachTo: document.body,
|
||||
})
|
||||
await flushPromises()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
function el<T extends Element>(selector: string): T {
|
||||
const found = document.body.querySelector<T>(selector)
|
||||
if (!found) {
|
||||
throw new Error(`missing ${selector}`)
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
function type(selector: string, value: string): void {
|
||||
const input = el<HTMLInputElement | HTMLTextAreaElement>(selector)
|
||||
input.value = value
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
}
|
||||
|
||||
async function save(): Promise<void> {
|
||||
el<HTMLButtonElement>('[data-action="save-caption"]').click()
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
resetState()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
enableAutoUnmount(afterEach)
|
||||
|
||||
describe('FileCaptionModal mounted directly', () => {
|
||||
it('prefills title and description from the file', async () => {
|
||||
await mountModal(routesStub(async () => ok(FILE)))
|
||||
expect(document.body.querySelector('[data-caption-modal]')).not.toBeNull()
|
||||
expect(el<HTMLInputElement>('[data-caption-title]').value).toBe('Front')
|
||||
expect(el<HTMLTextAreaElement>('[data-caption-description]').value).toBe('The front side')
|
||||
expect(document.body.textContent).toContain(FILE.file_name)
|
||||
})
|
||||
|
||||
it('saves the edited caption through routes.update and closes', async () => {
|
||||
const saved: FileItem = { ...FILE, title: 'Front cover', description: 'Shown first' }
|
||||
const routes = routesStub(async () => ok(saved))
|
||||
const wrapper = await mountModal(routes)
|
||||
type('[data-caption-title]', 'Front cover')
|
||||
type('[data-caption-description]', 'Shown first')
|
||||
await save()
|
||||
|
||||
expect(routes.update).toHaveBeenCalledTimes(1)
|
||||
expect(routes.update).toHaveBeenCalledWith(FILE.id, { title: 'Front cover', description: 'Shown first' })
|
||||
expect(wrapper.emitted('saved')).toEqual([[saved]])
|
||||
expect(wrapper.emitted('update:open')).toEqual([[false]])
|
||||
expect(toasts.value[0]?.tone).toBe('success')
|
||||
})
|
||||
|
||||
it('keeps the dialog open on a 422 and shows the field message', async () => {
|
||||
const routes = routesStub(async () => ({
|
||||
data: null,
|
||||
status: 422,
|
||||
error: { code: 'validation_failed', message: 'Invalid', details: { title: ['Title is too long.'] } },
|
||||
}))
|
||||
const wrapper = await mountModal(routes)
|
||||
type('[data-caption-title]', 'x'.repeat(300))
|
||||
await save()
|
||||
|
||||
expect(wrapper.emitted('saved')).toBeUndefined()
|
||||
expect(wrapper.emitted('update:open')).toBeUndefined()
|
||||
expect(document.body.querySelector('[data-caption-modal]')).not.toBeNull()
|
||||
expect(el('#gadget-caption-title-error').textContent).toContain('Title is too long.')
|
||||
expect(el<HTMLInputElement>('[data-caption-title]').getAttribute('aria-invalid')).toBe('true')
|
||||
expect(toasts.value).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('posts a danger toast on any other failure and keeps the typed input', async () => {
|
||||
const routes = routesStub(async () => ({
|
||||
data: null,
|
||||
status: 500,
|
||||
error: { code: 'internal', message: 'Server exploded', details: {} },
|
||||
}))
|
||||
const wrapper = await mountModal(routes)
|
||||
type('[data-caption-title]', 'Kept title')
|
||||
await save()
|
||||
|
||||
expect(wrapper.emitted('saved')).toBeUndefined()
|
||||
expect(wrapper.emitted('update:open')).toBeUndefined()
|
||||
expect(toasts.value[0]).toMatchObject({ tone: 'danger', text: 'Server exploded' })
|
||||
expect(el<HTMLInputElement>('[data-caption-title]').value).toBe('Kept title')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user