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() 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 = {}) { 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(`[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('[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('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('[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('[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() }) })