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
76 changes: 76 additions & 0 deletions web/packages/studio/e2e-tests/a11y/a11y.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { runAxeScan } from '@e2e-tests/a11y/axe';
import { disableAuthForTest } from '@e2e-tests/utils/pageUtils';
import { expect, test, type Page, type Route } from '@playwright/test';

// ---------------------------------------------------------------------------
// Fixed mock data — no live backend required.
// All API calls are intercepted by page.route() before any navigation.
// ---------------------------------------------------------------------------

const MOCK_WORKSPACE = {
id: 'mock-workspace-id',
name: 'default',
description: 'Mock workspace for accessibility tests',
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
};

const fulfillJson = (route: Route, body: unknown) =>
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(body) });

/**
* Intercept NMP API calls and return fixed mock responses.
*
* Playwright uses LIFO for route handlers, so the workspace-specific route is
* registered last and therefore runs before the catch-all.
*/
const setupApiMocks = async (page: Page): Promise<void> => {
// Catch-all: empty success for any unmatched API route
await page.route('**/apis/**', (route) => fulfillJson(route, {}));

// WorkspaceProvider calls this on every authenticated page load
await page.route('**/apis/entities/v2/workspaces/default', (route) =>
fulfillJson(route, MOCK_WORKSPACE)
);
};

test.describe('Accessibility — Studio routes (axe / WCAG 2.x A+AA)', () => {
test.beforeEach(async ({ page }) => {
await setupApiMocks(page);
await disableAuthForTest(page);
});

test('workspace dashboard has no axe violations', async ({ page }) => {
await page.goto('/workspaces/default/dashboard');
await page.waitForLoadState('networkidle');

const results = await runAxeScan(page);

expect(
results.violations,
`axe violations on /workspaces/default/dashboard:\n${formatViolations(results.violations)}`
).toHaveLength(0);
});
});

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

interface AxeViolation {
id: string;
description: string;
nodes: Array<{ html: string }>;
}

const formatViolations = (violations: AxeViolation[]): string =>
violations
.map(
(v) =>
` [${v.id}] ${v.description}\n` +
v.nodes.map((n) => ` → ${n.html.slice(0, 120)}`).join('\n')
)
.join('\n');
23 changes: 23 additions & 0 deletions web/packages/studio/e2e-tests/a11y/axe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { AxeBuilder } from '@axe-core/playwright';
import type { Page } from '@playwright/test';

/**
* WCAG 2.x conformance tags targeted by our accessibility harness.
* Covers levels A and AA for WCAG 2.0, 2.1, and 2.2.
*/
const A11Y_TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'] as const;

/** Return type of a single axe scan — re-exported from @axe-core/playwright's analyze(). */
export type AxeScanResult = Awaited<ReturnType<AxeBuilder['analyze']>>;

/**
* Runs an axe accessibility scan on the current page state.
*
* Returns the raw AxeResults so callers can assert on violations,
* incomplete checks, or passes as appropriate for their test.
*/
export const runAxeScan = async (page: Page): Promise<AxeScanResult> =>
new AxeBuilder({ page }).withTags([...A11Y_TAGS]).analyze();
1 change: 1 addition & 0 deletions web/packages/studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
"@nemo/testing": "workspace:*",
"@huggingface/hub": "^1.0.0",
"@nvidia/foundations-tailwind-plugin": "catalog:",
"@axe-core/playwright": "^4.12.1",
"@playwright/test": "^1.57.0",
"@storybook/react": "catalog:",
"@tailwindcss/postcss": "catalog:",
Expand Down
13 changes: 13 additions & 0 deletions web/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.