Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@ chromatic-build-*.xml
chromatic-diagnostics*.json
chromatic.config.json

# Playwright test reports (generated)
1st-gen/test/playwright-a11y/report/
2nd-gen/test/playwright-a11y/report/

# Playwright test results (generated)
playwright-report/
test-results/

# yarn
.pnp.*
.yarn/*
Expand Down
101 changes: 101 additions & 0 deletions 1st-gen/packages/badge/test/badge.a11y.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import { expect, test } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
import { gotoStory } from '../../../test/a11y-helpers.js';

/**
* Accessibility tests for Badge component
*
* Tests both ARIA snapshot structure and aXe WCAG compliance
*/

test.describe('Badge - ARIA Snapshots', () => {
test('should have correct accessibility tree for default badge', async ({
page,
}) => {
const badge = await gotoStory(page, 'badge--default', 'sp-badge');
const snapshot = await badge.ariaSnapshot();

expect(snapshot).toBeTruthy();
await expect(badge).toMatchAriaSnapshot();
});

test('should handle badge with icon', async ({ page }) => {
const badge = await gotoStory(page, 'badge--icons', 'sp-badge');
const snapshot = await badge.ariaSnapshot();

expect(snapshot).toBeTruthy();
});

test('should maintain accessibility with semantic variants', async ({
page,
}) => {
await gotoStory(page, 'badge--semantic', 'sp-badge');
const badges = page.locator('sp-badge');

const count = await badges.count();
expect(count).toBeGreaterThan(0);

// Verify each badge is accessible
for (let i = 0; i < count; i++) {
const badge = badges.nth(i);
const snapshot = await badge.ariaSnapshot();
expect(snapshot).toBeTruthy();
}
});
});

test.describe('Badge - aXe Validation', () => {
test('should not have accessibility violations - default', async ({
page,
}) => {
await gotoStory(page, 'badge--default', 'sp-badge');

const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();

expect(results.violations).toEqual([]);
});

test('should not have violations - semantic variants', async ({ page }) => {
await gotoStory(page, 'badge--semantic', 'sp-badge');

const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();

expect(results.violations).toEqual([]);
});

test('should not have violations - with icon', async ({ page }) => {
await gotoStory(page, 'badge--icons', 'sp-badge');

const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();

expect(results.violations).toEqual([]);
});

test('should verify color contrast', async ({ page }) => {
await gotoStory(page, 'badge--semantic', 'sp-badge');

const results = await new AxeBuilder({ page })
.withRules(['color-contrast'])
.analyze();

expect(results.violations).toEqual([]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- text: Badge
111 changes: 111 additions & 0 deletions 1st-gen/packages/status-light/test/status-light.a11y.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import { expect, test } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
import { gotoStory } from '../../../test/a11y-helpers.js';

/**
* Accessibility tests for Status Light component
*
* Tests both ARIA snapshot structure and aXe WCAG compliance
*/

test.describe('Status Light - ARIA Snapshots', () => {
test('should have correct accessibility tree structure', async ({
page,
}) => {
const statusLight = await gotoStory(
page,
'statuslight--m',
'sp-status-light'
);

const snapshot = await statusLight.ariaSnapshot();
expect(snapshot).toBeTruthy();
await expect(statusLight).toMatchAriaSnapshot();
});

test('should reflect different sizes', async ({ page }) => {
const sizes = ['s', 'm', 'l'];

for (const size of sizes) {
const statusLight = await gotoStory(
page,
`statuslight--${size}`,
'sp-status-light'
);

const snapshot = await statusLight.ariaSnapshot();
expect(snapshot).toBeTruthy();
}
});

test('should handle disabled state', async ({ page }) => {
const statusLight = await gotoStory(
page,
'statuslight--disabled-true',
'sp-status-light'
);

const snapshot = await statusLight.ariaSnapshot();
expect(snapshot).toBeTruthy();
});
});

test.describe('Status Light - aXe Validation', () => {
test('should not have accessibility violations - medium size', async ({
page,
}) => {
await gotoStory(page, 'statuslight--m', 'sp-status-light');

const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();

expect(results.violations).toEqual([]);
});

test('should not have violations - different sizes', async ({ page }) => {
const sizes = ['s', 'l'];

for (const size of sizes) {
await gotoStory(page, `statuslight--${size}`, 'sp-status-light');

const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();

expect(results.violations).toEqual([]);
}
});

test('should not have violations - disabled state', async ({ page }) => {
await gotoStory(page, 'statuslight--disabled-true', 'sp-status-light');

const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();

expect(results.violations).toEqual([]);
});

test('should verify color contrast', async ({ page }) => {
await gotoStory(page, 'statuslight--m', 'sp-status-light');

const results = await new AxeBuilder({ page })
.withRules(['color-contrast'])
.analyze();

expect(results.violations).toEqual([]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- text: accent
17 changes: 0 additions & 17 deletions 1st-gen/playwright.config.ts

This file was deleted.

93 changes: 93 additions & 0 deletions 1st-gen/test/a11y-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import type { Locator, Page } from '@playwright/test';

/**
* Wait for a custom element to be fully defined and upgraded.
* This is more deterministic than waiting for visibility alone.
*
* @param page - Playwright page object
* @param tagName - Custom element tag name (e.g., 'sp-badge')
* @returns Promise that resolves when element is defined
*/
export async function waitForCustomElement(
page: Page,
tagName: string
): Promise<void> {
await page.evaluate((tag) => {
return customElements.whenDefined(tag);
}, tagName);
}

/**
* Wait for Storybook story to be fully rendered.
* More deterministic than arbitrary timeouts.
*
* @param page - Playwright page object
* @param elementSelector - CSS selector for the element to wait for
* @returns The located element
*/
export async function waitForStoryReady(
page: Page,
elementSelector: string
): Promise<Locator> {
// Extract tag name from selector (handles 'sp-badge', 'sp-badge.class', etc.)
const tagName = elementSelector.split(/[.#\s[\]]/)[0];

// Step 1: Wait for the custom element to be defined in the registry
await waitForCustomElement(page, tagName);

// Step 2: Wait for Storybook's story rendering to complete
await page.waitForFunction(() => {
// Check if Storybook has finished rendering
const root = document.querySelector('#storybook-root');
return root && root.children.length > 0;
});

// Step 3: Locate the element and wait for it to be visible
const element = page.locator(elementSelector).first();
await element.waitFor({ state: 'visible' });

// Step 4: Wait for Web Component to be fully upgraded (has shadow root if applicable)
await element.evaluate(async (el) => {
// If it's a custom element, wait for it to be fully upgraded
if (el.tagName.includes('-')) {
await customElements.whenDefined(el.tagName.toLowerCase());
}
});

return element;
}

/**
* Navigate to a Storybook story and wait for it to be ready.
* Combines navigation + deterministic waiting.
*
* @param page - Playwright page object
* @param storyId - Storybook story ID (e.g., 'badge--default')
* @param elementSelector - CSS selector for the main element to wait for
* @returns The located element
*/
export async function gotoStory(
page: Page,
storyId: string,
elementSelector: string
): Promise<Locator> {
// Navigate to story (baseURL is set by Playwright project config)
await page.goto(`/iframe.html?id=${storyId}&viewMode=story`, {
waitUntil: 'domcontentloaded',
});

// Wait for story to be ready
return waitForStoryReady(page, elementSelector);
}
7 changes: 6 additions & 1 deletion 2nd-gen/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@
"start": "run-p dev:core dev:analyze storybook",
"storybook": "yarn workspace @adobe/swc storybook",
"storybook:build": "yarn workspace @adobe/swc storybook:build",
"test": "yarn workspace @adobe/swc test"
"test": "yarn workspace @adobe/swc test",
"test:a11y": "playwright test --config=../playwright.a11y.config.ts",
"test:a11y:1st": "playwright test --config=../playwright.a11y.config.ts --project=1st-gen",
"test:a11y:2nd": "playwright test --config=../playwright.a11y.config.ts --project=2nd-gen",
"test:a11y:report": "playwright show-report test/playwright-a11y/report",
"test:a11y:ui": "playwright test --config=../playwright.a11y.config.ts --ui"
},
"workspaces": [
"packages/*"
Expand Down
Loading
Loading