feat(FormField): set aria-describedby and aria-invalid attributes (#3123)

This commit is contained in:
Romain Hamel
2025-01-20 11:46:09 +01:00
committed by GitHub
parent b8d99726ef
commit b95b91391a
21 changed files with 275 additions and 135 deletions

View File

@@ -1,16 +1,58 @@
import { defineComponent } from 'vue'
import { describe, it, expect } from 'vitest'
import FormField, { type FormFieldProps, type FormFieldSlots } from '../../src/runtime/components/FormField.vue'
import { describe, it, expect, test, vi } from 'vitest'
import type { FormFieldProps, FormFieldSlots } from '../../src/runtime/components/FormField.vue'
import ComponentRender from '../component-render'
import theme from '#build/ui/form-field'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import {
UInput,
URadioGroup,
UTextarea,
UCheckbox,
USelect,
USelectMenu,
UInputMenu,
UInputNumber,
USwitch,
USlider,
UPinInput,
UFormField
} from '#components'
const inputComponents = [UInput, URadioGroup, UTextarea, UCheckbox, USelect, USelectMenu, UInputMenu, UInputNumber, USwitch, USlider, UPinInput]
async function renderFormField(options: {
props: Partial<FormFieldProps>
inputComponent: typeof inputComponents[number]
}) {
return await mountSuspended(UFormField, {
props: options.props,
slots: {
default: {
// @ts-expect-error - Object literal may only specify known properties, and setup does not exist in type
setup: () => ({ inputComponent: options.inputComponent }),
components: {
UFormField,
...inputComponents
},
template: `
<component :is="inputComponent" />
`
}
}
})
}
// A wrapper component is needed here because of a conflict with the error prop / expose.
// See: https://github.com/nuxt/test-utils/issues/684
const FormFieldWrapper = defineComponent({
components: {
UFormField: FormField
UFormField
},
template: `<UFormField>
template: `
<UFormField>
<template v-for="(_, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
@@ -42,4 +84,80 @@ describe('FormField', () => {
const html = await ComponentRender(nameOrHtml, options, FormFieldWrapper)
expect(html).toMatchSnapshot()
})
describe.each(inputComponents.map(inputComponent => [(inputComponent as any).__name, inputComponent]))('%s integration', async (name: string, inputComponent: any) => {
// Mock useId to force a consistent return value in Nuxt and Vue. This is required to test aria attributes.
vi.mock('vue', async () => {
const actual = await vi.importActual('vue')
return {
...actual,
useId: () => 'v-0-0' // Static value matching Nuxt's format
}
})
if (name === 'RadioGroup') {
test('unbinds label for', async () => {
const wrapper = await renderFormField({
props: { label: 'Label' },
inputComponent
})
const label = wrapper.find('label[for=v-0-0]')
expect(label.exists()).toBe(false)
})
} else {
test('binds label for', async () => {
const wrapper = await renderFormField({
props: { label: 'Label' },
inputComponent
})
const label = wrapper.find('label[for=v-0-0]')
expect(label.exists()).toBe(true)
const input = wrapper.find('[id=v-0-0]')
expect(input.exists()).toBe(true)
})
}
test('binds hints with aria-describedby', async () => {
const wrapper = await renderFormField({
props: { hint: 'somehint' },
inputComponent
})
const attr = wrapper.find('[aria-describedby=v-0-0-hint]')
expect(attr.exists()).toBe(true)
})
test('binds description with aria-describedby', async () => {
const wrapper = await renderFormField({
props: { description: 'somedescription' },
inputComponent
})
const attr = wrapper.find('[aria-describedby=v-0-0-description]')
expect(attr.exists()).toBe(true)
})
test('binds error with aria-describedby', async () => {
const wrapper = await renderFormField({
props: { error: 'someerror' },
inputComponent
})
const attr = wrapper.find('[aria-describedby=v-0-0-error]')
expect(attr.exists()).toBe(true)
})
test('binds aria-invalid on error', async () => {
const wrapper = await renderFormField({
props: { error: 'someerror' },
inputComponent
})
const attr = wrapper.find('[aria-invalid=true]')
expect(attr.exists()).toBe(true)
})
})
})