- 41 unit and component suites under admin/tests/{app,state,shell,list,form,relation,views,ui}
covering states and a11y roles; every src module is imported by a test
- typed fixture helper assigns each JSON fixture to its generated OpenAPI type
- fix: iconFor ignores inherited object members such as "constructor"
- fix: field controls import ./control instead of the registry (import cycle
left a renderer unregistered depending on module load order)
- fix: dropdown shows the placeholder for an unknown stored value next to an emptyOption
- fix: list announces a failed schema load even when the rows arrive after it
- tailwind no longer scans admin/tests; boardwalk/dist rebuilt
216 lines
10 KiB
TypeScript
216 lines
10 KiB
TypeScript
import { beforeEach, describe, expect, it } from 'vitest'
|
|
import { mount } from '@vue/test-utils'
|
|
import CheckboxField from '../../src/components/form/fields/CheckboxField.vue'
|
|
import DropdownField from '../../src/components/form/fields/DropdownField.vue'
|
|
import NumberField from '../../src/components/form/fields/NumberField.vue'
|
|
import SwitchField from '../../src/components/form/fields/SwitchField.vue'
|
|
import TextareaField from '../../src/components/form/fields/TextareaField.vue'
|
|
import TextField from '../../src/components/form/fields/TextField.vue'
|
|
import UnsupportedField from '../../src/components/form/fields/UnsupportedField.vue'
|
|
import { setBundle } from '../../src/app/i18n'
|
|
import type { FormField } from '../../src/api/types'
|
|
import { formField } from '../fixtures/typed'
|
|
import { resetState } from '../helpers'
|
|
|
|
const common = { controlId: 'c', describedBy: 'c-comment' }
|
|
|
|
beforeEach(() => {
|
|
resetState()
|
|
})
|
|
|
|
describe('text field', () => {
|
|
it('is a required-aware text input that emits its value', async () => {
|
|
const field = { ...formField('name'), attributes: { placeholder: 'Type', onclick: 'x' } }
|
|
const wrapper = mount(TextField, { props: { field, modelValue: 'Blue', invalid: true, ...common } })
|
|
const input = wrapper.find('input')
|
|
expect(input.attributes()).toMatchObject({
|
|
id: 'c',
|
|
name: 'name',
|
|
type: 'text',
|
|
'aria-required': 'true',
|
|
'aria-invalid': 'true',
|
|
'aria-describedby': 'c-comment',
|
|
placeholder: 'Type',
|
|
})
|
|
expect(input.attributes('required')).toBeDefined()
|
|
expect(input.attributes('onclick')).toBeUndefined()
|
|
expect(input.classes()).toContain('border-danger')
|
|
await input.setValue('Red')
|
|
expect(wrapper.emitted('update:modelValue')).toEqual([['Red']])
|
|
})
|
|
|
|
it('shows an empty input for null and stringifies numbers', () => {
|
|
const optional = formField('code')
|
|
expect((mount(TextField, { props: { field: optional, modelValue: null, controlId: 'c' } }).find('input').element as HTMLInputElement).value).toBe('')
|
|
const input = mount(TextField, { props: { field: optional, modelValue: 12, controlId: 'c' } }).find('input')
|
|
expect((input.element as HTMLInputElement).value).toBe('12')
|
|
expect(input.attributes('aria-required')).toBeUndefined()
|
|
expect(input.attributes('required')).toBeUndefined()
|
|
expect(input.attributes('aria-describedby')).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('textarea field', () => {
|
|
it.each([
|
|
['tiny', '2'],
|
|
['small', '4'],
|
|
['large', '8'],
|
|
['huge', '12'],
|
|
['giant', '18'],
|
|
[undefined, '5'],
|
|
['odd', '5'],
|
|
])('maps size %s to %s rows', (size, rows) => {
|
|
const field: FormField = { ...formField('notes'), size }
|
|
expect(mount(TextareaField, { props: { field, modelValue: 'x', controlId: 't' } }).find('textarea').attributes('rows')).toBe(rows)
|
|
})
|
|
|
|
it('emits its text', async () => {
|
|
const wrapper = mount(TextareaField, { props: { field: formField('notes'), modelValue: undefined, controlId: 't' } })
|
|
expect((wrapper.find('textarea').element as HTMLTextAreaElement).value).toBe('')
|
|
await wrapper.find('textarea').setValue('Long text')
|
|
expect(wrapper.emitted('update:modelValue')).toEqual([['Long text']])
|
|
})
|
|
})
|
|
|
|
describe('number field', () => {
|
|
it('emits numbers and null for an empty input', async () => {
|
|
const wrapper = mount(NumberField, { props: { field: formField('weight'), modelValue: 12, controlId: 'n' } })
|
|
const input = wrapper.find('input')
|
|
expect(input.attributes('type')).toBe('number')
|
|
expect(input.attributes('inputmode')).toBe('decimal')
|
|
expect((input.element as HTMLInputElement).value).toBe('12')
|
|
await input.setValue('3.5')
|
|
await input.setValue('')
|
|
await input.setValue(' ')
|
|
expect(wrapper.emitted('update:modelValue')).toEqual([[3.5], [null], [null]])
|
|
})
|
|
|
|
it('emits null for a value that is not a finite number', () => {
|
|
const wrapper = mount(NumberField, { props: { field: formField('weight'), modelValue: null, controlId: 'n' } })
|
|
const input = wrapper.find('input').element as HTMLInputElement
|
|
Object.defineProperty(input, 'value', { configurable: true, get: () => 'Infinity' })
|
|
input.dispatchEvent(new Event('input'))
|
|
expect(wrapper.emitted('update:modelValue')).toEqual([[null]])
|
|
})
|
|
})
|
|
|
|
describe('dropdown field', () => {
|
|
const typed: FormField = {
|
|
...formField('size'),
|
|
options: [
|
|
{ value: 's', label: 'Small' },
|
|
{ value: 2, label: 'Two' },
|
|
{ value: true, label: 'Yes' },
|
|
],
|
|
}
|
|
|
|
it('offers the emptyOption first and sends null for it', async () => {
|
|
const wrapper = mount(DropdownField, { props: { field: typed, modelValue: null, controlId: 'd' } })
|
|
const select = wrapper.find('select')
|
|
expect(select.findAll('option').map((option) => option.text())).toEqual(['— none —', 'Small', 'Two', 'Yes'])
|
|
expect((select.element as HTMLSelectElement).value).toBe('empty')
|
|
expect(select.classes()).toContain('text-muted')
|
|
await select.setValue('empty')
|
|
expect(wrapper.emitted('update:modelValue')).toEqual([[null]])
|
|
})
|
|
|
|
it('keeps the JSON type of the chosen option value', async () => {
|
|
const wrapper = mount(DropdownField, { props: { field: typed, modelValue: 's', controlId: 'd' } })
|
|
const select = wrapper.find('select')
|
|
expect((select.element as HTMLSelectElement).value).toBe('0')
|
|
expect(select.classes()).not.toContain('text-muted')
|
|
await select.setValue('1')
|
|
await select.setValue('2')
|
|
expect(wrapper.emitted('update:modelValue')).toEqual([[2], [true]])
|
|
})
|
|
|
|
it('matches a stored value by its string form', () => {
|
|
const wrapper = mount(DropdownField, { props: { field: typed, modelValue: '2', controlId: 'd' } })
|
|
expect((wrapper.find('select').element as HTMLSelectElement).value).toBe('1')
|
|
})
|
|
|
|
it('shows a disabled placeholder without an emptyOption or a matching value', () => {
|
|
const field: FormField = { ...typed, emptyOption: undefined, required: true }
|
|
const wrapper = mount(DropdownField, { props: { field, modelValue: 'unknown', controlId: 'd', invalid: true } })
|
|
const placeholder = wrapper.find('option[value="placeholder"]')
|
|
expect(placeholder.text()).toBe('Wybierz…')
|
|
expect(placeholder.attributes('disabled')).toBeDefined()
|
|
expect(wrapper.find('select').attributes('aria-required')).toBe('true')
|
|
expect(wrapper.find('select').attributes('aria-invalid')).toBe('true')
|
|
expect(mount(DropdownField, { props: { field, modelValue: 's', controlId: 'd' } }).find('option[value="placeholder"]').exists()).toBe(false)
|
|
})
|
|
|
|
it('shows the placeholder, not the empty option, for a stored value it does not know', () => {
|
|
const wrapper = mount(DropdownField, { props: { field: typed, modelValue: 'zzz', controlId: 'd' } })
|
|
expect((wrapper.find('select').element as HTMLSelectElement).value).toBe('placeholder')
|
|
})
|
|
})
|
|
|
|
describe('toggle cards', () => {
|
|
it('renders a switch with role=switch, aria-checked, its own label and comment', async () => {
|
|
const wrapper = mount(SwitchField, { props: { field: formField('active'), modelValue: true, controlId: 's', describedBy: 's-comment' } })
|
|
expect(wrapper.find('[data-toggle-card]').exists()).toBe(true)
|
|
expect(wrapper.find('label').attributes('for')).toBe('s')
|
|
expect(wrapper.find('label').text()).toBe('Active')
|
|
expect(wrapper.find('#s-comment').text()).toBe('Shown in the catalogue')
|
|
const control = wrapper.find('[role="switch"]')
|
|
expect(control.attributes('id')).toBe('s')
|
|
expect(control.attributes('type')).toBe('button')
|
|
expect(control.attributes('aria-checked')).toBe('true')
|
|
expect(control.attributes('aria-describedby')).toBe('s-comment')
|
|
await control.trigger('click')
|
|
expect(wrapper.emitted('update:modelValue')).toEqual([[false]])
|
|
})
|
|
|
|
it('keeps a numeric switch value numeric and marks it invalid', async () => {
|
|
const field = { ...formField('active'), required: true, label: '' }
|
|
const wrapper = mount(SwitchField, { props: { field, modelValue: 0, controlId: 's', invalid: true } })
|
|
expect(wrapper.find('[role="switch"]').attributes('aria-checked')).toBe('false')
|
|
expect(wrapper.find('[role="switch"]').attributes('aria-invalid')).toBe('true')
|
|
expect(wrapper.find('[data-toggle-card]').classes()).toContain('border-danger')
|
|
expect(wrapper.find('label').text()).toBe('active *')
|
|
await wrapper.find('[role="switch"]').trigger('click')
|
|
expect(wrapper.emitted('update:modelValue')).toEqual([[1]])
|
|
})
|
|
|
|
it('renders a checkbox card that emits its checked state', async () => {
|
|
const wrapper = mount(CheckboxField, { props: { field: formField('featured'), modelValue: '1', controlId: 'k' } })
|
|
const box = wrapper.find('input[type="checkbox"]')
|
|
expect((box.element as HTMLInputElement).checked).toBe(true)
|
|
expect(wrapper.find('label').attributes('for')).toBe('k')
|
|
expect(wrapper.find('#k-comment').text()).toBe('Pinned to the top')
|
|
await box.setValue(false)
|
|
expect(wrapper.emitted('update:modelValue')).toEqual([[false]])
|
|
|
|
const numeric = mount(CheckboxField, { props: { field: formField('featured'), modelValue: 0, controlId: 'k', invalid: true } })
|
|
expect(numeric.find('input').attributes('aria-invalid')).toBe('true')
|
|
await numeric.find('input').setValue(true)
|
|
expect(numeric.emitted('update:modelValue')).toEqual([[1]])
|
|
})
|
|
})
|
|
|
|
describe('unsupported field (D-05)', () => {
|
|
it('names the type as code inside the translated sentence', () => {
|
|
const wrapper = mount(UnsupportedField, { props: { field: formField('color'), modelValue: '#fff', controlId: 'u', describedBy: 'u-error' } })
|
|
const box = wrapper.find('[data-unsupported-field]')
|
|
expect(box.attributes('id')).toBe('u')
|
|
expect(box.attributes('aria-describedby')).toBe('u-error')
|
|
expect(box.text()).toBe('Nieobsługiwany typ pola: colorpicker')
|
|
expect(box.find('code').text()).toBe('colorpicker')
|
|
})
|
|
|
|
it('appends the type when the sentence has no :type placeholder', () => {
|
|
setBundle({ 'backend::lang.form.unsupported_field': { other: 'Unsupported field' } }, 'en')
|
|
const wrapper = mount(UnsupportedField, { props: { field: formField('color'), modelValue: null, controlId: 'u' } })
|
|
expect(wrapper.text()).toBe('Unsupported field colorpicker')
|
|
expect(wrapper.find('code').text()).toBe('colorpicker')
|
|
})
|
|
|
|
it('renders a hostile type name as text', () => {
|
|
const field = { ...formField('color'), type: '<img src=x onerror=alert(1)>' }
|
|
const wrapper = mount(UnsupportedField, { props: { field, modelValue: null, controlId: 'u' } })
|
|
expect(wrapper.find('img').exists()).toBe(false)
|
|
expect(wrapper.find('code').text()).toBe('<img src=x onerror=alert(1)>')
|
|
})
|
|
})
|