diff --git a/e2e/components/all.test.ts b/e2e/components/all.test.ts new file mode 100644 index 00000000000..f8a8125d867 --- /dev/null +++ b/e2e/components/all.test.ts @@ -0,0 +1,27 @@ +import {test, expect} from '@playwright/test' +import {stories} from '../../packages/react/src/utils/testing' +import {visit} from '../test-helpers/storybook' +import {themes} from '../test-helpers/themes' + +test.describe('axe @aat', () => { + for (const {story} of stories) { + for (const storyName of Object.keys(story)) { + for (const theme of themes) { + test.describe(theme, () => { + test('axe @aat', async ({page}) => { + await visit(page, { + id: `${story.default.title.replace('/', '-').toLowerCase()}--${storyName + .split(/(?=[A-Z])/) + .join('-') + .toLowerCase()}`, + globals: { + colorScheme: theme, + }, + }) + await expect(page).toHaveNoViolations() + }) + }) + } + } + } +}) diff --git a/packages/react/src/Button/ButtonBase.tsx b/packages/react/src/Button/ButtonBase.tsx index 3ef7ae902b1..f9f580707e0 100644 --- a/packages/react/src/Button/ButtonBase.tsx +++ b/packages/react/src/Button/ButtonBase.tsx @@ -72,7 +72,8 @@ const ButtonBase = forwardRef( if ( innerRef.current && !(innerRef.current instanceof HTMLButtonElement) && - !((innerRef.current as unknown) instanceof HTMLAnchorElement) + !((innerRef.current as unknown) instanceof HTMLAnchorElement) && + !((innerRef.current as HTMLElement).tagName === 'SUMMARY') ) { // eslint-disable-next-line no-console console.warn('This component should be an instanceof a semantic button or anchor') diff --git a/packages/react/src/__tests__/storybook.test.tsx b/packages/react/src/__tests__/storybook.test.tsx index d0bc7d1a7b7..63935d37cd7 100644 --- a/packages/react/src/__tests__/storybook.test.tsx +++ b/packages/react/src/__tests__/storybook.test.tsx @@ -1,80 +1,9 @@ import glob from 'fast-glob' +import {stories, ROOT_DIRECTORY, allowlist} from '../utils/testing' import groupBy from 'lodash.groupby' import fs from 'node:fs' import path from 'node:path' -const ROOT_DIRECTORY = path.resolve(__dirname, '..', '..') -// Components opted into the new story format -// TODO: Remove this allowlist when all components use the new story format -const allowlist = [ - 'ActionList', - 'ActionMenu', - 'AnchoredOverlay', - 'Autocomplete', - 'Avatar', - 'AvatarStack', - 'AvatarPair', - 'Breadcrumbs', - 'BranchName', - 'Blankslate', - 'Box', - 'Button', - 'Checkbox', - 'CheckboxGroup', - 'ConfirmationDialog', - 'CounterLabel', - 'DataTable', - 'Details', - 'Dialog', - 'Flash', - 'FormControl', - 'Header', - 'Heading', - 'IconButton', - 'FilteredActionList', - 'Link', - 'Octicon', - 'Pagehead', - 'Pagination', - 'ProgressBar', - 'Radio', - 'RadioGroup', - 'RelativeTime', - 'Select', - 'SegmentedControl', - 'Spinner', - 'StateLabel', - 'SubNav', - 'TabNav', - 'Textarea', - 'TextInput', - 'TextInputWithTokens', - 'Tooltip', - 'TreeView', - 'Timeline', - 'ToggleSwitch', - 'Token', - 'UnderlineNav2', -] -const stories = glob - .sync('src/**/*.stories.tsx', { - cwd: ROOT_DIRECTORY, - }) - // Filter out deprecated stories - .filter(file => !file.includes('deprecated')) - .filter(file => - allowlist.some( - component => file.includes(`/${component}.stories.tsx`) || file.includes(`/${component}.features.stories.tsx`), - ), - ) - .map(file => { - const filepath = path.join(ROOT_DIRECTORY, file) - const type = path.basename(filepath, '.stories.tsx').endsWith('features') ? 'feature' : 'default' - const name = type === 'feature' ? path.basename(file, '.features.stories.tsx') : path.basename(file, '.stories.tsx') - - return {name, story: require(filepath), type, relativeFilepath: path.relative(ROOT_DIRECTORY, filepath)} - }) - const components = Object.entries( groupBy(stories, ({name}) => { return name diff --git a/packages/react/src/utils/testing.tsx b/packages/react/src/utils/testing.tsx index a01cb435dec..b9047bca7da 100644 --- a/packages/react/src/utils/testing.tsx +++ b/packages/react/src/utils/testing.tsx @@ -8,6 +8,8 @@ import customRules from '@github/axe-github' import {ThemeProvider} from '..' import {default as defaultTheme} from '../theme' import type {LiveRegionElement} from '@primer/live-region-element' +import path from 'path' +import {glob} from 'fast-glob' type ComputedStyles = Record> @@ -16,6 +18,80 @@ const readFile = promisify(require('fs').readFile) export const COMPONENT_DISPLAY_NAME_REGEX = /^[A-Z][A-Za-z]+(\.[A-Z][A-Za-z]+)*$/ +export const ROOT_DIRECTORY = path.resolve(__dirname, '..', '..') + +// Components opted into the new story format +// TODO: Remove this allowlist when all components use the new story format +export const allowlist = [ + 'ActionList', + 'ActionMenu', + 'AnchoredOverlay', + 'Autocomplete', + 'Avatar', + 'AvatarStack', + 'AvatarPair', + 'Breadcrumbs', + 'BranchName', + 'Blankslate', + 'Box', + 'Button', + 'Checkbox', + 'CheckboxGroup', + 'ConfirmationDialog', + 'CounterLabel', + 'DataTable', + 'Details', + 'Dialog', + 'Flash', + 'FormControl', + 'Header', + 'Heading', + 'IconButton', + 'FilteredActionList', + 'Link', + 'Octicon', + 'Pagehead', + 'Pagination', + 'ProgressBar', + 'Radio', + 'RadioGroup', + 'RelativeTime', + 'Select', + 'SegmentedControl', + 'Spinner', + 'StateLabel', + 'SubNav', + 'TabNav', + 'Textarea', + 'TextInput', + 'TextInputWithTokens', + 'Tooltip', + 'TreeView', + 'Timeline', + 'ToggleSwitch', + 'Token', + 'UnderlineNav2', +] + +export const stories = glob + .sync('src/**/*.stories.tsx', { + cwd: ROOT_DIRECTORY, + }) + // Filter out deprecated stories + .filter(file => !file.includes('deprecated')) + .filter(file => + allowlist.some( + component => file.includes(`/${component}.stories.tsx`) || file.includes(`/${component}.features.stories.tsx`), + ), + ) + .map(file => { + const filepath = path.join(ROOT_DIRECTORY, file) + const type = path.basename(filepath, '.stories.tsx').endsWith('features') ? 'feature' : 'default' + const name = type === 'feature' ? path.basename(file, '.features.stories.tsx') : path.basename(file, '.stories.tsx') + + return {name, story: require(filepath), type, relativeFilepath: path.relative(ROOT_DIRECTORY, filepath)} + }) + declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace jest {