diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5ca8b7325f..6b918c3bb37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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.23.2' + 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. diff --git a/package.json b/package.json index 5a5459e2f15..5313e44c735 100644 --- a/package.json +++ b/package.json @@ -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", @@ -66,6 +69,9 @@ "@arethetypeswrong/cli": "0.18.5", "@babel/plugin-transform-react-constant-elements": "7.29.7", "@base-ui/monorepo-tests": "workspace:*", + "@guidepup/guidepup": "0.33.1", + "@guidepup/playwright": "0.19.1", + "@guidepup/setup": "0.24.6", "@microsoft/api-extractor": "7.58.12", "@mui/internal-code-infra": "0.0.4-canary.101", "@mui/internal-netlify-cache": "0.0.3-canary.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d9d8c69aa8..d5df1aa7cd5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,7 @@ settings: excludeLinksFromLockfile: false overrides: + '@guidepup/setup>@guidepup/record': '-' brace-expansion@1: 1.1.18 brace-expansion@2: 2.1.4 brace-expansion@5: 5.0.9 @@ -34,12 +35,21 @@ importers: '@base-ui/monorepo-tests': specifier: workspace:* version: link:test + '@guidepup/guidepup': + specifier: 0.33.1 + version: 0.33.1 + '@guidepup/playwright': + specifier: 0.19.1 + version: 0.19.1(@guidepup/guidepup@0.33.1)(@playwright/test@1.62.1) + '@guidepup/setup': + specifier: 0.24.6 + version: 0.24.6 '@microsoft/api-extractor': specifier: 7.58.12 version: 7.58.12(@types/node@22.20.1) '@mui/internal-code-infra': specifier: 0.0.4-canary.101 - version: 0.0.4-canary.101(4c6243aed1f3c8d993ccce0bc6817d78) + version: 0.0.4-canary.101(4dd64b75bdb2e090bf30a93c72a30a32) '@mui/internal-netlify-cache': specifier: 0.0.3-canary.5 version: 0.0.3-canary.5 @@ -1613,6 +1623,19 @@ packages: resolution: {integrity: sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==} engines: {node: ^20.17.0 || >=22.9.0} + '@guidepup/guidepup@0.33.1': + resolution: {integrity: sha512-G7kvFCVsllnJdE+UXROgN33aWnXQ3Cg/og6iuAl9bVAPs0PhDUDWFAZVdXbTvBYHsoH9KzDuMdUnIq7G5EhXyg==} + + '@guidepup/playwright@0.19.1': + resolution: {integrity: sha512-d0FMH6LFbRbV71l2NGzv2gqV3vQbHDfTVctXsKm6d1ZahpS2/Ozq1WCEoZ7QTAR6iuNzwy03tZfvyijGH+zUDw==} + peerDependencies: + '@guidepup/guidepup': '>=0.33.0' + '@playwright/test': ^1.57.0 + + '@guidepup/setup@0.24.6': + resolution: {integrity: sha512-Oxcb0aXTVhl0iXYYuecUjTU4Bt0P7+qsOktcLNSnhwdynBYesUofTfk9QdboOhqLmg5wesw4qE3hXDHCgCz5OA==} + hasBin: true + '@html-validate/stylish@6.0.0': resolution: {integrity: sha512-d1/lI3qIXhGVJF3+MmKEBn1dRX6U4Z+BDaOdDI3E6SXcO+OQ7hC/3mSo6BIjwQlcuV6NdDOKm6QCrzIQL1EezQ==} engines: {node: ^22.16.0 || >= 24.0.0} @@ -4340,6 +4363,10 @@ packages: '@wooorm/starry-night@3.10.0': resolution: {integrity: sha512-JRp2va6hfbC14fXgYQTmwdl4j1+4ZID9KKkCD+Mn/+B/j1QMlzZYPUtXOFNxA+CWrEo+IPha6EMVbsap2OFZig==} + '@xmldom/xmldom@0.9.11': + resolution: {integrity: sha512-tW8bcK3hsG0/uqSnNz6TK4BkcuZSezoU7DlnYssILmZDktPnSHHuDJJFM0AJv+13gz2r0iGdrj6qqKeUnxXEDg==} + engines: {node: '>=14.6'} + '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -4672,6 +4699,9 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -4943,6 +4973,10 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} + commander@15.0.0: + resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} + engines: {node: '>=22.12.0'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -5008,6 +5042,7 @@ packages: conventional-changelog-core@5.0.1: resolution: {integrity: sha512-Rvi5pH+LvgsqGwZPZ3Cq/tz4ty7mjijhr3qR4m9IBXNbxGGYgTVVO+duXzz9aArmHxFtwZ+LRkrNIMDQzgoY4A==} engines: {node: '>=14'} + deprecated: Deprecated and no longer maintained. Please use conventional-changelog instead. conventional-changelog-preset-loader@3.0.0: resolution: {integrity: sha512-qy9XbdSLmVnwnvzEisjxdDiLA4OmV3o8db+Zdg4WiFw14fP3B6XNz98X0swPPpkTd/pc1K7+adKgEDM1JCUMiA==} @@ -5287,6 +5322,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -5834,6 +5872,10 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@11.3.1: + resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==} + engines: {node: '>=14.14'} + fs-extra@11.3.4: resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} engines: {node: '>=14.14'} @@ -7480,6 +7522,9 @@ packages: node-html-parser@9.0.0: resolution: {integrity: sha512-MhdaHPyxnyYu/sf0TpiRvDnTrkum0UKHC7FdbDGIUQNlx3I7xzwXoyV0eMUMv/XU+lkJT1glOUzpDPq7b2p1Ew==} + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + node-releases@2.0.51: resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} engines: {node: '>=18'} @@ -7918,6 +7963,10 @@ packages: engines: {node: '>=20'} hasBin: true + plist@4.0.0: + resolution: {integrity: sha512-4dOqNo0Y2NpfSf9q4+zr4bh7pzNWeckIam34Z0KYJhg8qtNNfh59VbD+Yna5SjwcxawVvLKx5w5FtuCijpEF4Q==} + engines: {node: '>=18'} + pngjs@7.0.0: resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} engines: {node: '>=14.19.0'} @@ -9116,6 +9165,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@8.10.0: + resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==} + engines: {node: '>=22.19.0'} + unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} @@ -9197,6 +9250,9 @@ packages: unrs-resolver@1.12.2: resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} + unzipper@0.12.5: + resolution: {integrity: sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==} + upath@2.0.1: resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} engines: {node: '>=4'} @@ -9550,6 +9606,10 @@ packages: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} @@ -10833,6 +10893,23 @@ snapshots: '@gar/promise-retry@1.0.3': {} + '@guidepup/guidepup@0.33.1': + dependencies: + plist: 4.0.0 + + '@guidepup/playwright@0.19.1(@guidepup/guidepup@0.33.1)(@playwright/test@1.62.1)': + dependencies: + '@guidepup/guidepup': 0.33.1 + '@playwright/test': 1.62.1 + + '@guidepup/setup@0.24.6': + dependencies: + chalk: 4.1.2 + commander: 15.0.0 + semver: 7.8.5 + undici: 8.10.0 + unzipper: 0.12.5 + '@html-validate/stylish@6.0.0': {} '@humanfs/core@0.19.1': {} @@ -11333,7 +11410,7 @@ snapshots: - tsx - yaml - '@mui/internal-code-infra@0.0.4-canary.101(4c6243aed1f3c8d993ccce0bc6817d78)': + '@mui/internal-code-infra@0.0.4-canary.101(4dd64b75bdb2e090bf30a93c72a30a32)': dependencies: '@argos-ci/core': 6.5.0(@types/node@22.20.1)(supports-color@7.2.0) '@babel/cli': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) @@ -11381,7 +11458,7 @@ snapshots: eslint-plugin-compat: 7.0.2(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.5)(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) eslint-plugin-jsx-a11y: 6.10.2(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) - eslint-plugin-mdx: 3.8.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + eslint-plugin-mdx: 3.8.1(bluebird@3.7.2)(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) eslint-plugin-mocha: 11.3.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) eslint-plugin-react: 7.37.5(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) eslint-plugin-react-compiler: 19.1.0-rc.2(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) @@ -11766,10 +11843,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@npmcli/config@8.3.4': + '@npmcli/config@8.3.4(bluebird@3.7.2)': dependencies: '@npmcli/map-workspaces': 3.0.6 - '@npmcli/package-json': 5.2.1 + '@npmcli/package-json': 5.2.1(bluebird@3.7.2) ci-info: 4.3.1 ini: 4.1.3 nopt: 7.2.1 @@ -11787,14 +11864,14 @@ snapshots: dependencies: semver: 7.8.5 - '@npmcli/git@5.0.8': + '@npmcli/git@5.0.8(bluebird@3.7.2)': dependencies: '@npmcli/promise-spawn': 7.0.2 ini: 4.1.3 lru-cache: 10.4.3 npm-pick-manifest: 9.1.0 proc-log: 4.2.0 - promise-inflight: 1.0.1 + promise-inflight: 1.0.1(bluebird@3.7.2) promise-retry: 2.0.1 semver: 7.8.5 which: 4.0.0 @@ -11867,9 +11944,9 @@ snapshots: '@npmcli/node-gyp@5.0.0': {} - '@npmcli/package-json@5.2.1': + '@npmcli/package-json@5.2.1(bluebird@3.7.2)': dependencies: - '@npmcli/git': 5.0.8 + '@npmcli/git': 5.0.8(bluebird@3.7.2) glob: 10.5.0 hosted-git-info: 7.0.2 json-parse-even-better-errors: 3.0.2 @@ -13816,6 +13893,8 @@ snapshots: vscode-oniguruma: 2.0.1 vscode-textmate: 9.3.2 + '@xmldom/xmldom@0.9.11': {} + '@xtuc/ieee754@1.2.0': {} '@xtuc/long@4.2.2': {} @@ -14162,6 +14241,8 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + bluebird@3.7.2: {} + boolbase@1.0.0: {} boxen@7.0.0: @@ -14435,6 +14516,8 @@ snapshots: commander@10.0.1: {} + commander@15.0.0: {} + commander@2.20.3: {} commander@4.1.1: {} @@ -14804,6 +14887,10 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + eastasianwidth@0.2.0: {} ejs@3.1.10: @@ -15059,7 +15146,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-mdx@3.8.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0): + eslint-mdx@3.8.1(bluebird@3.7.2)(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0): dependencies: acorn: 8.16.0 acorn-jsx: 5.3.2(acorn@8.16.0) @@ -15071,7 +15158,7 @@ snapshots: remark-stringify: 11.0.0 synckit: 0.11.12 unified: 11.0.5 - unified-engine: 11.2.2(supports-color@7.2.0) + unified-engine: 11.2.2(bluebird@3.7.2)(supports-color@7.2.0) unist-util-visit: 5.1.0 vfile: 6.0.3 transitivePeerDependencies: @@ -15168,10 +15255,10 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-mdx@3.8.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0): + eslint-plugin-mdx@3.8.1(bluebird@3.7.2)(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0): dependencies: eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) - eslint-mdx: 3.8.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + eslint-mdx: 3.8.1(bluebird@3.7.2)(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-mdx: 3.0.0(supports-color@7.2.0) micromark-extension-mdxjs: 3.0.0 @@ -15620,6 +15707,12 @@ snapshots: fs-constants@1.0.0: {} + fs-extra@11.3.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fs-extra@11.3.4: dependencies: graceful-fs: 4.2.11 @@ -16762,9 +16855,9 @@ snapshots: strip-bom: 4.0.0 type-fest: 0.6.0 - load-plugin@6.0.3: + load-plugin@6.0.3(bluebird@3.7.2): dependencies: - '@npmcli/config': 8.3.4 + '@npmcli/config': 8.3.4(bluebird@3.7.2) import-meta-resolve: 4.2.0 transitivePeerDependencies: - bluebird @@ -17761,6 +17854,8 @@ snapshots: css-select: 5.2.2 entities: 8.0.0 + node-int64@0.4.0: {} + node-releases@2.0.51: {} nopt@7.2.1: @@ -18375,6 +18470,11 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + plist@4.0.0: + dependencies: + '@xmldom/xmldom': 0.9.11 + xmlbuilder: 15.1.1 + pngjs@7.0.0: {} possible-typed-array-names@1.1.0: {} @@ -18486,7 +18586,9 @@ snapshots: promise-call-limit@3.0.2: {} - promise-inflight@1.0.1: {} + promise-inflight@1.0.1(bluebird@3.7.2): + optionalDependencies: + bluebird: 3.7.2 promise-retry@2.0.1: dependencies: @@ -19923,6 +20025,8 @@ snapshots: undici-types@6.21.0: {} + undici@8.10.0: {} + unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-emoji-modifier-base@1.0.0: {} @@ -19940,7 +20044,7 @@ snapshots: unicorn-magic@0.4.0: {} - unified-engine@11.2.2(supports-color@7.2.0): + unified-engine@11.2.2(bluebird@3.7.2)(supports-color@7.2.0): dependencies: '@types/concat-stream': 2.0.3 '@types/debug': 4.1.13 @@ -19954,7 +20058,7 @@ snapshots: ignore: 6.0.2 is-empty: 1.2.0 is-plain-obj: 4.1.0 - load-plugin: 6.0.3 + load-plugin: 6.0.3(bluebird@3.7.2) parse-json: 7.1.1 trough: 2.2.0 unist-util-inspect: 8.1.0 @@ -20068,6 +20172,14 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + unzipper@0.12.5: + dependencies: + bluebird: 3.7.2 + duplexer2: 0.1.4 + fs-extra: 11.3.1 + graceful-fs: 4.2.11 + node-int64: 0.4.0 + upath@2.0.1: {} update-browserslist-db@1.2.3(browserslist@4.28.6): @@ -20455,6 +20567,8 @@ snapshots: xml-name-validator@5.0.0: {} + xmlbuilder@15.1.1: {} + xmlchars@2.2.0: {} xtend@4.0.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bb160b82760..78396c221cc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -24,6 +24,8 @@ allowBuilds: # # Remove each entry once the parent dependency ships the fixed version on its own. overrides: + # Screen recording is not needed for the tests and pulls in ffmpeg-static. + '@guidepup/setup>@guidepup/record': '-' # via lerna -> minimatch, eslint-plugin-* -> minimatch (GHSA-3jxr-9vmj-r5cp) brace-expansion@1: '1.1.18' brace-expansion@2: '2.1.4' @@ -53,7 +55,6 @@ overrides: tar: '^7.5.22' # via @vitest/browser (GHSA-96hv-2xvq-fx4p) ws: '^8.21.3' - engineStrict: true updateNotifier: false blockExoticSubdeps: true diff --git a/test/README.md b/test/README.md index 7b6aadb6a7e..3356447c5e3 100644 --- a/test/README.md +++ b/test/README.md @@ -171,6 +171,33 @@ 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 screen reader must use English because the tests and Guidepup's navigation match English +announcements. + +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. diff --git a/test/screen-reader/playwright.config.ts b/test/screen-reader/playwright.config.ts new file mode 100644 index 00000000000..1678fdac0e6 --- /dev/null +++ b/test/screen-reader/playwright.config.ts @@ -0,0 +1,33 @@ +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: 5 * 60_000, + retries: 1, + 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, + }, +}); diff --git a/test/screen-reader/radio.spec.ts b/test/screen-reader/radio.spec.ts new file mode 100644 index 00000000000..a1b6450183a --- /dev/null +++ b/test/screen-reader/radio.spec.ts @@ -0,0 +1,38 @@ +import { expect } from '@playwright/test'; +import { screenReaderTest as test, type ScreenReaderPlaywright } from '@guidepup/playwright'; + +const MAX_NAVIGATION_STEPS = 10; + +async function navigateToItem( + screenReader: ScreenReaderPlaywright, + name: RegExp, + step = 0, +): Promise { + 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); +}); diff --git a/test/tsconfig.json b/test/tsconfig.json index 7fbdf4b4fea..365cc7f54c2 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -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"] }