Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
Comment thread
dmytrokirpa marked this conversation as resolved.
"type": "none",
"comment": "test: add axe accessibility checks to component conformance tests",
"packageName": "@fluentui/react-headless-components-preview",
"email": "dmytrokirpa@microsoft.com",
"dependentChangeType": "none"
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe('Avatar', () => {
isConformant({
Component: Avatar,
displayName: 'Avatar',
requiredProps: { name: 'John Doe' },
});

it('renders a default state', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe('AvatarGroupItem', () => {
isConformant({
Component: AvatarGroupItem,
displayName: 'AvatarGroupItem',
requiredProps: { name: 'John Doe' },
disabledTests: ['has-top-level-file-extra', 'component-has-root-ref'],
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe('Button', () => {
isConformant({
Component: Button,
displayName: 'Button',
requiredProps: { children: 'Button' },
});

it('renders a default state', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe('Checkbox', () => {
Component: Checkbox,
displayName: 'Checkbox',
primarySlot: 'input',
requiredProps: { label: 'Checkbox' },
});

it('renders a default state', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe('AlphaSlider', () => {
displayName: 'AlphaSlider',
disabledTests: ['has-top-level-file-extra'],
primarySlot: 'input',
requiredProps: { 'aria-label': 'Alpha' },
});

it('renders opacity as a native range value', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ describe('ColorArea', () => {
isConformant({
Component: ColorArea,
displayName: 'ColorArea',
requiredProps: {
inputX: { 'aria-label': 'Saturation' },
inputY: { 'aria-label': 'Value' },
},
disabledTests: ['has-top-level-file-extra'],
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe('ColorSlider', () => {
displayName: 'ColorSlider',
disabledTests: ['has-top-level-file-extra'],
primarySlot: 'input',
requiredProps: { 'aria-label': 'Color' },
});

it('renders a native range input with channel semantics', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe('Combobox', () => {
displayName: 'Combobox',
primarySlot: 'input',
requiredProps: {
'aria-label': 'Options',
open: true,
children: (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe('CompoundButton', () => {
isConformant({
Component: CompoundButton,
displayName: 'CompoundButton',
requiredProps: { children: 'Action' },
});

it('renders a button with primary and secondary text in its accessible name', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe('Dropdown', () => {
displayName: 'Dropdown',
primarySlot: 'button',
requiredProps: {
'aria-label': 'Options',
open: true,
children: (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe('Image', () => {
isConformant({
Component: Image,
displayName: 'Image',
requiredProps: { alt: 'Image' },
});

it('renders an image element with the correct attributes', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe('Input', () => {
Component: Input,
displayName: 'Input',
primarySlot: 'input',
requiredProps: { 'aria-label': 'Text' },
});

it('renders a default state', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe('Link', () => {
isConformant({
Component: Link,
displayName: 'Link',
requiredProps: { children: 'Link' },
});

it('renders as a button when no href is provided', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe('MenuButton', () => {
isConformant({
Component: MenuButton,
displayName: 'MenuButton',
requiredProps: { children: 'Open menu' },
});

it('renders a default state without a default menu icon', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe('Persona', () => {
isConformant({
Component: Persona,
displayName: 'Persona',
requiredProps: { name: 'User' },
});

it('renders a default state', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe('ProgressBar', () => {
isConformant({
Component: ProgressBar,
displayName: 'ProgressBar',
requiredProps: { 'aria-label': 'Progress' },
});

it('renders a default state', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe('RatingDisplay', () => {
isConformant({
Component: RatingDisplay,
displayName: 'RatingDisplay',
requiredProps: { 'aria-label': 'Rating' },
});

it('renders a default state', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe('SearchBox', () => {
Component: SearchBox,
displayName: 'SearchBox',
primarySlot: 'input',
requiredProps: { 'aria-label': 'Search' },
});

it('renders a default state', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe('Select', () => {
Component: Select,
displayName: 'Select',
primarySlot: 'select',
requiredProps: { 'aria-label': 'Options' },
});

it('renders a default state', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe('Skeleton', () => {
isConformant({
Component: Skeleton,
displayName: 'Skeleton',
requiredProps: { 'aria-label': 'Loading' },
});

it('renders a default state', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe('Slider', () => {
Component: Slider,
displayName: 'Slider',
primarySlot: 'input',
requiredProps: { 'aria-label': 'Value' },
});

it('renders a default state', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe('SpinButton', () => {
Component: SpinButton,
displayName: 'SpinButton',
primarySlot: 'input',
requiredProps: { 'aria-label': 'Value' },
});

it('renders a default state', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe('Spinner', () => {
isConformant({
Component: Spinner,
displayName: 'Spinner',
requiredProps: { 'aria-label': 'Loading' },
});

it('renders a default state', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe('SplitButton', () => {
isConformant({
Component: SplitButton,
displayName: 'SplitButton',
requiredProps: { children: 'Action' },
});

it('renders both the primary action button and the menu button', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ describe('ColorSwatch', () => {
isConformant({
Component: ColorSwatch,
displayName: 'ColorSwatch',
requiredProps: { color: '#f09', value: 'pink', 'aria-label': 'Pink' },
renderOptions: {
wrapper: ({ children }) => <SwatchPicker aria-label="Colors">{children}</SwatchPicker>,
},
getTargetElement: result => result.getByRole('radio'),
disabledTests: ['has-top-level-file-extra'],
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { isConformant } from '../../../testing/isConformant';
import { EmptySwatch } from './EmptySwatch';

describe('EmptySwatch', () => {
isConformant({ Component: EmptySwatch, displayName: 'EmptySwatch', disabledTests: ['has-top-level-file-extra'] });
isConformant({
Component: EmptySwatch,
displayName: 'EmptySwatch',
requiredProps: { 'aria-label': 'Empty' },
disabledTests: ['has-top-level-file-extra'],
});

it('renders a native radio swatch with state attributes', () => {
const swatch = render(<EmptySwatch aria-label="Empty" disabled />).getByRole('radio');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@ import { ImageSwatch } from './ImageSwatch';
import { SwatchPicker } from '../SwatchPicker';

describe('ImageSwatch', () => {
isConformant({ Component: ImageSwatch, displayName: 'ImageSwatch', disabledTests: ['has-top-level-file-extra'] });
isConformant({
Component: ImageSwatch,
displayName: 'ImageSwatch',
requiredProps: { src: 'image.png', value: 'image', 'aria-label': 'Image' },
renderOptions: {
wrapper: ({ children }) => <SwatchPicker aria-label="Images">{children}</SwatchPicker>,
},
getTargetElement: result => result.getByRole('radio'),
disabledTests: ['has-top-level-file-extra'],
});

it('renders an image swatch with its selection state', () => {
const { getByRole } = render(<ImageSwatch src="image.png" value="image" aria-label="Image" />, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,29 @@ import * as React from 'react';
import { render } from '@testing-library/react';
import { isConformant } from '../../../testing/isConformant';
import { SwatchPickerRow } from './SwatchPickerRow';
import { ColorSwatch } from '../ColorSwatch/ColorSwatch';
import { SwatchPicker } from '../SwatchPicker';

describe('SwatchPickerRow', () => {
isConformant({
Component: SwatchPickerRow,
displayName: 'SwatchPickerRow',
requiredProps: {
children: (
<>
<ColorSwatch color="#f09" value="pink" aria-label="Pink" />
<ColorSwatch color="#09f" value="blue" aria-label="Blue" />
</>
),
},
renderOptions: {
wrapper: ({ children }) => (
<SwatchPicker layout="grid" aria-label="Color Picker">
{children}
</SwatchPicker>
),
},
getTargetElement: result => result.getByRole('row'),
disabledTests: ['has-top-level-file-extra'],
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe('Switch', () => {
Component: Switch,
displayName: 'Switch',
primarySlot: 'input',
requiredProps: { label: 'Switch' },
});

it('renders a default state', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe('Textarea', () => {
Component: Textarea,
displayName: 'Textarea',
primarySlot: 'textarea',
requiredProps: { 'aria-label': 'Text' },
});

it('renders a default state', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ describe('ToastContainer', () => {
Component: ToastContainer,
displayName: 'ToastContainer',
requiredProps: defaultToastContainerProps,
axeRenderOptions: {
// ToastContainer is a list of toasts, so we need to wrap it in a list for accessibility.
wrapper: ({ children }) => <div role="list">{children}</div>,
},
disabledTests: [
// Callback argument signature includes toast metadata from ToastData.
'consistent-callback-args',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe('ToggleButton', () => {
isConformant({
Component: ToggleButton,
displayName: 'ToggleButton',
requiredProps: { children: 'Toggle' },
});

it('renders a default state', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
import * as React from 'react';
import { render, type RenderOptions } from '@testing-library/react';
import { isConformant as baseIsConformant } from '@fluentui/react-conformance';
import type { IsConformantOptions } from '@fluentui/react-conformance';
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

type HeadlessIsConformantOptions<TProps> = Omit<IsConformantOptions<TProps>, 'componentPath'> & {
/**
* Path to component file. This is optional because the test file is usually in the same folder as the component file.
*/
componentPath?: string;
/**
* Custom render options applied only to the axe test.
*/
axeRenderOptions?: RenderOptions;
};

function kebabCase(str: string): string {
return str
Expand All @@ -8,9 +24,8 @@ function kebabCase(str: string): string {
.toLowerCase();
}

export function isConformant<TProps = {}>(
testInfo: Omit<IsConformantOptions<TProps>, 'componentPath'> & { componentPath?: string },
): void {
export function isConformant<TProps = {}>(testInfo: HeadlessIsConformantOptions<TProps>): void {
const { axeRenderOptions, ...baseTestInfo } = testInfo;
const name = kebabCase(testInfo.displayName);

const defaultOptions: Partial<IsConformantOptions<TProps>> = {
Expand All @@ -26,6 +41,16 @@ export function isConformant<TProps = {}>(
],
disableTypeTests: true,
extraTests: {
'component-has-no-axe-violations': ({ Component, requiredProps, renderOptions }: IsConformantOptions<TProps>) => {
it('has no axe violations (component-has-no-axe-violations)', async () => {
const { container } = render(
React.createElement(Component as React.ComponentType<Partial<TProps>>, requiredProps),
{ ...renderOptions, ...axeRenderOptions },
);

expect(await axe(container)).toHaveNoViolations();
});
},
'has-top-level-file-extra': ({ displayName, Component }: IsConformantOptions<TProps>) => {
it(`has corresponding top-level file 'src/${name}.ts' (has-top-level-file)`, () => {
const topLevelFile = require(`../${name}.ts`);
Expand Down Expand Up @@ -53,5 +78,5 @@ export function isConformant<TProps = {}>(
},
};

baseIsConformant(defaultOptions, testInfo);
baseIsConformant(defaultOptions, baseTestInfo);
}
Loading