feat(media): align visual checks and variant UI with merged design
This commit is contained in:
1 parent
e6746f2be7
commit
160c0a49ce
23 files changed
+606
-99
No files matched your search
@@ -760,7 +760,7 @@ describe('Copy URL, Purge and Preview', () => {
|
||||
const preview = Array.from(
|
||||
document.body.querySelectorAll<HTMLElement>('[role="dialog"]'),
|
||||
).find((entry) => textOf(entry).includes('Preview: thumb'))!
|
||||
expect(preview.className).toContain('max-sm:!bottom-4')
|
||||
expect(preview.className).toContain('max-sm:!bottom-0')
|
||||
expect(findButton('Close', preview)!.className).toContain('h-11')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { flushPromises } from '@vue/test-utils'
|
||||
import { useApi } from '@/composables/useApi'
|
||||
import { initializeLocale } from '@/i18n'
|
||||
import {
|
||||
CREATE,
|
||||
alertDialog,
|
||||
buttonByLabel,
|
||||
buttonIn,
|
||||
click,
|
||||
clickBackdrop,
|
||||
deferred,
|
||||
field,
|
||||
mountPage,
|
||||
openAddForm,
|
||||
openEditForm,
|
||||
pressEscape,
|
||||
rows,
|
||||
slideover,
|
||||
stubBrowser,
|
||||
textOf,
|
||||
typeInto,
|
||||
unmountPage,
|
||||
variant,
|
||||
} from './variantsTestKit'
|
||||
|
||||
vi.mock('@/composables/useApi', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/composables/useApi')>()
|
||||
return { ...actual, useApi: vi.fn() }
|
||||
})
|
||||
|
||||
const toasts = vi.hoisted(() => ({ add: vi.fn() }))
|
||||
|
||||
vi.mock('@nuxt/ui/composables', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@nuxt/ui/composables')>()
|
||||
return { ...actual, useToast: () => toasts }
|
||||
})
|
||||
|
||||
function sheet() {
|
||||
return slideover()!
|
||||
}
|
||||
|
||||
function addButton() {
|
||||
return document.body.querySelector<HTMLElement>('[data-testid="add-variant"]')!
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
toasts.add.mockClear()
|
||||
localStorage.clear()
|
||||
initializeLocale()
|
||||
stubBrowser()
|
||||
vi.mocked(useApi).mockReset()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
vi.unstubAllGlobals()
|
||||
await unmountPage()
|
||||
localStorage.clear()
|
||||
initializeLocale()
|
||||
})
|
||||
|
||||
describe('Rule: The variant form follows the overlay pattern', () => {
|
||||
it('Add variant is a right slide-over on desktop', async () => {
|
||||
await mountPage()
|
||||
|
||||
await openAddForm()
|
||||
|
||||
const panel = sheet()
|
||||
expect(panel.getAttribute('data-side')).toBe('right')
|
||||
expect(panel.className).toContain('sm:max-w-240')
|
||||
expect(panel.className).not.toContain('h-23/25')
|
||||
expect(buttonByLabel('Close')).not.toBeNull()
|
||||
expect(buttonByLabel('Back')).toBeNull()
|
||||
expect(panel.querySelector('[data-slot="body"] form')!.className).toContain(
|
||||
'sm:grid-cols-[440px_minmax(0,1fr)]',
|
||||
)
|
||||
expect(rows()).toHaveLength(6)
|
||||
})
|
||||
|
||||
it('Add variant is a tall bottom sheet on mobile', async () => {
|
||||
await mountPage({ desktop: false })
|
||||
|
||||
await openAddForm()
|
||||
|
||||
const panel = sheet()
|
||||
expect(panel.getAttribute('data-side')).toBe('bottom')
|
||||
expect(panel.className).toContain('h-23/25')
|
||||
expect(panel.className).toContain('before:w-9')
|
||||
expect(buttonByLabel('Close')!.className).toContain('size-11')
|
||||
expect(buttonByLabel('Back')).toBeNull()
|
||||
expect(panel.querySelector('[data-slot="footer"]')!.className).toContain('h-18')
|
||||
expect(rows()).toHaveLength(6)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['desktop', true],
|
||||
['mobile', false],
|
||||
])('Edit uses the same container on %s', async (_viewport, desktop) => {
|
||||
await mountPage({ desktop })
|
||||
await openAddForm()
|
||||
const addSide = sheet().getAttribute('data-side')
|
||||
await click(buttonByLabel('Close'))
|
||||
|
||||
await openEditForm('hero')
|
||||
|
||||
expect(sheet().getAttribute('data-side')).toBe(addSide)
|
||||
expect(sheet().querySelector('[data-slot="title"]')?.textContent?.trim()).toBe(
|
||||
'Edit variant: hero',
|
||||
)
|
||||
})
|
||||
|
||||
it('Focus moves to the Name field in create mode', async () => {
|
||||
await mountPage({ desktop: false })
|
||||
|
||||
await openAddForm()
|
||||
|
||||
expect(document.activeElement).toBe(field('name'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('Rule: Dismissing the mobile sheet', () => {
|
||||
const gestures: [string, () => Promise<void>][] = [
|
||||
['the close button', () => click(buttonByLabel('Close'))],
|
||||
['Cancel', () => click(buttonIn(sheet(), 'Cancel'))],
|
||||
['Escape', () => pressEscape()],
|
||||
['the scrim', () => clickBackdrop()],
|
||||
]
|
||||
|
||||
it.each(gestures)(
|
||||
'A pristine form closes with %s and returns focus',
|
||||
async (_name, dismiss) => {
|
||||
await mountPage({ desktop: false })
|
||||
await openAddForm()
|
||||
|
||||
await dismiss()
|
||||
|
||||
expect(slideover()).toBeNull()
|
||||
expect(alertDialog()).toBeNull()
|
||||
expect(document.activeElement).toBe(addButton())
|
||||
},
|
||||
)
|
||||
|
||||
it.each(gestures)('A dirty form asks first with %s', async (_name, dismiss) => {
|
||||
await mountPage({ desktop: false })
|
||||
await openAddForm()
|
||||
await typeInto(field('name'), 'banner')
|
||||
|
||||
await dismiss()
|
||||
|
||||
expect(textOf(alertDialog()!)).toContain('Discard changes?')
|
||||
expect(slideover()).not.toBeNull()
|
||||
})
|
||||
|
||||
it('Nothing closes while saving', async () => {
|
||||
const pending = deferred<unknown>()
|
||||
await mountPage({ desktop: false, routes: { [CREATE]: () => pending.promise } })
|
||||
await openAddForm()
|
||||
await typeInto(field('name'), 'banner')
|
||||
await typeInto(field('width'), '400')
|
||||
await click(buttonIn(sheet(), 'Add variant'))
|
||||
|
||||
await pressEscape()
|
||||
await click(buttonByLabel('Close'))
|
||||
await click(buttonIn(sheet(), 'Cancel'))
|
||||
await clickBackdrop()
|
||||
|
||||
expect(slideover()).not.toBeNull()
|
||||
expect(alertDialog()).toBeNull()
|
||||
pending.resolve(variant({ name: 'banner' }))
|
||||
await flushPromises()
|
||||
})
|
||||
})
|
||||
@@ -185,23 +185,33 @@ describe('Rule: Slide-over opening and closing', () => {
|
||||
expect(rows()).toHaveLength(6)
|
||||
})
|
||||
|
||||
it('Add variant opens a full page on mobile', async () => {
|
||||
it('Add variant opens a bottom sheet on mobile', async () => {
|
||||
await mountPage({ desktop: false })
|
||||
|
||||
await openAddForm()
|
||||
|
||||
const panel = form()
|
||||
expect(buttonByLabel('Close')).toBeNull()
|
||||
const back = buttonByLabel('Back')!
|
||||
expect(iconNames(back)).toContain('i-lucide-arrow-left')
|
||||
expect(panel.getAttribute('data-side')).toBe('bottom')
|
||||
expect(panel.className).toContain('h-23/25')
|
||||
expect(panel.className).toContain('before:w-9')
|
||||
expect(panel.className).toContain('before:h-1')
|
||||
expect(panel.className).toContain('before:bg-(--ui-border-accented)')
|
||||
expect(buttonByLabel('Back')).toBeNull()
|
||||
const close = buttonByLabel('Close')!
|
||||
expect(iconNames(close)).toContain('i-lucide-x')
|
||||
expect(close.className).toContain('size-11')
|
||||
expect(panel.querySelector('[data-slot="title"]')?.textContent?.trim()).toBe('Add variant')
|
||||
expect(panel.className).toContain('w-full')
|
||||
expect(panel.className).toContain('inset-x-0')
|
||||
expect(panel.querySelector('[data-slot="footer"]')!.className).toContain('h-18')
|
||||
expect(buttonIn(panel, 'Cancel')!.className).toContain('h-11')
|
||||
expect(buttonIn(panel, 'Cancel')!.className).toContain('flex-1')
|
||||
expect(saveButton().className).toContain('h-11')
|
||||
expect(saveButton().className).toContain('flex-1')
|
||||
expect(panel.textContent).toContain('Preview')
|
||||
const sections = panel.querySelectorAll('[data-slot="body"] form > div')
|
||||
expect(sections[1]!.className).toContain('border-t')
|
||||
expect(rows()).toHaveLength(6)
|
||||
expect(document.body.querySelector('[data-slot="overlay"]')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('Edit opens with the stored values', async () => {
|
||||
@@ -287,12 +297,12 @@ describe('Rule: Slide-over opening and closing', () => {
|
||||
expect(dialog.querySelectorAll('button')[1]!.className).toContain('bg-error')
|
||||
})
|
||||
|
||||
it('Dismissing a dirty form on mobile with the back arrow asks first', async () => {
|
||||
it('Dismissing a dirty form on mobile with the close button asks first', async () => {
|
||||
await mountPage({ desktop: false })
|
||||
await openAddForm()
|
||||
await typeInto(field('name'), 'banner')
|
||||
|
||||
await click(buttonByLabel('Back'))
|
||||
await click(buttonByLabel('Close'))
|
||||
|
||||
expect(textOf(alertDialog()!)).toContain('Discard changes?')
|
||||
})
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { visualStates } from '../../../visual/states'
|
||||
|
||||
const GROUPS: Record<string, string[]> = {
|
||||
'Variants list': [
|
||||
'Loaded',
|
||||
'Loading',
|
||||
'Empty',
|
||||
'Error loading',
|
||||
'Forbidden',
|
||||
'Row menu',
|
||||
'Row action in progress',
|
||||
'Purge confirm',
|
||||
'Delete confirm',
|
||||
'Delete in progress',
|
||||
'Request reference',
|
||||
'Entry: sidebar',
|
||||
],
|
||||
'Add edit variant': [
|
||||
'Create: pristine',
|
||||
'Create: dirty',
|
||||
'Field errors',
|
||||
'No dimension',
|
||||
'Name already taken',
|
||||
'Saving',
|
||||
'Save error',
|
||||
'Edit: pristine',
|
||||
'Edit: dirty',
|
||||
'Unsaved changes',
|
||||
'PNG: no quality',
|
||||
],
|
||||
'Media library': [
|
||||
'V3a · Grid with badges',
|
||||
'V3b · Sort menu',
|
||||
'V3c · Filter: With variants, sorted by total size',
|
||||
'V3d · Filter: Without variants',
|
||||
'V3e · Filter: nothing to show',
|
||||
'V3f · Delete confirm',
|
||||
],
|
||||
'Image detail': [
|
||||
'V4a · Partly generated',
|
||||
'V4b · Nothing generated yet',
|
||||
'V4c · Generating',
|
||||
'V4d · Generation failed',
|
||||
'V4e · No variants defined',
|
||||
'V4f · Source is an SVG',
|
||||
'V4h · Preview dialog',
|
||||
],
|
||||
'Cache panel': [
|
||||
'V5a · Loaded',
|
||||
'V5b · Loading',
|
||||
'V5c · Empty cache',
|
||||
'V5d · Purging',
|
||||
'V5e · Purge done',
|
||||
'V5f · Purge failed',
|
||||
'V5g · Error loading',
|
||||
'V5h · Purge all: confirm',
|
||||
'V5i · Purge all: confirm, typed',
|
||||
'V5j · Purge unused: confirm',
|
||||
],
|
||||
'Editor picker': [
|
||||
'V6a · Variant select',
|
||||
'V6b · Select open',
|
||||
'V6c · No variants defined',
|
||||
'V6d · Variant deleted',
|
||||
'V6e · Variant disabled',
|
||||
],
|
||||
}
|
||||
|
||||
describe('Visual setups of the image variants screens', () => {
|
||||
it.each(Object.entries(GROUPS))('Every state of %s has a setup', (_group, states) => {
|
||||
const setups = visualStates['image-variants']!
|
||||
expect(states.filter((state) => !(state in setups))).toEqual([])
|
||||
})
|
||||
|
||||
it('No setup carries a clock-sensitive skip without a reason to', () => {
|
||||
const setups = Object.values(visualStates['image-variants']!)
|
||||
expect(setups.filter((setup) => setup.skip === 'pending')).toEqual([])
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user