Skip to content
Draft
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
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@ jobs:
# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret
token: ${{secrets.GITHUB_TOKEN}}

screen-reader-windows:
name: Screen Reader (Windows / NVDA)
runs-on: windows-2025
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up pnpm
uses: pnpm/action-setup@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6.0.5
- name: Use Node.js 22.x
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '22.22.3'
cache: 'pnpm'
- run: pnpm install
- run: pnpm test:screen-reader

# Mirrors the real publish workflow environment (`publish.yml`) so an env
# mismatch — e.g. the Node version pinned by `publish-prepare` violating our
# engineStrict `engines` range — is caught here instead of during publishing.
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
"test:regressions:dev": "vite --config test/regressions/vite.config.mjs --port 5173",
"test:regressions:run": "cross-env VITEST_ENV=chromium vitest run --project regressions",
"test:regressions:server": "serve test/regressions -p 5173",
"pretest:screen-reader": "guidepup install",
"test:screen-reader": "playwright test --config test/screen-reader/playwright.config.ts",
"test:screen-reader:setup": "guidepup setup",
"test:_unit": "cross-env TZ=UTC vitest --project @base-ui/react --project @base-ui/utils --project docs",
"test:jsdom": "cross-env VITEST_ENV=jsdom pnpm test:_unit",
"test:jsdom:coverage": "pnpm test:jsdom --coverage",
Expand All @@ -66,6 +69,9 @@
"@arethetypeswrong/cli": "0.18.2",
"@babel/plugin-transform-react-constant-elements": "7.27.1",
"@base-ui/monorepo-tests": "workspace:*",
"@guidepup/guidepup": "0.29.2",
"@guidepup/playwright": "0.18.0",
"@guidepup/setup": "0.24.1",
"@microsoft/api-extractor": "7.58.9",
"@mui/internal-code-infra": "0.0.4-canary.75",
"@mui/internal-netlify-cache": "0.0.3-canary.5",
Expand Down
90 changes: 90 additions & 0 deletions pnpm-lock.yaml

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

8 changes: 8 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@ allowBuilds:
sharp: true
unrs-resolver: true

# Screen recording is not needed for the tests and pulls in ffmpeg-static.
overrides:
'@guidepup/setup>@guidepup/record': '-'

engineStrict: true
updateNotifier: false
blockExoticSubdeps: true
minimumReleaseAge: 4320
minimumReleaseAgeExclude:
- '@base-ui/*'
# Exact versions intentionally updated before the 72-hour release-age window elapsed.
- '@guidepup/guidepup@0.29.2'
- '@guidepup/playwright@0.18.0'
- '@guidepup/setup@0.24.1'
Comment on lines +25 to +28

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be removed before merging.

- '@mui/internal-*'
trustPolicy: no-downgrade
# Skip trust-policy checks for packages published more than 365 days ago.
Expand Down
25 changes: 25 additions & 0 deletions test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,31 @@ Check out the [visual regression testing readme](./regressions/README.md) for mo

Checkout the [end-to-end testing readme](./e2e/README.md) for more information.

#### Screen reader tests

Screen reader tests use [Guidepup](https://www.guidepup.dev/) with the existing end-to-end
fixtures. They run in a headed Chromium browser with VoiceOver on macOS or NVDA on Windows.

The setup package is installed by `pnpm install`. On macOS, developers who run these tests must
configure their host once:

```bash
pnpm test:screen-reader:setup
```

This macOS-level step is intentionally not run during `pnpm install` because it changes system
settings and may require manual permission prompts.

If the test fails with `Failed to mount Guidepup preferences` and an `EPERM` error, grant Full
Disk Access to the terminal, IDE, or other application running the test under **System Settings
→ Privacy & Security → Full Disk Access**. Fully quit and reopen that application before retrying.

Then run `pnpm test:screen-reader`. The compatible screen reader assets are installed
automatically before the test.

GitHub Actions runs the same cross-platform test with NVDA and Chrome on a Windows Server 2025
runner.

##### Development

When working on the visual regression tests you can run `pnpm test:regressions:dev` in the background to constantly rebuild the views used for visual regression testing.
Expand Down
32 changes: 32 additions & 0 deletions test/screen-reader/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { defineConfig, devices } from '@playwright/test';
import { screenReaderConfig } from '@guidepup/playwright';

const BASE_URL = 'http://localhost:5173';

export default defineConfig({
...screenReaderConfig,
outputDir: '../../test-results/screen-reader',
testDir: '.',
testMatch: '*.spec.ts',
timeout: 60_000,
use: {
...screenReaderConfig.use,
baseURL: BASE_URL,
},
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
channel: process.env.CI ? 'chrome' : undefined,
},
},
],
webServer: {
command: 'pnpm test:e2e:dev',
cwd: process.cwd(),
reuseExistingServer: !process.env.CI,
timeout: 120_000,
url: BASE_URL,
},
});
38 changes: 38 additions & 0 deletions test/screen-reader/radio.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { expect } from '@playwright/test';
import { screenReaderTest as test, type ScreenReaderPlaywright } from '@guidepup/playwright';

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

screenReaderTest automatically uses VoiceOver or NVDA depending on the OS.


const MAX_NAVIGATION_STEPS = 10;

async function navigateToItem(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper should be extracted as we see fit.

screenReader: ScreenReaderPlaywright,
name: RegExp,
step = 0,
): Promise<string> {
const itemText = await screenReader.itemText();

if (name.test(itemText)) {
return itemText;
}

if (step === MAX_NAVIGATION_STEPS) {
throw new Error(`Guidepup did not navigate to an item matching ${name}.`);
}

await screenReader.next();
return navigateToItem(screenReader, name, step + 1);
}

test.use({ screenReaderStartOptions: { capture: true } });

test("announces a radio option's accessible name", async ({ page, screenReader }) => {
await page.goto('/e2e-fixtures/Radio#no-dev');
await page.locator('[data-testid="testcase"]:not([aria-busy="true"])').waitFor();
await screenReader.navigateToWebContent();

const itemText = await navigateToItem(screenReader, /fuji/i);
const spokenPhrase = await screenReader.lastSpokenPhrase();

expect(itemText).toMatch(/fuji/i);
expect(spokenPhrase).toMatch(/fuji/i);
expect(spokenPhrase).toMatch(/radio/i);
});
2 changes: 1 addition & 1 deletion test/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
"moduleResolution": "bundler",
"types": ["vite/client", "vitest/globals", "@testing-library/jest-dom"]
},
"include": ["e2e/**/*", "regressions/**/*", "./*.ts"],
"include": ["e2e/**/*", "regressions/**/*", "screen-reader/**/*", "./*.ts"],
"exclude": ["node_modules", "build"]
}
Loading