Skip to content

Commit 43c8eea

Browse files
feat(testing): add playwright setup (#5860)
* feat(testing): add playwright setup * chore: update yarn.lock * fix(a11y-testing-spike): remove cross workspace import * fix(a11y-testing-spike): clean up helper paths * fix(a11y-testing-spike): move playwright a11y config to root * fix(a11y-testing-spike): address test failures --------- Co-authored-by: Cory Dransfeldt <[email protected]>
1 parent 0173074 commit 43c8eea

23 files changed

+1229
-40
lines changed

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,14 @@ chromatic-build-*.xml
137137
chromatic-diagnostics*.json
138138
chromatic.config.json
139139

140+
# Playwright test reports (generated)
141+
1st-gen/test/playwright-a11y/report/
142+
2nd-gen/test/playwright-a11y/report/
143+
144+
# Playwright test results (generated)
145+
playwright-report/
146+
test-results/
147+
140148
# yarn
141149
.pnp.*
142150
.yarn/*
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* Copyright 2025 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import { expect, test } from '@playwright/test';
14+
import AxeBuilder from '@axe-core/playwright';
15+
import { gotoStory } from '../../../test/a11y-helpers.js';
16+
17+
/**
18+
* Accessibility tests for Badge component
19+
*
20+
* Tests both ARIA snapshot structure and aXe WCAG compliance
21+
*/
22+
23+
test.describe('Badge - ARIA Snapshots', () => {
24+
test('should have correct accessibility tree for default badge', async ({
25+
page,
26+
}) => {
27+
const badge = await gotoStory(page, 'badge--default', 'sp-badge');
28+
const snapshot = await badge.ariaSnapshot();
29+
30+
expect(snapshot).toBeTruthy();
31+
await expect(badge).toMatchAriaSnapshot();
32+
});
33+
34+
test('should handle badge with icon', async ({ page }) => {
35+
const badge = await gotoStory(page, 'badge--icons', 'sp-badge');
36+
const snapshot = await badge.ariaSnapshot();
37+
38+
expect(snapshot).toBeTruthy();
39+
});
40+
41+
test('should maintain accessibility with semantic variants', async ({
42+
page,
43+
}) => {
44+
await gotoStory(page, 'badge--semantic', 'sp-badge');
45+
const badges = page.locator('sp-badge');
46+
47+
const count = await badges.count();
48+
expect(count).toBeGreaterThan(0);
49+
50+
// Verify each badge is accessible
51+
for (let i = 0; i < count; i++) {
52+
const badge = badges.nth(i);
53+
const snapshot = await badge.ariaSnapshot();
54+
expect(snapshot).toBeTruthy();
55+
}
56+
});
57+
});
58+
59+
test.describe('Badge - aXe Validation', () => {
60+
test('should not have accessibility violations - default', async ({
61+
page,
62+
}) => {
63+
await gotoStory(page, 'badge--default', 'sp-badge');
64+
65+
const results = await new AxeBuilder({ page })
66+
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
67+
.analyze();
68+
69+
expect(results.violations).toEqual([]);
70+
});
71+
72+
test('should not have violations - semantic variants', async ({ page }) => {
73+
await gotoStory(page, 'badge--semantic', 'sp-badge');
74+
75+
const results = await new AxeBuilder({ page })
76+
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
77+
.analyze();
78+
79+
expect(results.violations).toEqual([]);
80+
});
81+
82+
test('should not have violations - with icon', async ({ page }) => {
83+
await gotoStory(page, 'badge--icons', 'sp-badge');
84+
85+
const results = await new AxeBuilder({ page })
86+
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
87+
.analyze();
88+
89+
expect(results.violations).toEqual([]);
90+
});
91+
92+
test('should verify color contrast', async ({ page }) => {
93+
await gotoStory(page, 'badge--semantic', 'sp-badge');
94+
95+
const results = await new AxeBuilder({ page })
96+
.withRules(['color-contrast'])
97+
.analyze();
98+
99+
expect(results.violations).toEqual([]);
100+
});
101+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- text: Badge
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/**
2+
* Copyright 2025 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import { expect, test } from '@playwright/test';
14+
import AxeBuilder from '@axe-core/playwright';
15+
import { gotoStory } from '../../../test/a11y-helpers.js';
16+
17+
/**
18+
* Accessibility tests for Status Light component
19+
*
20+
* Tests both ARIA snapshot structure and aXe WCAG compliance
21+
*/
22+
23+
test.describe('Status Light - ARIA Snapshots', () => {
24+
test('should have correct accessibility tree structure', async ({
25+
page,
26+
}) => {
27+
const statusLight = await gotoStory(
28+
page,
29+
'statuslight--m',
30+
'sp-status-light'
31+
);
32+
33+
const snapshot = await statusLight.ariaSnapshot();
34+
expect(snapshot).toBeTruthy();
35+
await expect(statusLight).toMatchAriaSnapshot();
36+
});
37+
38+
test('should reflect different sizes', async ({ page }) => {
39+
const sizes = ['s', 'm', 'l'];
40+
41+
for (const size of sizes) {
42+
const statusLight = await gotoStory(
43+
page,
44+
`statuslight--${size}`,
45+
'sp-status-light'
46+
);
47+
48+
const snapshot = await statusLight.ariaSnapshot();
49+
expect(snapshot).toBeTruthy();
50+
}
51+
});
52+
53+
test('should handle disabled state', async ({ page }) => {
54+
const statusLight = await gotoStory(
55+
page,
56+
'statuslight--disabled-true',
57+
'sp-status-light'
58+
);
59+
60+
const snapshot = await statusLight.ariaSnapshot();
61+
expect(snapshot).toBeTruthy();
62+
});
63+
});
64+
65+
test.describe('Status Light - aXe Validation', () => {
66+
test('should not have accessibility violations - medium size', async ({
67+
page,
68+
}) => {
69+
await gotoStory(page, 'statuslight--m', 'sp-status-light');
70+
71+
const results = await new AxeBuilder({ page })
72+
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
73+
.analyze();
74+
75+
expect(results.violations).toEqual([]);
76+
});
77+
78+
test('should not have violations - different sizes', async ({ page }) => {
79+
const sizes = ['s', 'l'];
80+
81+
for (const size of sizes) {
82+
await gotoStory(page, `statuslight--${size}`, 'sp-status-light');
83+
84+
const results = await new AxeBuilder({ page })
85+
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
86+
.analyze();
87+
88+
expect(results.violations).toEqual([]);
89+
}
90+
});
91+
92+
test('should not have violations - disabled state', async ({ page }) => {
93+
await gotoStory(page, 'statuslight--disabled-true', 'sp-status-light');
94+
95+
const results = await new AxeBuilder({ page })
96+
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
97+
.analyze();
98+
99+
expect(results.violations).toEqual([]);
100+
});
101+
102+
test('should verify color contrast', async ({ page }) => {
103+
await gotoStory(page, 'statuslight--m', 'sp-status-light');
104+
105+
const results = await new AxeBuilder({ page })
106+
.withRules(['color-contrast'])
107+
.analyze();
108+
109+
expect(results.violations).toEqual([]);
110+
});
111+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- text: accent

1st-gen/playwright.config.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

1st-gen/test/a11y-helpers.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Copyright 2025 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import type { Locator, Page } from '@playwright/test';
14+
15+
/**
16+
* Wait for a custom element to be fully defined and upgraded.
17+
* This is more deterministic than waiting for visibility alone.
18+
*
19+
* @param page - Playwright page object
20+
* @param tagName - Custom element tag name (e.g., 'sp-badge')
21+
* @returns Promise that resolves when element is defined
22+
*/
23+
export async function waitForCustomElement(
24+
page: Page,
25+
tagName: string
26+
): Promise<void> {
27+
await page.evaluate((tag) => {
28+
return customElements.whenDefined(tag);
29+
}, tagName);
30+
}
31+
32+
/**
33+
* Wait for Storybook story to be fully rendered.
34+
* More deterministic than arbitrary timeouts.
35+
*
36+
* @param page - Playwright page object
37+
* @param elementSelector - CSS selector for the element to wait for
38+
* @returns The located element
39+
*/
40+
export async function waitForStoryReady(
41+
page: Page,
42+
elementSelector: string
43+
): Promise<Locator> {
44+
// Extract tag name from selector (handles 'sp-badge', 'sp-badge.class', etc.)
45+
const tagName = elementSelector.split(/[.#\s[\]]/)[0];
46+
47+
// Step 1: Wait for the custom element to be defined in the registry
48+
await waitForCustomElement(page, tagName);
49+
50+
// Step 2: Wait for Storybook's story rendering to complete
51+
await page.waitForFunction(() => {
52+
// Check if Storybook has finished rendering
53+
const root = document.querySelector('#storybook-root');
54+
return root && root.children.length > 0;
55+
});
56+
57+
// Step 3: Locate the element and wait for it to be visible
58+
const element = page.locator(elementSelector).first();
59+
await element.waitFor({ state: 'visible' });
60+
61+
// Step 4: Wait for Web Component to be fully upgraded (has shadow root if applicable)
62+
await element.evaluate(async (el) => {
63+
// If it's a custom element, wait for it to be fully upgraded
64+
if (el.tagName.includes('-')) {
65+
await customElements.whenDefined(el.tagName.toLowerCase());
66+
}
67+
});
68+
69+
return element;
70+
}
71+
72+
/**
73+
* Navigate to a Storybook story and wait for it to be ready.
74+
* Combines navigation + deterministic waiting.
75+
*
76+
* @param page - Playwright page object
77+
* @param storyId - Storybook story ID (e.g., 'badge--default')
78+
* @param elementSelector - CSS selector for the main element to wait for
79+
* @returns The located element
80+
*/
81+
export async function gotoStory(
82+
page: Page,
83+
storyId: string,
84+
elementSelector: string
85+
): Promise<Locator> {
86+
// Navigate to story (baseURL is set by Playwright project config)
87+
await page.goto(`/iframe.html?id=${storyId}&viewMode=story`, {
88+
waitUntil: 'domcontentloaded',
89+
});
90+
91+
// Wait for story to be ready
92+
return waitForStoryReady(page, elementSelector);
93+
}

2nd-gen/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@
2424
"start": "run-p dev:core dev:analyze storybook",
2525
"storybook": "yarn workspace @adobe/swc storybook",
2626
"storybook:build": "yarn workspace @adobe/swc storybook:build",
27-
"test": "yarn workspace @adobe/swc test"
27+
"test": "yarn workspace @adobe/swc test",
28+
"test:a11y": "playwright test --config=../playwright.a11y.config.ts",
29+
"test:a11y:1st": "playwright test --config=../playwright.a11y.config.ts --project=1st-gen",
30+
"test:a11y:2nd": "playwright test --config=../playwright.a11y.config.ts --project=2nd-gen",
31+
"test:a11y:report": "playwright show-report test/playwright-a11y/report",
32+
"test:a11y:ui": "playwright test --config=../playwright.a11y.config.ts --ui"
2833
},
2934
"workspaces": [
3035
"packages/*"

0 commit comments

Comments
 (0)