feat(SPA): media field type

This commit is contained in:
Florian Sylvain committed 2026-10-21 15:17:15 +02:00
1 parent a2d596f4ca
commit 3e7fc9a4a5
57 files changed
+6109 -75

No files matched your search

@@ -0,0 +1,159 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { flushPromises } from '@vue/test-utils'
import { MOBILE_WIDTH } from '@/components/__tests__/viewport'
import {
button,
calls,
chip,
failWith,
fieldWrapper,
hasText,
text,
unmountAll,
} from './entryEditorKit'
import {
focusedName,
libraryBody,
mediaButton,
mediaState,
mediaText,
mountMedia,
pickFromLibrary,
} from './entryMediaKit'
import { fieldLibrary } from './mediaFieldKit'
vi.mock('@/composables/useApi', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/composables/useApi')>()
return { ...actual, useApi: vi.fn() }
})
afterEach(unmountAll)
const dialog = () => document.body.querySelector<HTMLElement>('[role="dialog"]')
const tiles = () => [...document.body.querySelectorAll<HTMLButtonElement>('[data-picker-tile]')]
async function open(opener = 'Change image') {
mediaButton(opener)!.click()
await flushPromises()
await flushPromises()
}
describe('The picker opened from the entry editor', () => {
it('loads only the library page and never the variants', async () => {
await mountMedia('/content/events/42', { thumbnail: 7 })
await open()
expect(calls.filter((url) => url.startsWith('/v1/media/'))).toEqual([
'/v1/media/?page=1&size=25',
])
expect(calls.some((url) => url.includes('image-variants'))).toBe(false)
expect(text(dialog())).toContain('Choose an image')
expect(text(dialog())).not.toContain('Alt text')
})
it('marks the stored image as Current', async () => {
await mountMedia('/content/events/42', { thumbnail: 7 })
await open()
const current = tiles().filter((tile) =>
tile.getAttribute('aria-label')!.endsWith(', Current'),
)
expect(current.map((tile) => tile.dataset.imageId)).toEqual(['7'])
})
it('is 640 px wide on desktop and a bottom sheet on mobile', async () => {
await mountMedia('/content/events/42', { thumbnail: 7 })
await open()
expect(dialog()!.className).toContain('sm:max-w-160')
unmountAll()
await mountMedia('/content/events/42', { thumbnail: 7, width: MOBILE_WIDTH })
await open()
expect(dialog()!.className).toContain('max-sm:!bottom-0')
})
it('does not open while the form is saving', async () => {
let release = () => {}
await mountMedia('/content/events/42', {
thumbnail: 7,
writeEntry: () => new Promise((resolve) => (release = () => resolve(undefined))),
})
const title = fieldWrapper('title').querySelector('input')!
title.value = 'Changed'
title.dispatchEvent(new Event('input', { bubbles: true }))
await flushPromises()
button('Save')!.click()
await flushPromises()
mediaButton('Change image')!.click()
await flushPromises()
expect(dialog()).toBeNull()
expect(hasText('Saving…')).toBe(true)
release()
await flushPromises()
})
it('shows the empty library with the field copy and focuses nothing else', async () => {
await mountMedia('/content/events/new', {
library: () => ({ ...libraryBody([]), total: 0 }),
})
await open('Pick an image for Thumbnail')
const library = [...document.body.querySelectorAll<HTMLElement>('[role="tab"]')].find(
(tab) => text(tab) === 'Library',
)!
library.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, button: 0 }))
await flushPromises()
expect(text(dialog())).toContain(
'Images you upload appear here, ready to use in any article or entry.',
)
})
it('shows the error alert when the library fails and keeps the entry untouched', async () => {
await mountMedia('/content/events/42', { thumbnail: 7, library: failWith(500) })
await open()
expect(text(dialog())).toContain("Couldn't load your library")
expect(text(dialog())).toContain('Try again')
expect(chip()!.textContent).toContain('Saved')
})
})
describe('Focus and announcements in the editor', () => {
it('lands on Change image after Use this image', async () => {
await mountMedia('/content/events/42', { thumbnail: 7 })
await pickFromLibrary('bathroom-tiles.png')
expect(focusedName()).toBe('Change image')
expect(mediaState()).toBe('filled')
expect(mediaText()).toContain('bathroom-tiles.png')
})
it('lands on Pick an image after removing the image', async () => {
await mountMedia('/content/events/42', { thumbnail: 7 })
mediaButton('Remove image from Thumbnail')!.click()
await flushPromises()
await flushPromises()
expect(focusedName()).toBe('Pick an image for Thumbnail')
expect(chip()!.textContent).toContain('Unsaved changes')
})
it('returns to the opener with Cancel', async () => {
await mountMedia('/content/events/42', { thumbnail: 7 })
const opener = mediaButton('Change image')!
opener.focus()
await open()
;[...dialog()!.querySelectorAll('button')].find((b) => text(b) === 'Cancel')!.click()
await vi.waitFor(() => expect(document.activeElement).toBe(opener))
expect(fieldLibrary()).toHaveLength(12)
})
})