Skip to content

Commit

Permalink
test: added error message aria assertions
Browse files Browse the repository at this point in the history
  • Loading branch information
logaretm committed Aug 18, 2024
1 parent 30faeef commit 9dd8312
Show file tree
Hide file tree
Showing 12 changed files with 299 additions and 43 deletions.
19 changes: 14 additions & 5 deletions packages/core/src/useCheckbox/useCheckboxGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ import {
Arrayable,
TypedSchema,
} from '../types';
import { useUniqId, createDescribedByProps, normalizeProps, isEqual } from '../utils/common';
import {
useUniqId,
createDescribedByProps,
normalizeProps,
isEqual,
createAccessibleErrorMessageProps,
} from '../utils/common';
import { useLocale } from '../i18n/useLocale';
import { useFormField } from '../useFormField';
import { FieldTypePrefixes } from '../constants';
Expand Down Expand Up @@ -88,19 +94,22 @@ export function useCheckboxGroup<TCheckbox>(_props: Reactivify<CheckboxGroupProp
const { displayError } = useErrorDisplay(field);
const { validityDetails } = useInputValidity({ field });
const { fieldValue, setValue, isTouched, setTouched, errorMessage } = field;
const { describedBy, descriptionProps, errorMessageProps } = createDescribedByProps({
const { describedByProps, descriptionProps } = createDescribedByProps({
inputId: groupId,
errorMessage,
description: props.description,
});
const { accessibleErrorProps, errorMessageProps } = createAccessibleErrorMessageProps({
inputId: groupId,
errorMessage,
});

const checkboxGroupProps = computed<CheckboxGroupDomProps>(() => {
return {
...labelledByProps.value,
...describedByProps.value,
...accessibleErrorProps.value,
dir: toValue(props.dir) ?? direction.value,
role: 'group',
'aria-describedby': describedBy(),
'aria-invalid': errorMessage.value ? true : undefined,
};
});

Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/useNumberField/useNumberField.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const makeTest = (props?: SetOptional<NumberFieldProps, 'label'>): Component =>
incrementButtonProps,
decrementButtonProps,
isTouched,
errorMessageProps,
errorMessage,
} = useNumberField({
...(props || {}),
label,
Expand All @@ -34,13 +36,17 @@ const makeTest = (props?: SetOptional<NumberFieldProps, 'label'>): Component =>
decrementButtonProps,
isTouched,
fieldValue,
errorMessageProps,
errorMessage,
};
},
template: `
<div data-testid="fixture" :class="{ 'touched': isTouched }">
<label v-bind="labelProps">{{ label }}</label>
<input v-bind="inputProps" />
<span v-bind="descriptionProps">description</span>
<span v-bind="errorMessageProps">{{ errorMessage }}</span>
<button v-bind="incrementButtonProps">Incr</button>
<button v-bind="decrementButtonProps">Decr</button>
<div data-testid="value">{{ JSON.stringify(fieldValue) }}</div>
Expand Down Expand Up @@ -106,3 +112,15 @@ test('Applies decimal inputmode if the step contains decimals', async () => {
await render(makeTest({ step: 1.5 }));
expect(screen.getByLabelText(label)).toHaveAttribute('inputmode', 'decimal');
});

test('picks up native error messages', async () => {
await render(makeTest({ required: true }));

await fireEvent.invalid(screen.getByLabelText(label));
await flush();
expect(screen.getByLabelText(label)).toHaveErrorMessage('Constraints not satisfied');

vi.useRealTimers();
expect(await axe(screen.getByTestId('fixture'))).toHaveNoViolations();
vi.useFakeTimers();
});
13 changes: 9 additions & 4 deletions packages/core/src/useNumberField/useNumberField.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Ref, computed, nextTick, shallowRef, toValue } from 'vue';
import {
createAccessibleErrorMessageProps,
createDescribedByProps,
isEmpty,
isNullOrUndefined,
Expand Down Expand Up @@ -97,12 +98,16 @@ export function useNumberField(
targetRef: inputRef,
});

const { errorMessageProps, descriptionProps, describedBy } = createDescribedByProps({
const { descriptionProps, describedByProps } = createDescribedByProps({
inputId,
errorMessage,
description: props.description,
});

const { accessibleErrorProps, errorMessageProps } = createAccessibleErrorMessageProps({
inputId,
errorMessage,
});

const { incrementButtonProps, decrementButtonProps, increment, decrement, spinButtonProps, applyClamp } =
useSpinButton({
current: fieldValue,
Expand Down Expand Up @@ -172,15 +177,15 @@ export function useNumberField(
{
...propsToValues(props, ['name', 'placeholder', 'required', 'readonly', 'disabled']),
...labelledByProps.value,
...describedByProps.value,
...accessibleErrorProps.value,
...handlers,
onKeydown: spinButtonProps.value.onKeydown,
id: inputId,
inputmode: inputMode.value,
value: formattedText.value,
max: toValue(props.max),
min: toValue(props.min),
'aria-describedby': describedBy(),
'aria-invalid': errorMessage.value ? true : undefined,
type: 'text',
spellcheck: false,
},
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/useRadio/useRadioGroup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { RadioProps, useRadio } from './useRadio';
import { fireEvent, render, screen } from '@testing-library/vue';
import { axe } from 'vitest-axe';
import { describe } from 'vitest';
import { flush } from '@test-utils/flush';

const createGroup = (props: RadioGroupProps): Component => {
return defineComponent({
Expand Down Expand Up @@ -370,3 +371,29 @@ describe('Arrow keys behavior', () => {
expect(screen.getByTestId('value')).toHaveTextContent('');
});
});

describe('validation', () => {
test('picks up native error messages', async () => {
const RadioGroup = createGroup({ label: 'Group', required: true });
const RadioInput = createRadio();

await render({
components: { RadioGroup, RadioInput },
template: `
<RadioGroup data-testid="fixture">
<RadioInput label="First" value="1" />
<RadioInput label="Second" value="2" />
<RadioInput label="Third" value="3" />
</RadioGroup>
`,
});

await fireEvent.invalid(screen.getByLabelText('First'));
await flush();
expect(screen.getByLabelText('Group')).toHaveErrorMessage('Constraints not satisfied');

vi.useRealTimers();
expect(await axe(screen.getByTestId('fixture'))).toHaveNoViolations();
vi.useFakeTimers();
});
});
23 changes: 17 additions & 6 deletions packages/core/src/useRadio/useRadioGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ import {
Arrayable,
TypedSchema,
} from '../types';
import { useUniqId, createDescribedByProps, getNextCycleArrIdx, normalizeProps, isEmpty } from '../utils/common';
import {
useUniqId,
createDescribedByProps,
getNextCycleArrIdx,
normalizeProps,
isEmpty,
createAccessibleErrorMessageProps,
} from '../utils/common';
import { useLocale } from '../i18n/useLocale';
import { useFormField } from '../useFormField';
import { FieldTypePrefixes } from '../constants';
Expand Down Expand Up @@ -97,14 +104,18 @@ export function useRadioGroup<TValue = string>(_props: Reactivify<RadioGroupProp

const { validityDetails } = useInputValidity({ field });
const { displayError } = useErrorDisplay(field);
const { fieldValue, setValue, isValid, isTouched, setTouched, errorMessage, errors } = field;
const { fieldValue, setValue, isTouched, setTouched, errorMessage, errors } = field;

const { describedBy, descriptionProps, errorMessageProps } = createDescribedByProps({
const { descriptionProps, describedByProps } = createDescribedByProps({
inputId: groupId,
errorMessage,
description: props.description,
});

const { accessibleErrorProps, errorMessageProps } = createAccessibleErrorMessageProps({
inputId: groupId,
errorMessage,
});

function handleArrowNext() {
const currentIdx = radios.findIndex(radio => radio.isChecked());
if (currentIdx < 0) {
Expand Down Expand Up @@ -132,10 +143,10 @@ export function useRadioGroup<TValue = string>(_props: Reactivify<RadioGroupProp
const groupProps = computed<RadioGroupDomProps>(() => {
return {
...labelledByProps.value,
...describedByProps.value,
...accessibleErrorProps.value,
dir: toValue(props.dir) ?? direction.value,
role: 'radiogroup',
'aria-describedby': describedBy(),
'aria-invalid': !isValid.value ? true : undefined,
onKeydown(e: KeyboardEvent) {
if (toValue(props.disabled)) {
return;
Expand Down
54 changes: 48 additions & 6 deletions packages/core/src/useSearchField/useSearchField.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ test('should not have a11y errors with labels or descriptions', async () => {
<div data-testid="fixture">
<label v-bind="labelProps">{{ label }}</label>
<input v-bind="inputProps" />
<span v-bind="descriptionProps" class="error-message">description</span>
<span v-bind="descriptionProps">description</span>
</div>
`,
});
Expand Down Expand Up @@ -61,7 +61,7 @@ test('Enter key submit the value using the onSubmit prop', async () => {
<div data-testid="fixture">
<label v-bind="labelProps">{{ label }}</label>
<input v-bind="inputProps" />
<span v-bind="descriptionProps" class="error-message">description</span>
<span v-bind="descriptionProps">description</span>
</div>
`,
});
Expand Down Expand Up @@ -98,7 +98,7 @@ test('blur sets touched to true', async () => {
<div data-testid="fixture" :class="{ 'touched': isTouched }">
<label v-bind="labelProps">{{ label }}</label>
<input v-bind="inputProps" />
<span v-bind="descriptionProps" class="error-message">description</span>
<span v-bind="descriptionProps">description</span>
</div>
`,
});
Expand Down Expand Up @@ -132,7 +132,7 @@ test('Escape key clears the value', async () => {
<div data-testid="fixture">
<label v-bind="labelProps">{{ label }}</label>
<input v-bind="inputProps" />
<span v-bind="descriptionProps" class="error-message">description</span>
<span v-bind="descriptionProps">description</span>
</div>
`,
});
Expand Down Expand Up @@ -169,7 +169,7 @@ test('Can have a clear button that clears the value', async () => {
<div data-testid="fixture">
<label v-bind="labelProps">{{ label }}</label>
<input v-bind="inputProps" />
<span v-bind="descriptionProps" class="error-message">description</span>
<span v-bind="descriptionProps">description</span>
<button v-bind="clearBtnProps">Clear</button>
</div>
`,
Expand Down Expand Up @@ -206,7 +206,7 @@ test('change event updates the value', async () => {
<div data-testid="fixture">
<label v-bind="labelProps">{{ label }}</label>
<input v-bind="inputProps" />
<span v-bind="descriptionProps" class="error-message">description</span>
<span v-bind="descriptionProps">description</span>
</div>
`,
});
Expand All @@ -216,3 +216,45 @@ test('change event updates the value', async () => {
await fireEvent.change(screen.getByLabelText(label), { target: { value } });
expect(screen.getByLabelText(label)).toHaveDisplayValue(value);
});

test('picks up native error messages', async () => {
const label = 'Search';

await render({
setup() {
const description = 'Search for the thing';
const { inputProps, descriptionProps, labelProps, errorMessageProps, errorMessage } = useSearchField({
label,
description,
required: true,
});

return {
inputProps,
descriptionProps,
labelProps,
label,
description,
errorMessageProps,
errorMessage,
};
},
template: `
<div data-testid="fixture">
<label v-bind="labelProps">{{ label }}</label>
<input v-bind="inputProps" />
<span v-bind="descriptionProps">description</span>
<span v-bind="errorMessageProps">{{errorMessage}}</span>
</div>
`,
});

await fireEvent.invalid(screen.getByLabelText(label));
await flush();
expect(screen.getByLabelText(label)).toHaveErrorMessage('Constraints not satisfied');

vi.useRealTimers();
expect(await axe(screen.getByTestId('fixture'))).toHaveNoViolations();
vi.useFakeTimers();
});
21 changes: 16 additions & 5 deletions packages/core/src/useSearchField/useSearchField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import {
TextInputBaseAttributes,
TypedSchema,
} from '../types';
import { createDescribedByProps, normalizeProps, propsToValues, useUniqId, withRefCapture } from '../utils/common';
import {
createAccessibleErrorMessageProps,
createDescribedByProps,
normalizeProps,
propsToValues,
useUniqId,
withRefCapture,
} from '../utils/common';
import { useInputValidity } from '../validation/useInputValidity';
import { useLabel } from '../a11y/useLabel';
import { useFormField } from '../useFormField';
Expand Down Expand Up @@ -74,12 +81,16 @@ export function useSearchField(
targetRef: inputRef,
});

const { errorMessageProps, descriptionProps, describedBy } = createDescribedByProps({
const { descriptionProps, describedByProps } = createDescribedByProps({
inputId,
errorMessage,
description: props.description,
});

const { accessibleErrorProps, errorMessageProps } = createAccessibleErrorMessageProps({
inputId,
errorMessage,
});

const clearBtnProps = {
tabindex: '-1',
type: 'button' as const,
Expand Down Expand Up @@ -124,13 +135,13 @@ export function useSearchField(
{
...propsToValues(props, ['name', 'pattern', 'placeholder', 'required', 'readonly', 'disabled']),
...labelledByProps.value,
...describedByProps.value,
...accessibleErrorProps.value,
id: inputId,
value: fieldValue.value,
type: 'search',
maxlength: toValue(props.maxLength),
minlength: toValue(props.minLength),
'aria-describedby': describedBy(),
'aria-invalid': errorMessage.value ? true : undefined,
...handlers,
},
inputRef,
Expand Down
Loading

0 comments on commit 9dd8312

Please sign in to comment.