Files
summercms/admin/tests/form/FormField.test.ts
Jakub Zych 11c4e5466b test(10.1-04): bring the SPA extension point under Vitest
- WidgetField: skeleton and aria-busy, the 5000 ms whenDefined timeout,
  script failure, attributes only, fill-values and locale sync, one POST
  while busy, fill-key-only patching, danger toasts, unmount
- PartialHost and partialNodes: exhaustive tag, attribute and URL
  allowlist, depth and node caps, skeleton sizes, empty, failure,
  busy refetch and ?id= only with a record
- PartialField, ListToolbar, ListView, registry, formState, FormField and
  FormView: group labels, registered toolbar actions, header refetch
  rules, form context provision and asset loading
2026-09-29 02:59:13 +02:00

131 lines
6.3 KiB
TypeScript

import { beforeEach, describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import FieldRenderer from '../../src/components/form/FieldRenderer.vue'
import FormErrorBanner from '../../src/components/form/FormErrorBanner.vue'
import FormField from '../../src/components/form/FormField.vue'
import { clone, extensionFormSchemaFixture, formField } from '../fixtures/typed'
import { mockApi, resetState } from '../helpers'
beforeEach(() => {
resetState()
})
describe('form field', () => {
it('links the label to the control and hides the required asterisk from assistive tech', () => {
const wrapper = mount(FormField, { props: { field: formField('name'), modelValue: 'Blue', idPrefix: 'f' } })
const label = wrapper.find('label')
expect(label.attributes('for')).toBe('f-name')
expect(label.text()).toBe('Name *')
expect(label.find('[aria-hidden="true"]').text()).toBe('*')
const input = wrapper.find('#f-name')
expect(input.attributes('aria-required')).toBe('true')
expect(input.attributes('aria-invalid')).toBeUndefined()
expect(input.attributes('aria-describedby')).toBeUndefined()
})
it('describes the control by its comment and, when invalid, by its error line', () => {
const wrapper = mount(FormField, {
props: { field: formField('code'), modelValue: '', errors: ['Taken', 'Too short'], idPrefix: 'f' },
})
const input = wrapper.find('#f-code')
expect(input.attributes('aria-invalid')).toBe('true')
expect(input.attributes('aria-describedby')).toBe('f-code-comment f-code-error')
expect(wrapper.find('#f-code-comment').text()).toBe('Unique widget code')
expect(wrapper.findAll('#f-code-error p').map((line) => line.text())).toEqual(['Taken', 'Too short'])
})
it('uses the field name without a label and the default id prefix', () => {
const field = { ...formField('code'), label: '', comment: undefined }
const wrapper = mount(FormField, { props: { field, modelValue: '' } })
expect(wrapper.find('label').text()).toBe('code')
expect(wrapper.find('label').attributes('for')).toBe('field-code')
})
it('lets a toggle card render its own label and still points at its comment', () => {
const wrapper = mount(FormField, { props: { field: formField('active'), modelValue: false, errors: ['Bad'], idPrefix: 'f' } })
expect(wrapper.findAll('label')).toHaveLength(1)
expect(wrapper.find('label').attributes('for')).toBe('f-active')
expect(wrapper.find('[role="switch"]').attributes('aria-describedby')).toBe('f-active-comment f-active-error')
})
it('forwards value updates from the control', async () => {
const wrapper = mount(FormField, { props: { field: formField('name'), modelValue: '', idPrefix: 'f' } })
await wrapper.find('input').setValue('New')
expect(wrapper.emitted('update:modelValue')).toEqual([['New']])
})
})
describe('widget and partial rows (Phase 10.1)', () => {
function extensionField(name: string, change: Record<string, unknown> = {}) {
const field = clone(extensionFormSchemaFixture).data.fields.find((item) => item.name === name)
if (!field) {
throw new Error(`fixture field ${name} missing`)
}
return { ...field, ...change }
}
it.each(['lookup', 'summary'])('labels the %s group with a span the group points at', (name) => {
mockApi({})
const wrapper = mount(FormField, {
props: { field: extensionField(name, { required: true }), modelValue: undefined, idPrefix: 'f', source: null },
})
const label = wrapper.find(`#f-${name}-label`)
expect(label.element.tagName).toBe('SPAN')
expect(label.classes()).toContain('font-semibold')
expect(label.find('[aria-hidden="true"]').text()).toBe('*')
expect(wrapper.find('label').exists()).toBe(false)
expect(wrapper.find(`#f-${name}`).attributes('aria-labelledby')).toBe(`f-${name}-label`)
})
it.each(['lookup', 'summary'])('shows no label row for an unlabelled %s', (name) => {
mockApi({})
const wrapper = mount(FormField, { props: { field: extensionField(name, { label: '' }), modelValue: undefined, idPrefix: 'f', source: null } })
expect(wrapper.find(`#f-${name}-label`).exists()).toBe(false)
expect(wrapper.find('label').exists()).toBe(false)
expect(wrapper.find(`#f-${name}`).attributes('aria-labelledby')).toBeUndefined()
})
it('points the widget group at its comment', () => {
mockApi({})
const wrapper = mount(FormField, { props: { field: extensionField('lookup'), modelValue: undefined, idPrefix: 'f', source: null } })
expect(wrapper.find('#f-lookup-comment').text()).toBe('Fills in the name. Save to keep it.')
expect(wrapper.find('#f-lookup').attributes('aria-describedby')).toBe('f-lookup-comment')
})
})
describe('field renderer', () => {
it('picks the control by type and forwards the shared props and updates', async () => {
const wrapper = mount(FieldRenderer, {
props: { field: formField('weight'), modelValue: 3, controlId: 'x', invalid: true, describedBy: 'x-error' },
})
const input = wrapper.find('input[type="number"]')
expect(input.attributes('id')).toBe('x')
expect(input.attributes('aria-invalid')).toBe('true')
expect(input.attributes('aria-describedby')).toBe('x-error')
await input.setValue('7')
expect(wrapper.emitted('update:modelValue')).toEqual([[7]])
})
it('renders the unsupported box for an unknown type', () => {
const wrapper = mount(FieldRenderer, { props: { field: formField('color'), modelValue: '#fff', controlId: 'c' } })
expect(wrapper.find('[data-unsupported-field]').exists()).toBe(true)
})
})
describe('form error banner (D-09)', () => {
it('is an alert with the plural field count that lists messages of non-field keys', () => {
const wrapper = mount(FormErrorBanner, {
props: { errors: { name: ['Required'], code: ['Taken'], _form: ['Record is locked'] }, fieldNames: ['name', 'code'] },
})
const banner = wrapper.find('[role="alert"]')
expect(banner.text()).toContain('Nie udało się zapisać.')
expect(banner.text()).toContain('Popraw 3 pola oznaczone poniżej.')
expect(banner.findAll('p').at(-1)!.text()).toBe('Record is locked')
})
it('renders nothing without errors', () => {
const wrapper = mount(FormErrorBanner, { props: { errors: {}, fieldNames: ['name'] } })
expect(wrapper.find('[role="alert"]').exists()).toBe(false)
})
})