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 = {}) { 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) }) })