feat: schema builder's create flow

This commit is contained in:
Florian Sylvain committed 2026-10-20 00:29:01 +02:00
1 parent f514146fed
commit 083cc9e23e
71 files changed
+7223 -61

No files matched your search

@@ -0,0 +1,141 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { flushPromises } from '@vue/test-utils'
import { addCollection } from '@iconify/vue'
import lucide from 'virtual:lucide-subset'
import FieldList from '../FieldList.vue'
import { createField, type BuilderField } from '@/lib/schemaBuilderModel'
import { mountInApp, text, unmountApp, holder } from './directoryKit'
vi.mock('@/composables/useApi', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/composables/useApi')>()
return { ...actual, useApi: vi.fn() }
})
addCollection(lucide)
afterEach(unmountApp)
const FIELDS: BuilderField[] = [
createField(1, { label: 'Title', key: 'title' }),
createField(2, { label: 'Starts on', key: 'starts_on', type: 'date' }),
createField(3, { label: 'Ends on', key: 'ends_on', type: 'date' }),
]
function props(extra: Record<string, unknown> = {}) {
return {
fields: FIELDS,
selectedUid: 2,
errors: new Map(),
disabled: false,
mobile: false,
...extra,
}
}
function emitted(name: string) {
return holder.wrapper!.findComponent(FieldList).emitted(name) ?? []
}
function grip(label: string) {
return document.body.querySelector<HTMLElement>(`[aria-label="Move ${label}"]`)!
}
async function press(target: Element, key: string) {
target.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }))
await flushPromises()
}
function labels() {
return [...document.body.querySelectorAll('[data-testid="field-row-label"]')].map(text)
}
describe('FieldList', () => {
it('lists the rows and counts them against 50', async () => {
await mountInApp(FieldList, props())
expect(labels()).toEqual(['Title', 'Starts on', 'Ends on'])
expect(text(document.body.querySelector('[data-testid="field-count"]'))).toBe('3 of 50')
})
it('emits select, add, move and remove', async () => {
await mountInApp(FieldList, props())
document.body.querySelector<HTMLElement>('[data-field-row][data-uid="3"]')!.click()
;(document.body.querySelector('[data-action="up"]') as HTMLElement).click()
;(document.body.querySelector('[data-action="remove"]') as HTMLElement).click()
const add = [...document.body.querySelectorAll('button')].find(
(candidate) => text(candidate) === 'Add field',
)!
add.click()
await flushPromises()
expect(emitted('select')).toEqual([[3]])
expect(emitted('move')).toEqual([[2, -1]])
expect(emitted('remove')).toEqual([[2]])
expect(emitted('add')).toHaveLength(1)
})
it('disables Add field at 50 rows and when disabled', async () => {
const many = Array.from({ length: 50 }, (_, index) =>
createField(index + 1, { label: 'F' }),
)
await mountInApp(FieldList, props({ fields: many, selectedUid: 1 }))
const add = [...document.body.querySelectorAll<HTMLButtonElement>('button')].find(
(candidate) => text(candidate) === 'Add field',
)!
expect(add.disabled).toBe(true)
expect(text(document.body.querySelector('[data-testid="field-count"]'))).toBe('50 of 50')
})
it('shows the empty state with the error message in the error colour', async () => {
await mountInApp(FieldList, props({ fields: [], selectedUid: null, listError: 'Nope.' }))
const empty = document.body.querySelector('[data-testid="field-empty"]')!
expect(text(empty)).toBe('No fields yet Nope.')
expect(empty.querySelector('.text-error')).not.toBeNull()
})
it('emits the new order once, on drop, and nothing on cancel', async () => {
await mountInApp(FieldList, props())
await press(grip('Starts on'), ' ')
await press(grip('Starts on'), 'ArrowUp')
await press(grip('Starts on'), 'Escape')
expect(emitted('reorder')).toHaveLength(0)
expect(labels()).toEqual(['Title', 'Starts on', 'Ends on'])
await press(grip('Starts on'), ' ')
await press(grip('Starts on'), 'ArrowUp')
expect(emitted('reorder')).toHaveLength(0)
await press(grip('Starts on'), ' ')
expect(emitted('reorder')).toEqual([[[2, 1, 3]]])
})
it('moves the focus between rows with the arrow keys', async () => {
await mountInApp(FieldList, props())
const rows = [...document.body.querySelectorAll<HTMLElement>('[data-field-row]')]
rows[0]!.focus()
await press(rows[0]!, 'ArrowDown')
expect(document.activeElement).toBe(rows[1])
await press(rows[1]!, 'ArrowUp')
expect(document.activeElement).toBe(rows[0])
})
it('keeps one roving tab stop for the rows', async () => {
await mountInApp(FieldList, props())
const stops = [...document.body.querySelectorAll<HTMLElement>('[data-field-row]')].filter(
(row) => row.tabIndex === 0,
)
expect(stops.map((row) => row.getAttribute('data-uid'))).toEqual(['2'])
})
it('hides the row actions on mobile', async () => {
await mountInApp(FieldList, props({ mobile: true }))
expect(document.body.querySelector('[data-action]')).toBeNull()
})
})