diff --git a/change/@fluentui-web-components-c7f76868-02ae-4651-82cc-a43a6f56208c.json b/change/@fluentui-web-components-c7f76868-02ae-4651-82cc-a43a6f56208c.json
new file mode 100644
index 0000000000000..57a0d2853b636
--- /dev/null
+++ b/change/@fluentui-web-components-c7f76868-02ae-4651-82cc-a43a6f56208c.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "add axe automated a11y testing",
+ "packageName": "@fluentui/web-components",
+ "email": "machi@microsoft.com",
+ "dependentChangeType": "patch"
+}
diff --git a/package.json b/package.json
index 18cdbba8782a4..9f3adcd2be889 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
"devDependencies": {
"@actions/core": "1.9.1",
"@actions/github": "5.0.3",
+ "@axe-core/playwright": "^4.10.0",
"@babel/core": "7.24.6",
"@babel/generator": "7.24.6",
"@babel/parser": "7.24.6",
diff --git a/packages/web-components/src/accordion/accordion.spec.ts b/packages/web-components/src/accordion/accordion.spec.ts
index 26e9733fbedc4..a46a939257dec 100644
--- a/packages/web-components/src/accordion/accordion.spec.ts
+++ b/packages/web-components/src/accordion/accordion.spec.ts
@@ -1,10 +1,12 @@
import type { Locator } from '@playwright/test';
import { test } from '@playwright/test';
-import { expect, fixtureURL } from '../helpers.tests.js';
+import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js';
+
+const storybookDocId = 'components-accordion--docs';
test.describe('Accordion', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(fixtureURL('components-accordion--accordion'));
+ await page.goto(fixtureURL(storybookDocId));
await page.waitForFunction(() =>
Promise.all([
@@ -435,3 +437,20 @@ test.describe('Accordion', () => {
await expect(item).toHaveAttribute('expanded');
});
});
+
+test('should not have auto detectable accessibility issues', async ({ page }) => {
+ await createElementInternalsTrapsForAxe(page);
+
+ await page.goto(fixtureURL(storybookDocId));
+ await page.waitForFunction(() =>
+ Promise.all([
+ customElements.whenDefined('fluent-accordion'),
+ customElements.whenDefined('fluent-accordion-item'),
+ customElements.whenDefined('fluent-checkbox'),
+ ]),
+ );
+
+ const results = await analyzePageWithAxe(page);
+
+ expect(results.violations).toEqual([]);
+});
diff --git a/packages/web-components/src/anchor-button/anchor-button.spec.ts b/packages/web-components/src/anchor-button/anchor-button.spec.ts
index 0c02e9a52d3cf..5760d4d06e619 100644
--- a/packages/web-components/src/anchor-button/anchor-button.spec.ts
+++ b/packages/web-components/src/anchor-button/anchor-button.spec.ts
@@ -1,6 +1,8 @@
import { spinalCase } from '@microsoft/fast-web-utilities';
import { test } from '@playwright/test';
-import { expect, fixtureURL } from '../helpers.tests.js';
+import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js';
+
+const storybookDocId = 'components-button-anchor--docs';
const proxyAttributes = {
href: 'href',
@@ -27,7 +29,7 @@ const booleanAttributes = {
test.describe('Anchor Button', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(fixtureURL('components-button-anchor--anchor-button'));
+ await page.goto(fixtureURL(storybookDocId));
await page.waitForFunction(() => customElements.whenDefined('fluent-anchor-button'));
});
@@ -143,3 +145,14 @@ test.describe('Anchor Button', () => {
expect(newPage.url()).toContain(expectedUrl);
});
});
+
+test('should not have auto detectable accessibility issues', async ({ page }) => {
+ await createElementInternalsTrapsForAxe(page);
+
+ await page.goto(fixtureURL(storybookDocId));
+ await page.waitForFunction(() => customElements.whenDefined('fluent-anchor-button'));
+
+ const results = await analyzePageWithAxe(page);
+
+ expect(results.violations).toEqual([]);
+});
diff --git a/packages/web-components/src/avatar/avatar.spec.ts b/packages/web-components/src/avatar/avatar.spec.ts
index 18e38a1bb38e4..3473dda59c6c0 100644
--- a/packages/web-components/src/avatar/avatar.spec.ts
+++ b/packages/web-components/src/avatar/avatar.spec.ts
@@ -1,11 +1,13 @@
import { test } from '@playwright/test';
-import { expect, fixtureURL } from '../helpers.tests.js';
+import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js';
import type { Avatar } from './avatar.js';
-import { AvatarAppearance, AvatarColor, AvatarSize } from './avatar.options.js';
+import type { AvatarAppearance, AvatarColor, AvatarSize } from './avatar.options.js';
+
+const storybookDocId = 'components-avatar--docs';
test.describe('Avatar Component', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(fixtureURL('components-avatar--image'));
+ await page.goto(fixtureURL(storybookDocId));
await page.waitForFunction(() => customElements.whenDefined('fluent-avatar'));
});
@@ -81,6 +83,10 @@ test.describe('Avatar Component', () => {
test('should have a role of img', async ({ page }) => {
const element = page.locator('fluent-avatar');
+ await page.setContent(/* html */ `
+
+ `);
+
await expect(element).toHaveJSProperty('elementInternals.role', 'img');
});
@@ -119,6 +125,10 @@ test.describe('Avatar Component', () => {
test('should render correctly in active state', async ({ page }) => {
const element = page.locator('fluent-avatar');
+ await page.setContent(/* html */ `
+
+ `);
+
await element.evaluate((node: Avatar) => {
node.active = 'active';
});
@@ -129,6 +139,10 @@ test.describe('Avatar Component', () => {
test('should render correctly in inactive state', async ({ page }) => {
const element = page.locator('fluent-avatar');
+ await page.setContent(/* html */ `
+
+ `);
+
await element.evaluate((node: Avatar) => {
node.active = 'inactive';
});
@@ -139,6 +153,10 @@ test.describe('Avatar Component', () => {
test('default color should be neutral', async ({ page }) => {
const element = page.locator('fluent-avatar');
+ await page.setContent(/* html */ `
+
+ `);
+
await expect(element).toHaveCustomState('neutral');
});
@@ -165,6 +183,10 @@ test.describe('Avatar Component', () => {
test(`should set the color attribute on the internal control`, async ({ page }) => {
const element = page.locator('fluent-avatar');
+ await page.setContent(/* html */ `
+
+ `);
+
for (const [, value] of Object.entries(colorAttributes)) {
await test.step(value, async () => {
await element.evaluate((node: Avatar, colorValue: string) => {
@@ -181,6 +203,10 @@ test.describe('Avatar Component', () => {
test(`should set the size attribute on the internal control`, async ({ page }) => {
const element = page.locator('fluent-avatar');
+ await page.setContent(/* html */ `
+
+ `);
+
for (const [, value] of Object.entries(sizeAttributes)) {
await test.step(`${value}`, async () => {
await element.evaluate((node: Avatar, sizeValue: number) => {
@@ -197,6 +223,10 @@ test.describe('Avatar Component', () => {
test(`should set and reflect the appearance attribute on the internal control`, async ({ page }) => {
const element = page.locator('fluent-avatar');
+ await page.setContent(/* html */ `
+
+ `);
+
for (const [, value] of Object.entries(appearanceAttributes)) {
await test.step(value, async () => {
await element.evaluate((node: Avatar, appearanceValue: string) => {
@@ -209,3 +239,14 @@ test.describe('Avatar Component', () => {
}
});
});
+
+test('should not have auto detectable accessibility issues', async ({ page }) => {
+ await createElementInternalsTrapsForAxe(page);
+
+ await page.goto(fixtureURL(storybookDocId));
+ await page.waitForFunction(() => customElements.whenDefined('fluent-avatar'));
+
+ const results = await analyzePageWithAxe(page);
+
+ expect(results.violations).toEqual([]);
+});
diff --git a/packages/web-components/src/avatar/avatar.stories.ts b/packages/web-components/src/avatar/avatar.stories.ts
index 20f842e677c4b..09c97fb887535 100644
--- a/packages/web-components/src/avatar/avatar.stories.ts
+++ b/packages/web-components/src/avatar/avatar.stories.ts
@@ -14,6 +14,7 @@ const storyTemplate = html>`
size="${story => story.size}"
initials="${story => story.initials}"
name="${story => story.name}"
+ aria-label="avatar"
>
${story => story.slottedContent?.()} ${story => story.badgeSlottedContent?.()}
@@ -102,7 +103,7 @@ export default {
export const Default: Story = {};
export const Image: Story = {
- render: renderComponent(html>`
+ render: renderComponent(html>`
>`
-
+
`,
titleActionSlottedContent: () => html`
- ${dismissed20Regular}
+
+ ${dismissed20Regular}
+
`,
titleSlottedContent: () => html` Actions
`,
},
@@ -165,6 +171,7 @@ export const CustomTitleAction: Story = {
appearance="transparent"
icon-only
@click="${() => alert('This is a custom action')}"
+ aria-label="Dismiss"
>
${dismissCircle20Regular}
diff --git a/packages/web-components/src/dialog-body/dialog-body.template.ts b/packages/web-components/src/dialog-body/dialog-body.template.ts
index f8d636956d90f..b6e158030eac5 100644
--- a/packages/web-components/src/dialog-body/dialog-body.template.ts
+++ b/packages/web-components/src/dialog-body/dialog-body.template.ts
@@ -31,6 +31,7 @@ export const template: ElementViewTemplate = html`
class="title-action"
appearance="transparent"
icon-only
+ aria-label="${x => x.titleActionLabel}"
@click=${x => x.parentNode?.hide()}
${ref('defaultTitleAction')}
>
diff --git a/packages/web-components/src/dialog-body/dialog-body.ts b/packages/web-components/src/dialog-body/dialog-body.ts
index eeb70e03c5eae..012818793db79 100644
--- a/packages/web-components/src/dialog-body/dialog-body.ts
+++ b/packages/web-components/src/dialog-body/dialog-body.ts
@@ -7,9 +7,16 @@ import { attr, FASTElement } from '@microsoft/fast-element';
*/
export class DialogBody extends FASTElement {
/**
- * @public
* Indicates whether the dialog has a title action
+ * @public
*/
@attr({ mode: 'boolean', attribute: 'no-title-action' })
public noTitleAction: boolean = false;
+
+ /**
+ * ARIA label for the default title action button.
+ * @public
+ */
+ @attr({ attribute: 'title-action-label' })
+ public titleActionLabel = '';
}
diff --git a/packages/web-components/src/dialog/dialog.spec.ts b/packages/web-components/src/dialog/dialog.spec.ts
index 15d2ec0ae824d..949f9a3bc914c 100644
--- a/packages/web-components/src/dialog/dialog.spec.ts
+++ b/packages/web-components/src/dialog/dialog.spec.ts
@@ -1,7 +1,9 @@
-import { expect, test } from '@playwright/test';
+import { test } from '@playwright/test';
import type { Locator } from '@playwright/test';
-import { fixtureURL } from '../helpers.tests.js';
-import { Dialog } from './dialog.js';
+import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js';
+import type { Dialog } from './dialog.js';
+
+const storybookDocId = 'components-dialog-dialog--docs';
async function getPointOutside(element: Locator) {
// Get the bounding box of the element
@@ -16,7 +18,7 @@ async function getPointOutside(element: Locator) {
test.describe('Dialog', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(fixtureURL('components-dialog-dialog--default'));
+ await page.goto(fixtureURL(storybookDocId));
await page.waitForFunction(() =>
Promise.all([
@@ -237,3 +239,20 @@ test.describe('Dialog', () => {
});
});
});
+
+test('should not have auto detectable accessibility issues', async ({ page }) => {
+ await createElementInternalsTrapsForAxe(page);
+
+ await page.goto(fixtureURL(storybookDocId));
+ await page.waitForFunction(() =>
+ Promise.all([
+ customElements.whenDefined('fluent-button'),
+ customElements.whenDefined('fluent-dialog'),
+ customElements.whenDefined('fluent-dialog-body'),
+ ]),
+ );
+
+ const results = await analyzePageWithAxe(page);
+
+ expect(results.violations).toEqual([]);
+});
diff --git a/packages/web-components/src/divider/divider.spec.ts b/packages/web-components/src/divider/divider.spec.ts
index b4e4d237f57e1..15d9dda5e84b5 100644
--- a/packages/web-components/src/divider/divider.spec.ts
+++ b/packages/web-components/src/divider/divider.spec.ts
@@ -1,10 +1,12 @@
import { test } from '@playwright/test';
-import { expect, fixtureURL } from '../helpers.tests.js';
-import { Divider } from './divider.js';
+import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js';
+import type { Divider } from './divider.js';
+
+const storybookDocId = 'components-divider--docs';
test.describe('Divider', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(fixtureURL('components-divider--divider'));
+ await page.goto(fixtureURL(storybookDocId));
await page.waitForFunction(() => customElements.whenDefined('fluent-divider'));
});
@@ -73,6 +75,10 @@ test.describe('Divider', () => {
test('should add a custom state matching the `orientation` attribute when provided', async ({ page }) => {
const element = page.locator('fluent-divider');
+ await page.setContent(/* html */ `
+
+ `);
+
await element.evaluate((node: Divider) => {
node.orientation = 'vertical';
});
@@ -146,6 +152,10 @@ test.describe('Divider', () => {
test('should add a custom state matching the `appearance` attribute when provided', async ({ page }) => {
const element = page.locator('fluent-divider');
+ await page.setContent(/* html */ `
+
+ `);
+
await element.evaluate((node: Divider) => {
node.appearance = 'strong';
});
@@ -172,6 +182,10 @@ test.describe('Divider', () => {
test('should add a custom state of `inset` when the value is true', async ({ page }) => {
const element = page.locator('fluent-divider');
+ await page.setContent(/* html */ `
+
+ `);
+
await element.evaluate((node: Divider) => {
node.inset = true;
});
@@ -188,6 +202,10 @@ test.describe('Divider', () => {
test('should add a custom state matching the `align-content` attribute value when provided', async ({ page }) => {
const element = page.locator('fluent-divider');
+ await page.setContent(/* html */ `
+
+ `);
+
await element.evaluate((node: Divider) => {
node.alignContent = 'start';
});
@@ -211,3 +229,14 @@ test.describe('Divider', () => {
await expect(element).not.toHaveCustomState('align-end');
});
});
+
+test('should not have auto detectable accessibility issues', async ({ page }) => {
+ await createElementInternalsTrapsForAxe(page);
+
+ await page.goto(fixtureURL(storybookDocId));
+ await page.waitForFunction(() => customElements.whenDefined('fluent-divider'));
+
+ const results = await analyzePageWithAxe(page);
+
+ expect(results.violations).toEqual([]);
+});
diff --git a/packages/web-components/src/drawer/drawer.spec.ts b/packages/web-components/src/drawer/drawer.spec.ts
index 2d167763e7594..8b83150c09282 100644
--- a/packages/web-components/src/drawer/drawer.spec.ts
+++ b/packages/web-components/src/drawer/drawer.spec.ts
@@ -1,11 +1,13 @@
-import { expect, test } from '@playwright/test';
+import { test } from '@playwright/test';
-import { fixtureURL } from '../helpers.tests.js';
+import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js';
import type { Drawer } from './drawer.js';
+const storybookDocId = 'components-drawer--docs';
+
test.describe('Drawer', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(fixtureURL('components-drawer--drawer'));
+ await page.goto(fixtureURL(storybookDocId));
await page.waitForFunction(() => customElements.whenDefined('fluent-drawer'));
});
@@ -205,3 +207,23 @@ test.describe('Drawer', () => {
expect(wasDismissed).toBe(true);
});
});
+
+test('should not have auto detectable accessibility issues', async ({ page }) => {
+ await createElementInternalsTrapsForAxe(page);
+
+ await page.goto(fixtureURL(storybookDocId));
+ await page.waitForFunction(() => customElements.whenDefined('fluent-drawer'));
+
+ const results = await analyzePageWithAxe(page);
+
+ expect(results.violations).toEqual([]);
+
+ const button = page.getByText('Toggle Drawer');
+ const drawer = page.locator('fluent-drawer-body');
+ await button.click();
+ await drawer.waitFor({ state: 'visible' });
+
+ const openedResult = await analyzePageWithAxe(page);
+
+ expect(openedResult.violations).toEqual([]);
+});
diff --git a/packages/web-components/src/drawer/drawer.stories.ts b/packages/web-components/src/drawer/drawer.stories.ts
index 260e400e8907d..596571edec0fa 100644
--- a/packages/web-components/src/drawer/drawer.stories.ts
+++ b/packages/web-components/src/drawer/drawer.stories.ts
@@ -65,6 +65,7 @@ const storyTemplate = html>`
position="${story => story.position}"
size="${story => story.size}"
type="${story => story.type}"
+ dialog-label="Drawer example"
style="${story =>
story['--drawer-width'] !== '' ? `--drawer-width: ${story['--drawer-width']};` : ''} ${story =>
story['--dialog-backdrop'] !== '' ? `--dialog-backdrop: ${story['--dialog-backdrop']};` : ''}"
diff --git a/packages/web-components/src/drawer/drawer.template.ts b/packages/web-components/src/drawer/drawer.template.ts
index b223c8de96911..54de12b3638c4 100644
--- a/packages/web-components/src/drawer/drawer.template.ts
+++ b/packages/web-components/src/drawer/drawer.template.ts
@@ -14,7 +14,7 @@ export function drawerTemplate(): ElementViewTemplate {
aria-modal="${x => (x.type === 'modal' ? 'true' : void 0)}"
aria-describedby="${x => x.ariaDescribedby}"
aria-labelledby="${x => x.ariaLabelledby}"
- aria-label="${x => x.ariaLabel}"
+ aria-label="${x => x.dialogLabel}"
size="${x => x.size}"
position="${x => x.position}"
type="${x => x.type}"
diff --git a/packages/web-components/src/drawer/drawer.ts b/packages/web-components/src/drawer/drawer.ts
index e6b0332ab9ce7..604520876a220 100644
--- a/packages/web-components/src/drawer/drawer.ts
+++ b/packages/web-components/src/drawer/drawer.ts
@@ -38,6 +38,9 @@ export class Drawer extends FASTElement {
public type: DrawerType = DrawerType.modal;
/**
+ * FIXME: this should be deprecated and removed, because it’s not valid to
+ * use `aria-labelledby` and `aria-label` on an element that doesn’t have a
+ * valid labellable role (the host element doesn’t have any role).
* @public
* The ID of the element that labels the drawer.
*/
@@ -51,7 +54,14 @@ export class Drawer extends FASTElement {
@attr({ attribute: 'aria-describedby' })
public ariaDescribedby?: string;
- /**""
+ /**
+ * @public
+ * The accessible label of the dialog.
+ */
+ @attr({ attribute: 'dialog-label' })
+ public dialogLabel = '';
+
+ /**
* @public
* @defaultValue start
* Sets the position of the drawer (start/end).
diff --git a/packages/web-components/src/field/field.spec.ts b/packages/web-components/src/field/field.spec.ts
index d661608b8a2e4..4776bade169ad 100644
--- a/packages/web-components/src/field/field.spec.ts
+++ b/packages/web-components/src/field/field.spec.ts
@@ -1,11 +1,13 @@
import { test } from '@playwright/test';
-import { expect, fixtureURL } from '../helpers.tests.js';
-import { TextInput } from '../index.js';
+import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js';
+import type { TextInput } from '../index.js';
import type { Field } from './field.js';
+const storybookDocId = 'components-field--docs';
+
test.describe('Field', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(fixtureURL('components-field--field'));
+ await page.goto(fixtureURL(storybookDocId));
await page.waitForFunction(() => customElements.whenDefined('fluent-field'));
});
@@ -482,3 +484,18 @@ test.describe('Field', () => {
await expect(element.locator('input:right-of(label)')).toHaveCount(1);
});
});
+
+// FIXME: Should remove all examples of using `` because it’s not
+// currently supported and it’s causing ARIA issues since `` would add
+// `aria-labelledby` on the `` element but it doesn’t have a valid
+// labellable role.
+test.fixme('should not have auto detectable accessibility issues', async ({ page }) => {
+ await createElementInternalsTrapsForAxe(page);
+
+ await page.goto(fixtureURL(storybookDocId));
+ await page.waitForFunction(() => customElements.whenDefined('fluent-field'));
+
+ const results = await analyzePageWithAxe(page);
+
+ expect(results.violations).toEqual([]);
+});
diff --git a/packages/web-components/src/helpers.tests.ts b/packages/web-components/src/helpers.tests.ts
index 46d72c8403a92..b96913328d96f 100644
--- a/packages/web-components/src/helpers.tests.ts
+++ b/packages/web-components/src/helpers.tests.ts
@@ -1,5 +1,6 @@
import qs from 'qs';
-import { expect as baseExpect, type ExpectMatcherState, type Locator } from '@playwright/test';
+import AxeBuilder from '@axe-core/playwright';
+import { expect as baseExpect, type ExpectMatcherState, type Locator, type Page } from '@playwright/test';
/**
* Returns a formatted URL for a given Storybook fixture.
@@ -84,3 +85,81 @@ async function toHaveCustomState(
export const expect = baseExpect.extend({
toHaveCustomState,
});
+
+/**
+ * A helper function to override the built-in `HTMLElement.prototype.attachInternals()`
+ * method, the overridden `attachInternals()` would set ARIA-related attributes
+ * on the host element when they are set on the `ElementInternals` object, so that
+ * Axe is able to assess the element’s accessibility properly.
+ * @see https://github.com/dequelabs/axe-core/issues/4259
+ *
+ * This function should be called before calling `page.goto(..)`. And it should
+ * be contained in 1-2 tests per suite. It shouldn’t be used casually due to the
+ * heavy-handedness of modifying built-in API.
+ */
+export async function createElementInternalsTrapsForAxe(page: Page) {
+ await page.addInitScript(() => {
+ function getAriaAttrName(prop: string | symbol): string | null {
+ return typeof prop === 'string' && (prop === 'role' || prop.startsWith('aria'))
+ ? prop.replace(/(?:aria)(\w+)/, (_, w) => `aria-${w.toLowerCase()}`)
+ : null;
+ }
+
+ const original = HTMLElement.prototype.attachInternals;
+ HTMLElement.prototype.attachInternals = function () {
+ const originalInternals = original.call(this);
+
+ return new Proxy({} as ElementInternals, {
+ get(target, prop) {
+ if (getAriaAttrName(prop)) {
+ return Reflect.get(target, prop) ?? null;
+ }
+
+ const propValue = Reflect.get(originalInternals, prop);
+
+ if (typeof propValue === 'function') {
+ return propValue.bind(originalInternals);
+ }
+
+ return propValue;
+ },
+ set(target, prop, value) {
+ const attrName = getAriaAttrName(prop);
+ if (attrName) {
+ Reflect.set(target, prop, value);
+ const host = originalInternals.shadowRoot?.host;
+ if (value !== null && value !== undefined) {
+ host?.setAttribute(attrName, value.toString());
+ } else {
+ host?.removeAttribute(attrName);
+ }
+ }
+
+ return Reflect.set(originalInternals, prop, value);
+ },
+ });
+ };
+ });
+}
+
+interface AnalyzePageWithAxeOptions {
+ exclude: string[];
+}
+/**
+ * Helper function to run Axe analysis. The main motivation of creating this
+ * function is to centralize the `.include('.sb-story')` call in case Storybook
+ * changes the class name in future.
+ */
+export async function analyzePageWithAxe(
+ page: Page,
+ options?: AnalyzePageWithAxeOptions,
+): Promise> {
+ let builder = new AxeBuilder({ page }).include('.sb-story');
+ if (options?.exclude?.length) {
+ for (const exclude of options.exclude) {
+ builder = builder.exclude(exclude);
+ console.log(builder);
+ }
+ }
+ return await builder.analyze();
+}
diff --git a/packages/web-components/src/image/image.spec.ts b/packages/web-components/src/image/image.spec.ts
index 5ff754a011e07..d3bf50d3e7904 100644
--- a/packages/web-components/src/image/image.spec.ts
+++ b/packages/web-components/src/image/image.spec.ts
@@ -1,10 +1,12 @@
import { test } from '@playwright/test';
-import { expect, fixtureURL } from '../helpers.tests.js';
+import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js';
import type { Image } from './image.js';
+const storybookDocId = 'components-image--docs';
+
test.describe('Image', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(fixtureURL('components-image--image'));
+ await page.goto(fixtureURL(storybookDocId));
await page.waitForFunction(() => customElements.whenDefined('fluent-image'));
});
@@ -30,6 +32,12 @@ test.describe('Image', () => {
test('should add a custom state of `block` when a value of true is provided', async ({ page }) => {
const element = page.locator('fluent-image');
+ await page.setContent(/* html */ `
+
+
+
+ `);
+
await element.evaluate((node: Image) => {
node.block = true;
});
@@ -64,6 +72,12 @@ test.describe('Image', () => {
test('should add a custom state of `bordered` when a value of true is provided', async ({ page }) => {
const element = page.locator('fluent-image');
+ await page.setContent(/* html */ `
+
+
+
+ `);
+
await element.evaluate((node: Image) => {
node.bordered = true;
});
@@ -98,6 +112,12 @@ test.describe('Image', () => {
test('should add a custom state of `shadow` when a value of true is provided', async ({ page }) => {
const element = page.locator('fluent-image');
+ await page.setContent(/* html */ `
+
+
+
+ `);
+
await element.evaluate((node: Image) => {
node.shadow = true;
});
@@ -150,6 +170,12 @@ test.describe('Image', () => {
test('should add a custom state matching the `fit` attribute when provided', async ({ page }) => {
const element = page.locator('fluent-image');
+ await page.setContent(/* html */ `
+
+
+
+ `);
+
await element.evaluate((node: Image) => {
node.fit = 'contain';
});
@@ -200,6 +226,12 @@ test.describe('Image', () => {
test('should add a custom state matching the `shape` attribute when provided', async ({ page }) => {
const element = page.locator('fluent-image');
+ await page.setContent(/* html */ `
+
+
+
+ `);
+
await element.evaluate((node: Image) => {
node.shape = 'circular';
});
@@ -233,3 +265,14 @@ test.describe('Image', () => {
await expect(element).not.toHaveCustomState('circular');
});
});
+
+test('should not have auto detectable accessibility issues', async ({ page }) => {
+ await createElementInternalsTrapsForAxe(page);
+
+ await page.goto(fixtureURL(storybookDocId));
+ await page.waitForFunction(() => customElements.whenDefined('fluent-image'));
+
+ const results = await analyzePageWithAxe(page);
+
+ expect(results.violations).toEqual([]);
+});
diff --git a/packages/web-components/src/link/link.spec.ts b/packages/web-components/src/link/link.spec.ts
index f71399fe13723..86ebe50223906 100644
--- a/packages/web-components/src/link/link.spec.ts
+++ b/packages/web-components/src/link/link.spec.ts
@@ -1,7 +1,9 @@
import { spinalCase } from '@microsoft/fast-web-utilities';
import { test } from '@playwright/test';
-import { expect, fixtureURL } from '../helpers.tests.js';
-import { Link } from './link.js';
+import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js';
+import type { Link } from './link.js';
+
+const storybookDocId = 'components-link--docs';
const proxyAttributes = {
href: 'href',
@@ -21,7 +23,7 @@ const attributes = {
test.describe('Link', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(fixtureURL('components-link--appearance'));
+ await page.goto(fixtureURL(storybookDocId));
await page.waitForFunction(() => customElements.whenDefined('fluent-link'));
});
@@ -60,6 +62,10 @@ test.describe('Link', () => {
test('should add a custom state matching the `appearance` attribute when provided', async ({ page }) => {
const element = page.locator('fluent-link');
+ await page.setContent(/* html */ `
+
+ `);
+
await element.evaluate((node: Link) => {
node.appearance = 'subtle';
});
@@ -76,6 +82,10 @@ test.describe('Link', () => {
test('should add a custom state of `inline` when true', async ({ page }) => {
const element = page.locator('fluent-link');
+ await page.setContent(/* html */ `
+
+ `);
+
await element.evaluate((node: Link) => {
node.inline = true;
});
@@ -89,3 +99,14 @@ test.describe('Link', () => {
await expect(element).not.toHaveCustomState('inline');
});
});
+
+test('should not have auto detectable accessibility issues', async ({ page }) => {
+ await createElementInternalsTrapsForAxe(page);
+
+ await page.goto(fixtureURL(storybookDocId));
+ await page.waitForFunction(() => customElements.whenDefined('fluent-link'));
+
+ const results = await analyzePageWithAxe(page);
+
+ expect(results.violations).toEqual([]);
+});
diff --git a/packages/web-components/src/menu-list/menu-list.spec.ts b/packages/web-components/src/menu-list/menu-list.spec.ts
index c099c3138bce6..f4e88e0e60216 100644
--- a/packages/web-components/src/menu-list/menu-list.spec.ts
+++ b/packages/web-components/src/menu-list/menu-list.spec.ts
@@ -1,14 +1,18 @@
import { once } from 'events';
import { test } from '@playwright/test';
-import { expect, fixtureURL } from '../helpers.tests.js';
+import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js';
import { MenuItemRole } from '../menu-item/menu-item.options.js';
import { MenuItem } from '../menu-item/menu-item.js';
-test.describe('Menu', () => {
+const storybookDocId = 'components-menulist--docs';
+
+test.describe('MenuList', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(fixtureURL('components-menulist--menu-list'));
+ await page.goto(fixtureURL(storybookDocId));
- await page.waitForFunction(() => customElements.whenDefined('fluent-menu-list'));
+ await page.waitForFunction(() =>
+ Promise.all([customElements.whenDefined('fluent-menu-list'), customElements.whenDefined('fluent-menu-item')]),
+ );
});
test('should have a role of `menu`', async ({ page }) => {
@@ -589,3 +593,16 @@ test.describe('Menu', () => {
});
});
});
+
+test('should not have auto detectable accessibility issues', async ({ page }) => {
+ await createElementInternalsTrapsForAxe(page);
+
+ await page.goto(fixtureURL(storybookDocId));
+ await page.waitForFunction(() =>
+ Promise.all([customElements.whenDefined('fluent-menu-list'), customElements.whenDefined('fluent-menu-item')]),
+ );
+
+ const results = await analyzePageWithAxe(page);
+
+ expect(results.violations).toEqual([]);
+});
diff --git a/packages/web-components/src/menu/menu.spec.ts b/packages/web-components/src/menu/menu.spec.ts
index 6fe3a3b4fc5a8..559f62016bcf9 100644
--- a/packages/web-components/src/menu/menu.spec.ts
+++ b/packages/web-components/src/menu/menu.spec.ts
@@ -1,10 +1,12 @@
import { test } from '@playwright/test';
-import { expect, fixtureURL } from '../helpers.tests.js';
+import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js';
import type { Menu } from './menu.js';
+const storybookDocId = 'components-menu--docs';
+
test.describe('Menu', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(fixtureURL('components-menu--default'));
+ await page.goto(fixtureURL(storybookDocId));
await page.waitForFunction(() =>
Promise.all([
@@ -382,3 +384,30 @@ test.describe('Menu', () => {
await expect(menuButton).toBeFocused();
});
});
+
+test('should not have auto detectable accessibility issues', async ({ page }) => {
+ await createElementInternalsTrapsForAxe(page);
+
+ await page.goto(fixtureURL(storybookDocId));
+ await page.waitForFunction(() =>
+ Promise.all([
+ customElements.whenDefined('fluent-menu'),
+ customElements.whenDefined('fluent-menu-list'),
+ customElements.whenDefined('fluent-menu-item'),
+ customElements.whenDefined('fluent-menu-button'),
+ ]),
+ );
+
+ const results = await analyzePageWithAxe(page);
+
+ expect(results.violations).toEqual([]);
+
+ const menu = page.locator('fluent-menu').nth(0);
+ const menuButton = menu.locator('fluent-menu-button');
+ const menuList = menu.locator('fluent-menu-list');
+ await menuButton.click();
+ await menuList.waitFor({ state: 'visible' });
+ const openedResults = await analyzePageWithAxe(page);
+
+ expect(openedResults.violations).toEqual([]);
+});
diff --git a/packages/web-components/src/message-bar/message-bar.integration.spec.ts b/packages/web-components/src/message-bar/message-bar.integration.spec.ts
index 32e82043e951f..e842bc37f89d1 100644
--- a/packages/web-components/src/message-bar/message-bar.integration.spec.ts
+++ b/packages/web-components/src/message-bar/message-bar.integration.spec.ts
@@ -1,11 +1,16 @@
import { test } from '@playwright/test';
-import { expect, fixtureURL } from '../helpers.tests.js';
+import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js';
import type { MessageBar } from './message-bar.js';
+const storybookDocId = 'components-messagebar--docs';
+
test.describe('Message Bar', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(fixtureURL('components-messagebar--default'));
+ await page.goto(fixtureURL(storybookDocId));
await page.waitForFunction(() => customElements.whenDefined('fluent-message-bar'));
+ await page.setContent(/* html */ `
+
+ `);
});
test('should include a role of status', async ({ page }) => {
@@ -101,3 +106,14 @@ test.describe('Message Bar', () => {
await expect(element).toHaveAttribute('dismissed', 'true');
});
});
+
+test('should not have auto detectable accessibility issues', async ({ page }) => {
+ await createElementInternalsTrapsForAxe(page);
+
+ await page.goto(fixtureURL(storybookDocId));
+ await page.waitForFunction(() => customElements.whenDefined('fluent-message-bar'));
+
+ const results = await analyzePageWithAxe(page);
+
+ expect(results.violations).toEqual([]);
+});
diff --git a/packages/web-components/src/message-bar/message-bar.stories.ts b/packages/web-components/src/message-bar/message-bar.stories.ts
index b198bcfd66109..5f6e0751920ab 100644
--- a/packages/web-components/src/message-bar/message-bar.stories.ts
+++ b/packages/web-components/src/message-bar/message-bar.stories.ts
@@ -72,7 +72,7 @@ export default {
actionsSlottedContent: () => html` Action `,
dismissSlottedContent: () => html`
-
+
${dismiss20Regular}
`,
diff --git a/packages/web-components/src/progress-bar/progress-bar.spec.ts b/packages/web-components/src/progress-bar/progress-bar.spec.ts
index a39baa7ef63d8..f947683421d49 100644
--- a/packages/web-components/src/progress-bar/progress-bar.spec.ts
+++ b/packages/web-components/src/progress-bar/progress-bar.spec.ts
@@ -1,12 +1,18 @@
import { test } from '@playwright/test';
-import { expect, fixtureURL } from '../helpers.tests.js';
+import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js';
import type { ProgressBar } from './progress-bar.js';
+const storybookDocId = 'components-progressbar--docs';
+
test.describe('Progress Bar', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(fixtureURL('components-progressbar--default'));
+ await page.goto(fixtureURL(storybookDocId));
await page.waitForFunction(() => customElements.whenDefined('fluent-progress-bar'));
+
+ await page.setContent(/* html */ `
+
+ `);
});
test('should include a role of progressbar', async ({ page }) => {
@@ -166,3 +172,14 @@ test.describe('Progress Bar', () => {
await expect(element).not.toHaveCustomState('error');
});
});
+
+test('should not have auto detectable accessibility issues', async ({ page }) => {
+ await createElementInternalsTrapsForAxe(page);
+
+ await page.goto(fixtureURL(storybookDocId));
+ await page.waitForFunction(() => customElements.whenDefined('fluent-progress-bar'));
+
+ const results = await analyzePageWithAxe(page);
+
+ expect(results.violations).toEqual([]);
+});
diff --git a/packages/web-components/src/progress-bar/progress-bar.stories.ts b/packages/web-components/src/progress-bar/progress-bar.stories.ts
index 818efb31bdb36..fd4d5de677f2d 100644
--- a/packages/web-components/src/progress-bar/progress-bar.stories.ts
+++ b/packages/web-components/src/progress-bar/progress-bar.stories.ts
@@ -13,6 +13,7 @@ const storyTemplate = html>`
min="${story => story.min}"
value="${story => story.value}"
validation-state="${story => story.validationState}"
+ aria-label="Example progress"
>
`;
diff --git a/packages/web-components/src/radio-group/radio-group.spec.ts b/packages/web-components/src/radio-group/radio-group.spec.ts
index efda17debdf0e..66d14cf86bee8 100644
--- a/packages/web-components/src/radio-group/radio-group.spec.ts
+++ b/packages/web-components/src/radio-group/radio-group.spec.ts
@@ -1,11 +1,13 @@
import { test } from '@playwright/test';
-import { expect, fixtureURL } from '../helpers.tests.js';
+import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js';
import type { Radio } from '../radio/index.js';
import type { RadioGroup } from './radio-group.js';
+const storybookDocId = 'components-radiogroup--docs';
+
test.describe('RadioGroup', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(fixtureURL('components-radiogroup--radio-group'));
+ await page.goto(fixtureURL(storybookDocId));
await page.waitForFunction(() =>
Promise.all([customElements.whenDefined('fluent-radio'), customElements.whenDefined('fluent-radio-group')]),
@@ -639,3 +641,16 @@ test.describe('RadioGroup', () => {
await expect(page).not.toHaveURL(/radio=/);
});
});
+
+test('should not have auto detectable accessibility issues', async ({ page }) => {
+ await createElementInternalsTrapsForAxe(page);
+
+ await page.goto(fixtureURL(storybookDocId));
+ await page.waitForFunction(() =>
+ Promise.all([customElements.whenDefined('fluent-radio'), customElements.whenDefined('fluent-radio-group')]),
+ );
+
+ const results = await analyzePageWithAxe(page);
+
+ expect(results.violations).toEqual([]);
+});
diff --git a/packages/web-components/src/radio/radio.spec.ts b/packages/web-components/src/radio/radio.spec.ts
index 6e8cf6ae07491..be1a76e6bcfaa 100644
--- a/packages/web-components/src/radio/radio.spec.ts
+++ b/packages/web-components/src/radio/radio.spec.ts
@@ -1,10 +1,12 @@
import { test } from '@playwright/test';
-import { expect, fixtureURL } from '../helpers.tests.js';
+import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js';
import type { Radio } from './radio.js';
+const storybookDocId = 'components-radio--docs';
+
test.describe('Radio', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(fixtureURL('components-radio--radio'));
+ await page.goto(fixtureURL(storybookDocId));
await page.waitForFunction(() => customElements.whenDefined('fluent-radio'));
});
@@ -349,3 +351,14 @@ test.describe('Radio', () => {
expect(page.url()).not.toContain('?radio=foo');
});
});
+
+test('should not have auto detectable accessibility issues', async ({ page }) => {
+ await createElementInternalsTrapsForAxe(page);
+
+ await page.goto(fixtureURL(storybookDocId));
+ await page.waitForFunction(() => customElements.whenDefined('fluent-radio'));
+
+ const results = await analyzePageWithAxe(page);
+
+ expect(results.violations).toEqual([]);
+});
diff --git a/packages/web-components/src/radio/radio.stories.ts b/packages/web-components/src/radio/radio.stories.ts
index f71842259faae..72c6ab81dde86 100644
--- a/packages/web-components/src/radio/radio.stories.ts
+++ b/packages/web-components/src/radio/radio.stories.ts
@@ -12,6 +12,7 @@ const storyTemplate = html>`
name="${story => story.name}"
?required="${story => story.required}"
value="${story => story.value}"
+ aria-label="Example radio button"
>
`;
diff --git a/packages/web-components/src/rating-display/rating-display.spec.ts b/packages/web-components/src/rating-display/rating-display.spec.ts
index 4215afb18bf95..cd4ae86ee3515 100644
--- a/packages/web-components/src/rating-display/rating-display.spec.ts
+++ b/packages/web-components/src/rating-display/rating-display.spec.ts
@@ -1,15 +1,21 @@
-import { Locator, test } from '@playwright/test';
-import { expect, fixtureURL } from '../helpers.tests.js';
+import { type Locator, test } from '@playwright/test';
+import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js';
import { RatingDisplaySize } from './rating-display.options.js';
-import { RatingDisplay } from './rating-display.js';
+import type { RatingDisplay } from './rating-display.js';
+
+const storybookDocId = 'components-rating-display--docs';
test.describe('Rating Display', () => {
let element: Locator;
test.beforeEach(async ({ page }) => {
- await page.goto(fixtureURL('components-rating-display--default'));
+ await page.goto(fixtureURL(storybookDocId));
await page.waitForFunction(() => customElements.whenDefined('fluent-rating-display'));
+ await page.setContent(/* html */ `
+
+ `);
+
element = page.locator('fluent-rating-display');
});
@@ -204,3 +210,14 @@ test.describe('Rating Display', () => {
await expect(icon.locator('> use')).toBeHidden();
});
});
+
+test('should not have auto detectable accessibility issues', async ({ page }) => {
+ await createElementInternalsTrapsForAxe(page);
+
+ await page.goto(fixtureURL(storybookDocId));
+ await page.waitForFunction(() => customElements.whenDefined('fluent-rating-display'));
+
+ const results = await analyzePageWithAxe(page);
+
+ expect(results.violations).toEqual([]);
+});
diff --git a/packages/web-components/src/rating-display/rating-display.stories.ts b/packages/web-components/src/rating-display/rating-display.stories.ts
index 5ac7976f77d8f..507fd98b67adf 100644
--- a/packages/web-components/src/rating-display/rating-display.stories.ts
+++ b/packages/web-components/src/rating-display/rating-display.stories.ts
@@ -13,6 +13,7 @@ const storyTemplate = html>`
max=${story => story.max}
size=${story => story.size}
value=${story => story.value}
+ aria-label="${story => `Rating: ${story.value}`}"
>${story => story.iconSlottedContent?.()}
`;
diff --git a/packages/web-components/src/slider/slider.spec.ts b/packages/web-components/src/slider/slider.spec.ts
index ac63388a34df0..7cb6313b8c2e9 100644
--- a/packages/web-components/src/slider/slider.spec.ts
+++ b/packages/web-components/src/slider/slider.spec.ts
@@ -1,11 +1,13 @@
import type { Direction } from '@microsoft/fast-web-utilities';
import { test } from '@playwright/test';
-import { expect, fixtureURL } from '../helpers.tests.js';
+import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js';
import type { Slider } from './slider.js';
+const storybookDocId = 'components-slider--docs';
+
test.describe('Slider', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(fixtureURL('components-slider--slider'));
+ await page.goto(fixtureURL(storybookDocId));
await page.waitForFunction(() => customElements.whenDefined('fluent-slider'));
});
@@ -220,6 +222,10 @@ test.describe('Slider', () => {
test('should set and retrieve the `size` property correctly', async ({ page }) => {
const element = page.locator('fluent-slider');
+ await page.setContent(/* html */ `
+
+ `);
+
await element.evaluate((node: Slider) => {
node.size = 'small';
});
@@ -774,3 +780,14 @@ test.describe('Slider', () => {
});
});
});
+
+test('should not have auto detectable accessibility issues', async ({ page }) => {
+ await createElementInternalsTrapsForAxe(page);
+
+ await page.goto(fixtureURL(storybookDocId));
+ await page.waitForFunction(() => customElements.whenDefined('fluent-slider'));
+
+ const results = await analyzePageWithAxe(page);
+
+ expect(results.violations).toEqual([]);
+});
diff --git a/packages/web-components/src/slider/slider.stories.ts b/packages/web-components/src/slider/slider.stories.ts
index bd807a032ae0d..22c0f9ea57782 100644
--- a/packages/web-components/src/slider/slider.stories.ts
+++ b/packages/web-components/src/slider/slider.stories.ts
@@ -16,6 +16,7 @@ const storyTemplate = html>`
orientation="${story => story.orientation}"
value="${story => story.value}"
slot="${story => story.slot}"
+ aria-label="Example slider"
>
`;
diff --git a/packages/web-components/src/spinner/spinner.spec.ts b/packages/web-components/src/spinner/spinner.spec.ts
index a509677369d4c..d3b79e98ed144 100644
--- a/packages/web-components/src/spinner/spinner.spec.ts
+++ b/packages/web-components/src/spinner/spinner.spec.ts
@@ -1,11 +1,13 @@
import { test } from '@playwright/test';
-import { expect, fixtureURL } from '../helpers.tests.js';
+import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js';
import type { Spinner } from './spinner.js';
import { SpinnerAppearance, SpinnerSize } from './spinner.options.js';
+const storybookDocId = 'components-spinner--docs';
+
test.describe('Spinner', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(fixtureURL('components-spinner--default'));
+ await page.goto(fixtureURL(storybookDocId));
await page.waitForFunction(() => customElements.whenDefined('fluent-spinner'));
});
@@ -14,6 +16,10 @@ test.describe('Spinner', () => {
test(`should set and retrieve the \`appearance\` property correctly to ${thisAppearance}`, async ({ page }) => {
const element = page.locator('fluent-spinner');
+ await page.setContent(/* html */ `
+
+ `);
+
await element.evaluate((node: Spinner, appearance) => {
node.appearance = appearance;
}, thisAppearance as SpinnerAppearance);
@@ -37,6 +43,10 @@ test.describe('Spinner', () => {
test(`should set and retrieve the \`size\` property correctly to ${thisSize}`, async ({ page }) => {
const element = page.locator('fluent-spinner');
+ await page.setContent(/* html */ `
+
+ `);
+
await element.evaluate((node: Spinner, size) => {
node.size = size;
}, thisSize as SpinnerSize);
@@ -56,3 +66,14 @@ test.describe('Spinner', () => {
});
}
});
+
+test('should not have auto detectable accessibility issues', async ({ page }) => {
+ await createElementInternalsTrapsForAxe(page);
+
+ await page.goto(fixtureURL(storybookDocId));
+ await page.waitForFunction(() => customElements.whenDefined('fluent-spinner'));
+
+ const results = await analyzePageWithAxe(page);
+
+ expect(results.violations).toEqual([]);
+});
diff --git a/packages/web-components/src/spinner/spinner.stories.ts b/packages/web-components/src/spinner/spinner.stories.ts
index 522c4bcb2842e..5f118a572310e 100644
--- a/packages/web-components/src/spinner/spinner.stories.ts
+++ b/packages/web-components/src/spinner/spinner.stories.ts
@@ -6,7 +6,11 @@ import { Spinner as FluentSpinner } from './spinner.js';
type Story = StoryObj;
const storyTemplate = html>`
-
+
`;
export default {
diff --git a/packages/web-components/src/switch/switch.spec.ts b/packages/web-components/src/switch/switch.spec.ts
index 3b9617428ea86..33e5d7063aa28 100644
--- a/packages/web-components/src/switch/switch.spec.ts
+++ b/packages/web-components/src/switch/switch.spec.ts
@@ -1,10 +1,12 @@
import { test } from '@playwright/test';
-import { expect, fixtureURL } from '../helpers.tests.js';
+import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js';
import type { Switch } from './switch.js';
+const storybookDocId = 'components-switch--docs';
+
test.describe('Switch', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(fixtureURL('components-switch--switch'));
+ await page.goto(fixtureURL(storybookDocId));
await page.waitForFunction(() => customElements.whenDefined('fluent-switch'));
});
@@ -289,3 +291,14 @@ test.describe('Switch', () => {
expect(page.url()).toContain('?switch=foo&switch=bar');
});
});
+
+test('should not have auto detectable accessibility issues', async ({ page }) => {
+ await createElementInternalsTrapsForAxe(page);
+
+ await page.goto(fixtureURL(storybookDocId));
+ await page.waitForFunction(() => customElements.whenDefined('fluent-switch'));
+
+ const results = await analyzePageWithAxe(page);
+
+ expect(results.violations).toEqual([]);
+});
diff --git a/packages/web-components/src/switch/switch.stories.ts b/packages/web-components/src/switch/switch.stories.ts
index 97a8ee1e477ce..be0137f484e1e 100644
--- a/packages/web-components/src/switch/switch.stories.ts
+++ b/packages/web-components/src/switch/switch.stories.ts
@@ -13,6 +13,7 @@ const storyTemplate = html>`
name="${story => story.name}"
?required="${story => story.required}"
slot="${story => story.slot}"
+ aria-label="Example switch"
>
`;
@@ -93,8 +94,12 @@ export const Required: Story = {
render: renderComponent(html>`