feat: schema builder's create flow
This commit is contained in:
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()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user