From b6ed7a415934c55d7b9af94a6f2ccabcf0e4701d Mon Sep 17 00:00:00 2001 From: Nicolas Merget <104347736+nmerget@users.noreply.github.com> Date: Wed, 24 Jul 2024 13:40:50 +0200 Subject: [PATCH] test: add guidepup as screen-reader automation (#2556) * test: add guidepup as screen-reader automation * test: screen-reader in pipeline * test: screen-reader * chore: update frm evaluations * chore: update screen-reader tests * chore: update screen-reader tests * chore: update screen-reader tests * chore: update screen-reader tests * chore: update screen-reader tests * chore: update screen-reader tests * chore: update screen-reader tests * chore: update screen-reader tests * fix: screen reader workflow for ci * fix: screen reader workflow for ci * chore: update from main * fix: issues with typescript * chore: changed tests for guidedpup * fix: issue with angular * fix: snapshots * chore: update from main * chore: update guidepup tests * fix: issue with download showcase * chore: remove webkit from screen reader test * fix: path for recordings * fix: issue with line ending * test: if voiceover.next works for labels * fix: issue with voiceOver test * test: guidepup * test: guidepup * test: guidepup * test: guidepup * test: guidepup * test: guidepup * test: guidepup * test: guidepup * test: guidepup * test: guidepup * test: guidepup * test: guidepup * test: guidepup * fix: linting issue * fix: linting issue * test: guidepup * test: guidepup * test: guidepup --- .github/actions/npm-cache/action.yml | 6 + .github/actions/playwright-cache/action.yml | 44 ++++ .github/workflows/01-init-playwright.yml | 28 ++ .github/workflows/02-e2e-screen-reader.yml | 67 +++++ .github/workflows/default.yml | 17 +- .github/workflows/release.yml | 17 +- .gitignore | 1 + .jscpd.json | 3 +- .prettierignore | 1 + output/react/package.json | 1 + package-lock.json | 247 +++++++++++++++++- package.json | 2 + .../foundations/scss/helpers/_divider.scss | 15 +- .../src/app/components/default.component.ts | 4 +- showcases/playwright.config.ts | 23 +- showcases/playwright.screen-reader.macos.ts | 16 ++ showcases/playwright.screen-reader.ts | 15 ++ showcases/playwright.screen-reader.windows.ts | 16 ++ showcases/playwright.showcase.ts | 25 ++ showcases/react-showcase/package.json | 4 +- showcases/screen-reader/README.md | 47 ++++ ...d-not-have-icon-in-screen-reader-next-.txt | 1 + ...ut-should-have-message-and-label-next-.txt | 1 + .../DBRadio-should-label-duplicated-next-.txt | 1 + ...d-not-have-icon-in-screen-reader-next-.txt | 1 + ...ld-not-have-icon-in-screen-reader-tab-.txt | 1 + ...put-should-have-message-and-label-tab-.txt | 1 + ...BRadio-should-label-duplicated-arrows-.txt | 1 + .../DBRadio-should-label-duplicated-next-.txt | 1 + showcases/screen-reader/data.ts | 44 ++++ showcases/screen-reader/default.ts | 152 +++++++++++ showcases/screen-reader/tests/button.spec.ts | 39 +++ showcases/screen-reader/tests/input.spec.ts | 41 +++ showcases/screen-reader/tests/radio.spec.ts | 45 ++++ showcases/screen-reader/tsconfig.json | 10 + 35 files changed, 901 insertions(+), 37 deletions(-) create mode 100644 .github/actions/playwright-cache/action.yml create mode 100644 .github/workflows/01-init-playwright.yml create mode 100644 .github/workflows/02-e2e-screen-reader.yml create mode 100644 showcases/playwright.screen-reader.macos.ts create mode 100644 showcases/playwright.screen-reader.ts create mode 100644 showcases/playwright.screen-reader.windows.ts create mode 100644 showcases/playwright.showcase.ts create mode 100644 showcases/screen-reader/README.md create mode 100644 showcases/screen-reader/__snapshots__/macos/webkit/DBButton-should-not-have-icon-in-screen-reader-next-.txt create mode 100644 showcases/screen-reader/__snapshots__/macos/webkit/DBInput-should-have-message-and-label-next-.txt create mode 100644 showcases/screen-reader/__snapshots__/macos/webkit/DBRadio-should-label-duplicated-next-.txt create mode 100644 showcases/screen-reader/__snapshots__/windows/chromium/DBButton-should-not-have-icon-in-screen-reader-next-.txt create mode 100644 showcases/screen-reader/__snapshots__/windows/chromium/DBButton-should-not-have-icon-in-screen-reader-tab-.txt create mode 100644 showcases/screen-reader/__snapshots__/windows/chromium/DBInput-should-have-message-and-label-tab-.txt create mode 100644 showcases/screen-reader/__snapshots__/windows/chromium/DBRadio-should-label-duplicated-arrows-.txt create mode 100644 showcases/screen-reader/__snapshots__/windows/chromium/DBRadio-should-label-duplicated-next-.txt create mode 100644 showcases/screen-reader/data.ts create mode 100644 showcases/screen-reader/default.ts create mode 100644 showcases/screen-reader/tests/button.spec.ts create mode 100644 showcases/screen-reader/tests/input.spec.ts create mode 100644 showcases/screen-reader/tests/radio.spec.ts create mode 100644 showcases/screen-reader/tsconfig.json diff --git a/.github/actions/npm-cache/action.yml b/.github/actions/npm-cache/action.yml index 02ad70acd9e..0c72e4ae2ed 100644 --- a/.github/actions/npm-cache/action.yml +++ b/.github/actions/npm-cache/action.yml @@ -39,6 +39,12 @@ runs: restore-keys: | ${{ runner.os }}-node-${{ inputs.nodeVersion }} + - name: ๐ŸŽ„๐ŸŽธ๐ŸฅŠ Log Cache Hit + shell: bash + env: + HIT: ${{ steps.cache.outputs.cache-hit }} + run: echo $HIT + - name: โฌ NPM ci shell: bash if: steps.cache.outputs.cache-hit != 'true' diff --git a/.github/actions/playwright-cache/action.yml b/.github/actions/playwright-cache/action.yml new file mode 100644 index 00000000000..ab49d64c0ac --- /dev/null +++ b/.github/actions/playwright-cache/action.yml @@ -0,0 +1,44 @@ +--- +name: "Playwright Cache Action" +description: "Initialize Playwright Cache" +inputs: + version: + description: "Playwright version" + required: false +runs: + using: "composite" + steps: + - name: ๐Ÿ†™ Set env for os + shell: bash + env: + OS: ${{ runner.os }} + run: | + if [[ $OS == "Windows" ]]; then + echo "CACHE_PATH=C:\Users\runneradmin\AppData\Local\ms-playwright" >> "$GITHUB_ENV" + echo "BROWSERS=chromium firefox" >> "$GITHUB_ENV" + echo "OS=windows" >> "$GITHUB_ENV" + else + echo "CACHE_PATH=~/Library/Caches/ms-playwright" >> "$GITHUB_ENV" + echo "BROWSERS=webkit chromium firefox" >> "$GITHUB_ENV" + echo "OS=macos" >> "$GITHUB_ENV" + fi + + - name: ๐Ÿ†’ Cache Playwright binaries + uses: actions/cache@v4 + id: playwright-cache + with: + path: ${{ env.CACHE_PATH }} + key: "${{ runner.os }}-playwright-${{ inputs.version }}" + restore-keys: | + ${{ runner.os }}-playwright- + + - name: ๐ŸŽ„๐ŸŽธ๐ŸฅŠ Log Cache Hit + shell: bash + env: + HIT: ${{ steps.playwright-cache.outputs.cache-hit }} + run: echo $HIT + + - name: โฌ Install Playwright's dependencies + shell: bash + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: npx --no -- playwright install --with-deps ${{ env.BROWSERS }} diff --git a/.github/workflows/01-init-playwright.yml b/.github/workflows/01-init-playwright.yml new file mode 100644 index 00000000000..b73739a7782 --- /dev/null +++ b/.github/workflows/01-init-playwright.yml @@ -0,0 +1,28 @@ +name: ๐ŸŽญ Init Playwright + +on: + workflow_call: + inputs: + version: + required: true + type: string + +jobs: + init-playwright: + name: ๐ŸŽญ Init Playwright - ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macos-13, windows-2022] + steps: + - name: โฌ Checkout repo + uses: actions/checkout@v4 + + - name: ๐Ÿ”„ Init Cache Default + uses: ./.github/actions/npm-cache + + - name: ๐Ÿ”„ Init Playwright Cache + uses: ./.github/actions/playwright-cache + with: + version: ${{ inputs.version }} diff --git a/.github/workflows/02-e2e-screen-reader.yml b/.github/workflows/02-e2e-screen-reader.yml new file mode 100644 index 00000000000..d3b0845ae20 --- /dev/null +++ b/.github/workflows/02-e2e-screen-reader.yml @@ -0,0 +1,67 @@ +name: ๐ŸŽญ Playwright Screen Reader + +on: + workflow_call: + inputs: + version: + required: true + type: string + +permissions: + actions: write + contents: write + +jobs: + playwright-screen-reader: + name: ๐Ÿงช๐ŸŽญ - screen-reader - ${{ matrix.os }} - react - ${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macos-13, windows-2022] + shardIndex: [1, 2] + shardTotal: [2] + steps: + - name: โฌ Checkout repo + uses: actions/checkout@v4 + + - name: ๐Ÿฆฎ Guidepup Setup + uses: guidepup/setup-action@0.15.3 + with: + record: true + + - name: ๐Ÿ”„ Init Cache + uses: ./.github/actions/npm-cache + + - name: ๐Ÿ”„ Init Playwright Cache + uses: ./.github/actions/playwright-cache + with: + version: ${{ inputs.version }} + + - name: โฌ Download react-showcase + uses: actions/download-artifact@v4 + with: + name: db-ui-react-showcase + path: build-showcases/react-showcase + + - name: ๐Ÿ‘ฉโ€๐Ÿ”ฌ Test showcase with Playwright ๐ŸŽญ + env: + showcase: react-showcase + run: | + npm run test:screen-reader:${{ env.OS }} --workspace=react-showcase -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + + - name: ๐Ÿ†™ Upload test results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: react-screen-reader-${{ matrix.os }}-${{ matrix.shardIndex }} + path: ./showcases/react-showcase/test-results + retention-days: 30 + + - name: ๐Ÿ†™ Upload test results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: react-recordings-${{ matrix.os }}-${{ matrix.shardIndex }} + path: ./showcases/react-showcase/recordings + retention-days: 30 diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index 232fabe413d..1d6a63a272a 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -27,6 +27,12 @@ jobs: uses: ./.github/workflows/01-build-packages.yml needs: [init] + init-playwright: + uses: ./.github/workflows/01-init-playwright.yml + needs: [get-playwright-version] + with: + version: ${{ needs.get-playwright-version.outputs.version }} + build-outputs: uses: ./.github/workflows/01-build-outputs.yml needs: [build-packages] @@ -135,6 +141,12 @@ jobs: type: patternhub needs: [test-showcase-patternhub, get-playwright-version] + test-screen-reader: + uses: ./.github/workflows/02-e2e-screen-reader.yml + needs: [build-showcase-react, init-playwright, get-playwright-version] + with: + version: ${{ needs.get-playwright-version.outputs.version }} + regenerate-snapshots: if: always() && (needs.test-showcase-angular.result == 'failure' || needs.test-showcase-react.result == 'failure' || needs.test-showcase-vue.result == 'failure') uses: ./.github/workflows/02-e2e-regenerate.yml @@ -169,7 +181,9 @@ jobs: resultTestShowcaseReact="${{ needs.test-showcase-react.result }}" resultTestShowcaseVue="${{ needs.test-showcase-vue.result }}" resultTestShowcasePatternhub="${{ needs.test-showcase-patternhub.result }}" + resultTestScreenReaders="${{ needs.test-screen-reader.result }}" if [[ $resultTestFoundations == "success" ]] && \ + [[ $resultTestScreenReaders == "success" ]] && \ [[ $resultTestShowcaseAngular == "success" ]] && \ [[ $resultTestShowcaseReact == "success" ]] && \ [[ $resultTestShowcaseVue == "success" ]] && \ @@ -204,7 +218,8 @@ jobs: test-showcase-react, test-showcase-vue, test-showcase-patternhub, - test-foundations + test-foundations, + test-screen-reader ] deploy: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 24e57052aa0..2afcbff7550 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,6 +25,12 @@ jobs: uses: ./.github/workflows/01-build-packages.yml needs: [init] + init-playwright: + uses: ./.github/workflows/01-init-playwright.yml + needs: [get-playwright-version] + with: + version: ${{ needs.get-playwright-version.outputs.version }} + build-outputs: uses: ./.github/workflows/01-build-outputs.yml needs: [build-packages] @@ -112,6 +118,12 @@ jobs: version: ${{ needs.get-playwright-version.outputs.version }} showcase: patternhub + test-screen-reader: + uses: ./.github/workflows/02-e2e-screen-reader.yml + needs: [build-showcase-react, init-playwright, get-playwright-version] + with: + version: ${{ needs.get-playwright-version.outputs.version }} + checks-done: if: ${{ always() }} runs-on: ubuntu-latest @@ -132,7 +144,9 @@ jobs: resultTestShowcaseReact="${{ needs.test-showcase-react.result }}" resultTestShowcaseVue="${{ needs.test-showcase-vue.result }}" resultTestShowcasePatternhub="${{ needs.test-showcase-patternhub.result }}" + resultTestScreenReader="${{ needs.test-screen-reader.result }}" if [[ $resultTestFoundations == "success" ]] && \ + [[ resultTestScreenReader == "success" ]] && \ [[ $resultTestShowcaseAngular == "success" ]] && \ [[ $resultTestShowcaseReact == "success" ]] && \ [[ $resultTestShowcaseVue == "success" ]] && \ @@ -167,7 +181,8 @@ jobs: test-showcase-react, test-showcase-vue, test-showcase-patternhub, - test-foundations + test-foundations, + test-screen-reader ] deploy: diff --git a/.gitignore b/.gitignore index 4f56f3da8f7..b4ff78d8f0e 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ packages/components/src/**/*.css.map showcases/patternhub/public/iframe-resizer/* **/test-results/ +**/recordings/ /__snapshots__/**/*-win32.png /packages/foundations/assets/icons/functional/tmp/ diff --git a/.jscpd.json b/.jscpd.json index 9a6154f6d09..4abead46bc3 100644 --- a/.jscpd.json +++ b/.jscpd.json @@ -53,7 +53,8 @@ "packages/foundations/src", "**/.output/**", "**/.nuxt/**", - "showcases/nuxt-showcase/**" + "showcases/nuxt-showcase/**", + "**/tests/**" ], "absolute": true } diff --git a/.prettierignore b/.prettierignore index 77d071708e8..7c9a8997f86 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,3 +7,4 @@ output build-outputs build-showcases packages/foundations/scripts/generate-icon-fonts/styles/** +**/__snapshots__ diff --git a/output/react/package.json b/output/react/package.json index e4cc2c119b7..5e671b66529 100644 --- a/output/react/package.json +++ b/output/react/package.json @@ -22,6 +22,7 @@ "postbuild": "npm-run-all -p mv:*", "regenerate:screenshots": "playwright test -c playwright.config.ts --update-snapshots", "test:components": "playwright test -c playwright.config.ts", + "test:windows": "playwright test -c playwright.screen-reader.win.ts", "tsc": "tsc -p . --sourceMap false" }, "devDependencies": { diff --git a/package-lock.json b/package-lock.json index d7039c15c85..e8fb202abc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,8 @@ "@commitlint/cli": "19.3.0", "@commitlint/config-conventional": "19.2.2", "@double-great/stylelint-a11y": "3.0.2", + "@guidepup/guidepup": "0.22.3", + "@guidepup/playwright": "^0.13.2", "@playwright/test": "1.45.3", "@types/fs-extra": "^11.0.4", "accessibility-checker": "^3.1.71", @@ -4978,6 +4980,21 @@ "resolved": "output/webcomponent", "link": true }, + "node_modules/@derhuerst/http-basic": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz", + "integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==", + "dev": true, + "dependencies": { + "caseless": "^0.12.0", + "concat-stream": "^2.0.0", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -5534,6 +5551,28 @@ "node": ">=14" } }, + "node_modules/@guidepup/guidepup": { + "version": "0.22.3", + "resolved": "https://registry.npmjs.org/@guidepup/guidepup/-/guidepup-0.22.3.tgz", + "integrity": "sha512-0SbPyjoCgYQYPCjWJgQDUrfw1HgSIjVJ6k/UvTUV7FvMPv22LVS40g2P2ScObx2H+apMJGPTja8Tx1uoWZw9JQ==", + "dev": true, + "dependencies": { + "ffmpeg-static": "^5.2.0", + "regedit": "5.1.2", + "semver": "^7.3.8", + "shelljs": "^0.8.5" + } + }, + "node_modules/@guidepup/playwright": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@guidepup/playwright/-/playwright-0.13.2.tgz", + "integrity": "sha512-oIoF7paP8l5SxGmehqreGhiEuE5HoQ4B/qO3JfZzQH2PykF4eCrqijmOLLOAZOJFyG8+8KicVprg4GCsXhoeVQ==", + "dev": true, + "peerDependencies": { + "@guidepup/guidepup": "^0.22.1", + "@playwright/test": "^1.40.1" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -14167,6 +14206,12 @@ } ] }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -14905,6 +14950,21 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "dev": true, + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/confbox": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", @@ -18286,6 +18346,47 @@ "pend": "~1.2.0" } }, + "node_modules/ffmpeg-static": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.2.0.tgz", + "integrity": "sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@derhuerst/http-basic": "^8.2.0", + "env-paths": "^2.2.0", + "https-proxy-agent": "^5.0.0", + "progress": "^2.0.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/ffmpeg-static/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ffmpeg-static/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -20205,6 +20306,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "dev": true, + "dependencies": { + "@types/node": "^10.0.3" + } + }, + "node_modules/http-response-object/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "dev": true + }, "node_modules/http-server": { "version": "14.1.1", "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", @@ -20575,6 +20691,12 @@ } ] }, + "node_modules/if-async": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/if-async/-/if-async-3.7.4.tgz", + "integrity": "sha512-BFEH2mZyeF6KZKaKLVPZ0wMjIiWOdjvZ7zbx8ENec0qfZhJwKFbX/4jKM5LTKyJEc/GOqUKiiJ2IFKT9yWrZqA==", + "dev": true + }, "node_modules/iframe-resizer": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/iframe-resizer/-/iframe-resizer-4.4.5.tgz", @@ -28339,6 +28461,12 @@ "node": ">=6" } }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==", + "dev": true + }, "node_modules/parse-entities": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", @@ -30857,6 +30985,18 @@ "node": ">=8.10.0" } }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -30896,6 +31036,18 @@ "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "dev": true }, + "node_modules/regedit": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/regedit/-/regedit-5.1.2.tgz", + "integrity": "sha512-pQpWqO/I40bMNoMO9kTQx3e5iK542kYcB/Z8X3Y7Hcri6ydc4KZ9ByUsEWFkBRMcwo+2irHuNK5s+pMGPr6VPw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "if-async": "^3.7.4", + "stream-slicer": "0.0.6", + "through2": "^0.6.3" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -32132,6 +32284,44 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shelljs/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -32561,6 +32751,12 @@ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==" }, + "node_modules/stream-slicer": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/stream-slicer/-/stream-slicer-0.0.6.tgz", + "integrity": "sha512-QsY0LbweYE5L+e+iBQgtkM5WUIf7+kCMA/m2VULv8rEEDDnlDPsPvOHH4nli6uaZOKQEt64u65h0l/eeZo7lCw==", + "dev": true + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -33854,6 +34050,40 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, + "node_modules/through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==", + "dev": true, + "dependencies": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + }, + "node_modules/through2/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true + }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -34651,6 +34881,12 @@ "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", "dev": true }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true + }, "node_modules/typescript": { "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", @@ -37746,6 +37982,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -38095,7 +38340,7 @@ }, "showcases/next-showcase": { "dependencies": { - "next": "*", + "next": "latest", "react": "18.3.1", "react-dom": "18.3.1" }, diff --git a/package.json b/package.json index a500e3438c2..2bf4106558d 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,8 @@ "@commitlint/cli": "19.3.0", "@commitlint/config-conventional": "19.2.2", "@double-great/stylelint-a11y": "3.0.2", + "@guidepup/guidepup": "0.22.3", + "@guidepup/playwright": "^0.13.2", "@playwright/test": "1.45.3", "@types/fs-extra": "^11.0.4", "accessibility-checker": "^3.1.71", diff --git a/packages/foundations/scss/helpers/_divider.scss b/packages/foundations/scss/helpers/_divider.scss index bb66cfbc927..06f9c8085be 100644 --- a/packages/foundations/scss/helpers/_divider.scss +++ b/packages/foundations/scss/helpers/_divider.scss @@ -23,21 +23,18 @@ block-size: variables.$db-border-height-3xs; inset-block-start: 0; inset-inline: 0; - } - - @else if $position == "bottom" { + /* stylelint-disable-next-line at-rule-empty-line-before */ + } @else if $position == "bottom" { block-size: variables.$db-border-height-3xs; inset-block-end: 0; inset-inline: 0; - } - - @else if $position == "left" { + /* stylelint-disable-next-line at-rule-empty-line-before */ + } @else if $position == "left" { inline-size: variables.$db-border-height-3xs; inset-inline-start: 0; inset-block: 0; - } - - @else if $position == "right" { + /* stylelint-disable-next-line at-rule-empty-line-before */ + } @else if $position == "right" { inline-size: variables.$db-border-height-3xs; inset-inline-end: 0; inset-block: 0; diff --git a/showcases/angular-showcase/src/app/components/default.component.ts b/showcases/angular-showcase/src/app/components/default.component.ts index 7ae6641970e..700af655778 100644 --- a/showcases/angular-showcase/src/app/components/default.component.ts +++ b/showcases/angular-showcase/src/app/components/default.component.ts @@ -2,8 +2,8 @@ import { Component, Input, NO_ERRORS_SCHEMA, - OnInit, - TemplateRef + type OnInit, + type TemplateRef } from '@angular/core'; import { NgTemplateOutlet } from '@angular/common'; import { ActivatedRoute } from '@angular/router'; diff --git a/showcases/playwright.config.ts b/showcases/playwright.config.ts index 045952f8b62..d630f7ec88f 100644 --- a/showcases/playwright.config.ts +++ b/showcases/playwright.config.ts @@ -1,4 +1,5 @@ import { devices, type PlaywrightTestConfig } from '@playwright/test'; +import showcaseConfig from './playwright.showcase'; /** * See https://playwright.dev/docs/test-configuration. @@ -19,28 +20,10 @@ const config: PlaywrightTestConfig = { fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: Boolean(process.env.CI), - /* Retry on CI only */ - retries: process.env.CI ? 1 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: process.env.CI ? 'blob' : [['list'], ['html', { open: 'never' }]], - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ - actionTimeout: 0, - /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: `http://localhost:8080/${process.env.showcase}/`, - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: process.env.CI ? 'on-first-retry' : 'on' - }, - webServer: { - command: `cd ${process.env.showcase} && npm run preview`, - port: 8080, - reuseExistingServer: !process.env.CI - }, - /* Configure projects for major browsers */ projects: [ { @@ -86,9 +69,7 @@ const config: PlaywrightTestConfig = { } } ], - - /* Folder for test artifacts such as screenshots, videos, traces, etc. */ - outputDir: `./${process.env.showcase}/test-results/` + ...showcaseConfig }; export default config; diff --git a/showcases/playwright.screen-reader.macos.ts b/showcases/playwright.screen-reader.macos.ts new file mode 100644 index 00000000000..72ae01a5cd4 --- /dev/null +++ b/showcases/playwright.screen-reader.macos.ts @@ -0,0 +1,16 @@ +import { devices, type PlaywrightTestConfig } from '@playwright/test'; +import defaultScreenReaderConfig from './playwright.screen-reader'; + +const config: PlaywrightTestConfig = { + ...defaultScreenReaderConfig, + snapshotPathTemplate: + '{snapshotDir}/{testFileDir}/macos/{projectName}/{testName}{ext}', + projects: [ + { + name: 'webkit', + use: { ...devices['Desktop Safari'], headless: false } + } + ] +}; + +export default config; diff --git a/showcases/playwright.screen-reader.ts b/showcases/playwright.screen-reader.ts new file mode 100644 index 00000000000..fed16a2b912 --- /dev/null +++ b/showcases/playwright.screen-reader.ts @@ -0,0 +1,15 @@ +import { screenReaderConfig } from '@guidepup/playwright'; +import { devices, type PlaywrightTestConfig } from '@playwright/test'; +import showcaseConfig from './playwright.showcase'; + +const defaultScreenReaderConfig: PlaywrightTestConfig = { + ...screenReaderConfig, + ...showcaseConfig, + retries: process.env.CI ? 2 : 0, + reportSlowTests: null, + testDir: './screen-reader/tests', + snapshotDir: './screen-reader/__snapshots__', + timeout: 3 * 60 * 1000 +}; + +export default defaultScreenReaderConfig; diff --git a/showcases/playwright.screen-reader.windows.ts b/showcases/playwright.screen-reader.windows.ts new file mode 100644 index 00000000000..5663d9c65e6 --- /dev/null +++ b/showcases/playwright.screen-reader.windows.ts @@ -0,0 +1,16 @@ +import { devices, type PlaywrightTestConfig } from '@playwright/test'; +import defaultScreenReaderConfig from './playwright.screen-reader'; + +const config: PlaywrightTestConfig = { + ...defaultScreenReaderConfig, + snapshotPathTemplate: + '{snapshotDir}/{testFileDir}/windows/{projectName}/{testName}{ext}', + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'], headless: false } + } + ] +}; + +export default config; diff --git a/showcases/playwright.showcase.ts b/showcases/playwright.showcase.ts new file mode 100644 index 00000000000..8db46b41e40 --- /dev/null +++ b/showcases/playwright.showcase.ts @@ -0,0 +1,25 @@ +import { type PlaywrightTestConfig } from '@playwright/test'; + +const showcaseConfig: PlaywrightTestConfig = { + /* Retry on CI only */ + retries: process.env.CI ? 1 : 0, + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: `http://localhost:8080/${process.env.showcase}/`, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: process.env.CI ? 'on-first-retry' : 'on' + }, + webServer: { + command: `cd ${process.env.showcase} && npm run preview`, + port: 8080, + reuseExistingServer: !process.env.CI + }, + + /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + outputDir: `./${process.env.showcase}/test-results/` +}; + +export default showcaseConfig; diff --git a/showcases/react-showcase/package.json b/showcases/react-showcase/package.json index f68bfa55bb0..1f6d5ac1950 100644 --- a/showcases/react-showcase/package.json +++ b/showcases/react-showcase/package.json @@ -11,7 +11,9 @@ "preview": "npx http-server ../../build-showcases", "regenerate:screenshots": "cross-env showcase=react-showcase npx playwright test -c ../playwright.config.ts --update-snapshots", "start": "vite", - "test:e2e": "cross-env showcase=react-showcase npx playwright test --config=../playwright.config.ts" + "test:e2e": "cross-env showcase=react-showcase npx playwright test --config=../playwright.config.ts", + "test:screen-reader:macos": "cross-env showcase=react-showcase npx playwright test --config=../playwright.screen-reader.macos.ts", + "test:screen-reader:windows": "cross-env showcase=react-showcase npx playwright test --config=../playwright.screen-reader.windows.ts" }, "dependencies": { "react": "^18.3.1", diff --git a/showcases/screen-reader/README.md b/showcases/screen-reader/README.md new file mode 100644 index 00000000000..a642cc52a6a --- /dev/null +++ b/showcases/screen-reader/README.md @@ -0,0 +1,47 @@ +# Screen Automated Reader (ScAR ๐Ÿฆ๐Ÿ”ฅ๐Ÿ’€) + +Start a test with these commands: + +## MacOS + +```shell +npm run test:screen-reader:macos --workspace=react-showcase -- --ui +``` + +## Windows + +```shell +npm run test:screen-reader:windows --workspace=react-showcase -- --ui +``` + +## Gotchas + +- Local: Don't switch in between your windows while testing, it will capture only your current screen +- We should avoid auto-generate tests, because they take a lot of time. +- NVDAs `next` command is equivalent of executing Down Arrow - Won't work with radio/select as you might expect +- One simple test takes about 1 minute in CI โฌ… so you should only provide test important things + +## More information + +We use this [survey](https://webaim.org/projects/screenreadersurvey10/) to reduce amount of tests (only for VoiceOver and NVDA). + +> Most common screen reader and browser combinations: + +| Screen Reader & Browser | # of Respondents | % of Respondents | +| ----------------------- | ---------------- | ---------------- | +| NVDA with Chrome | 323 | 21.3% | +| NVDA with Firefox | 152 | 10.0% | +| VoiceOver with Safari | 107 | 7.0% | +| NVDA with Edge | 75 | 5.0% | +| VoiceOver with Chrome | 30 | 2.0% | + +> What operating system are you on when using your primary desktop/laptop screen reader? + +| Response | # of respondents | % of respondents | +| -------- | ---------------- | ---------------- | +| Windows | 1311 | 86.1% | +| Mac | 146 | 9.6% | +| Linux | 44 | 2.9% | +| Other | 21 | 1.4% | + +Conclusion: We only test Chrome for Windows and Safari for MacOS because these are the most common combinations. diff --git a/showcases/screen-reader/__snapshots__/macos/webkit/DBButton-should-not-have-icon-in-screen-reader-next-.txt b/showcases/screen-reader/__snapshots__/macos/webkit/DBButton-should-not-have-icon-in-screen-reader-next-.txt new file mode 100644 index 00000000000..2f9862eb9f6 --- /dev/null +++ b/showcases/screen-reader/__snapshots__/macos/webkit/DBButton-should-not-have-icon-in-screen-reader-next-.txt @@ -0,0 +1 @@ +["(Default) Text button","Icon & Text button","Icon button"] \ No newline at end of file diff --git a/showcases/screen-reader/__snapshots__/macos/webkit/DBInput-should-have-message-and-label-next-.txt b/showcases/screen-reader/__snapshots__/macos/webkit/DBInput-should-have-message-and-label-next-.txt new file mode 100644 index 00000000000..2a265deac6b --- /dev/null +++ b/showcases/screen-reader/__snapshots__/macos/webkit/DBInput-should-have-message-and-label-next-.txt @@ -0,0 +1 @@ +["Label (Default) Basic edit text","Label","Label Helper Message edit text","Helper Message"] \ No newline at end of file diff --git a/showcases/screen-reader/__snapshots__/macos/webkit/DBRadio-should-label-duplicated-next-.txt b/showcases/screen-reader/__snapshots__/macos/webkit/DBRadio-should-label-duplicated-next-.txt new file mode 100644 index 00000000000..a327eb8abc8 --- /dev/null +++ b/showcases/screen-reader/__snapshots__/macos/webkit/DBRadio-should-label-duplicated-next-.txt @@ -0,0 +1 @@ +["Functional radio button, 1 of 3","Functional","(Default) Regular radio button, 2 of 3","(Default) Regular","Expressive radio button, 3 of 3","Expressive"] \ No newline at end of file diff --git a/showcases/screen-reader/__snapshots__/windows/chromium/DBButton-should-not-have-icon-in-screen-reader-next-.txt b/showcases/screen-reader/__snapshots__/windows/chromium/DBButton-should-not-have-icon-in-screen-reader-next-.txt new file mode 100644 index 00000000000..5966d60a9d2 --- /dev/null +++ b/showcases/screen-reader/__snapshots__/windows/chromium/DBButton-should-not-have-icon-in-screen-reader-next-.txt @@ -0,0 +1 @@ +["button, (Default) Text","button, Icon and Text","button, Icon"] \ No newline at end of file diff --git a/showcases/screen-reader/__snapshots__/windows/chromium/DBButton-should-not-have-icon-in-screen-reader-tab-.txt b/showcases/screen-reader/__snapshots__/windows/chromium/DBButton-should-not-have-icon-in-screen-reader-tab-.txt new file mode 100644 index 00000000000..f7e1b68df91 --- /dev/null +++ b/showcases/screen-reader/__snapshots__/windows/chromium/DBButton-should-not-have-icon-in-screen-reader-tab-.txt @@ -0,0 +1 @@ +["(Default) Text, button","Icon and Text, button","Icon, button"] \ No newline at end of file diff --git a/showcases/screen-reader/__snapshots__/windows/chromium/DBInput-should-have-message-and-label-tab-.txt b/showcases/screen-reader/__snapshots__/windows/chromium/DBInput-should-have-message-and-label-tab-.txt new file mode 100644 index 00000000000..5a7d07d2153 --- /dev/null +++ b/showcases/screen-reader/__snapshots__/windows/chromium/DBInput-should-have-message-and-label-tab-.txt @@ -0,0 +1 @@ +["Label, edit, (Default) Basic, blank","Label, edit, Helper Message, Helper Message, blank"] \ No newline at end of file diff --git a/showcases/screen-reader/__snapshots__/windows/chromium/DBRadio-should-label-duplicated-arrows-.txt b/showcases/screen-reader/__snapshots__/windows/chromium/DBRadio-should-label-duplicated-arrows-.txt new file mode 100644 index 00000000000..b129506f450 --- /dev/null +++ b/showcases/screen-reader/__snapshots__/windows/chromium/DBRadio-should-label-duplicated-arrows-.txt @@ -0,0 +1 @@ +["Expressive, radio button, checked, 3 of 3","Functional, radio button, checked, 1 of 3","(Default) Regular, radio button, checked, 2 of 3"] \ No newline at end of file diff --git a/showcases/screen-reader/__snapshots__/windows/chromium/DBRadio-should-label-duplicated-next-.txt b/showcases/screen-reader/__snapshots__/windows/chromium/DBRadio-should-label-duplicated-next-.txt new file mode 100644 index 00000000000..b2f044f3e61 --- /dev/null +++ b/showcases/screen-reader/__snapshots__/windows/chromium/DBRadio-should-label-duplicated-next-.txt @@ -0,0 +1 @@ +["(Default) Regular, radio button, checked, 2 of 3","Expressive, radio button, checked, 3 of 3","Functional, radio button, checked, 1 of 3"] \ No newline at end of file diff --git a/showcases/screen-reader/data.ts b/showcases/screen-reader/data.ts new file mode 100644 index 00000000000..dad6df64834 --- /dev/null +++ b/showcases/screen-reader/data.ts @@ -0,0 +1,44 @@ +import { + type Page, + type PlaywrightTestArgs, + type PlaywrightTestOptions, + type PlaywrightWorkerArgs, + type PlaywrightWorkerOptions, + type TestType +} from '@playwright/test'; +import { + type NVDAPlaywright, + type VoiceOverPlaywright +} from '@guidepup/playwright'; + +export type ScreenReaderTestType = TestType< + PlaywrightTestArgs & + PlaywrightTestOptions & { + nvda?: NVDAPlaywright; + voiceOver?: VoiceOverPlaywright; + }, + PlaywrightWorkerArgs & PlaywrightWorkerOptions +>; + +export type DefaultTestType = { + test?: ScreenReaderTestType; + title: string; + url: string; + testFn?: ( + voiceOver?: VoiceOverPlaywright, + nvda?: NVDAPlaywright + ) => Promise; + postTestFn?: ( + voiceOver?: VoiceOverPlaywright, + nvda?: NVDAPlaywright, + retry?: number + ) => Promise; + additionalParams?: string; +}; + +export type RunTestType = { + page: Page; + retry: number; + nvda?: NVDAPlaywright; + voiceOver?: VoiceOverPlaywright; +}; diff --git a/showcases/screen-reader/default.ts b/showcases/screen-reader/default.ts new file mode 100644 index 00000000000..61e4188479a --- /dev/null +++ b/showcases/screen-reader/default.ts @@ -0,0 +1,152 @@ +/* eslint-disable import/no-anonymous-default-export */ +import { platform } from 'node:os'; +import { + type NVDAPlaywright, + nvdaTest, + type VoiceOverPlaywright, + voiceOverTest +} from '@guidepup/playwright'; +import { macOSRecord, windowsRecord } from '@guidepup/guidepup'; +import { expect } from '@playwright/test'; +import { + type DefaultTestType, + type RunTestType, + type ScreenReaderTestType +} from './data'; + +const translations: Record = { + button: ['Schalter'], + edit: ['Eingabefeld'], + 'radio button': ['Auswahlschalter'], + blank: ['Leer'], + checked: ['aktiviert'], + ' of ': [' von '], + clickable: ['anklickbar'], + 'has auto complete': ['mit Auto Vervollstรคndigung'] +}; + +const cleanSpeakInstructions = (phraseLog: string[]): string[] => + phraseLog.map((phrase) => + phrase + .split('. ') + .filter( + (sPhrase) => + !( + sPhrase.includes('You are currently') || + sPhrase.includes('To enter') || + sPhrase.includes('To exit') || + sPhrase.includes('To click') || + sPhrase.includes('To select') || + sPhrase.includes('To interact') || + sPhrase.includes('Press Control') + ) + ) + .join('. ') + ); + +export const generateSnapshot = async ( + screenReader?: VoiceOverPlaywright | NVDAPlaywright, + retry?: number +) => { + if (!screenReader) return; + + let phraseLog: string[] = await screenReader.spokenPhraseLog(); + + if (retry && retry > 0) { + process.stdout.write(JSON.stringify(phraseLog)); + } + + phraseLog = cleanSpeakInstructions(phraseLog); + + let snapshot = JSON.stringify(phraseLog); + + for (const [key, values] of Object.entries(translations)) { + for (const value of values) { + snapshot = snapshot.replaceAll(value, key); + } + } + + expect(snapshot).toMatchSnapshot(); +}; + +export const runTest = async ({ + title, + url, + testFn, + postTestFn, + additionalParams, + page, + nvda, + voiceOver, + retry +}: DefaultTestType & RunTestType) => { + await page.goto(`${url}${additionalParams}`, { + waitUntil: 'networkidle' + }); + await page.waitForTimeout(500); + + let recorder: (() => void) | undefined; + + if (retry > 0) { + const path = `./recordings/${title}-${retry}-${Date.now()}.mp4`; + recorder = isWin() ? windowsRecord(path) : macOSRecord(path); + } + + const screenRecorder: VoiceOverPlaywright | NVDAPlaywright | undefined = + nvda ?? voiceOver; + if (!screenRecorder) return; + + /** + * In macOS:Webkit the [automaticallySpeakWebPage](https://github.com/guidepup/guidepup/blob/main/src/macOS/VoiceOver/configureSettings.ts#L58) is active. + * Therefore, we need to move back with the cursor to the start and delete the logs before starting. + * In windows:Chrome the cursor is on the middle element. + * Therefore, we need to move back and delete the logs, and then start everything. + */ + await screenRecorder.navigateToWebContent(); + await page.waitForTimeout(500); + + await testFn?.(voiceOver, nvda); + await postTestFn?.(voiceOver, nvda, retry); + recorder?.(); +}; + +export const testDefault = (defaultTestType: DefaultTestType) => { + const { test, title, additionalParams, postTestFn } = defaultTestType; + const fallbackPostFn = async (voiceOver, nvda, retry) => { + await generateSnapshot(voiceOver ?? nvda, retry); + }; + + const testType: DefaultTestType = { + ...defaultTestType, + postTestFn: postTestFn ?? fallbackPostFn, + additionalParams: + additionalParams ?? '&color=neutral-bg-lvl-1&density=regular' + }; + + if (isWin()) { + test?.(title, async ({ page, nvda }, { retry }) => { + await runTest({ + ...testType, + page, + nvda, + retry + }); + }); + } else { + test?.(title, async ({ page, voiceOver }, { retry }) => { + await runTest({ + ...testType, + page, + voiceOver, + retry + }); + }); + } +}; + +const isWin = (): boolean => platform() === 'win32'; + +export const getTest = (): ScreenReaderTestType => + isWin() ? nvdaTest : voiceOverTest; + +export default { testDefault, generateSnapshot, getTest }; diff --git a/showcases/screen-reader/tests/button.spec.ts b/showcases/screen-reader/tests/button.spec.ts new file mode 100644 index 00000000000..c74590f698a --- /dev/null +++ b/showcases/screen-reader/tests/button.spec.ts @@ -0,0 +1,39 @@ +import { getTest, testDefault } from '../default'; + +const test = getTest(); + +test.describe('DBButton', () => { + testDefault({ + test, + title: 'should not have icon in screen reader (next)', + url: './#/02/button?page=content', + async testFn(voiceOver, nvda) { + if (nvda) { + await nvda?.next(); + } + + const screenReader = voiceOver ?? nvda; + await screenReader?.clearSpokenPhraseLog(); + await screenReader?.previous(); + await screenReader?.next(); + await screenReader?.next(); + } + }); + testDefault({ + test, + title: 'should not have icon in screen reader (tab)', + url: './#/02/button?page=content', + async testFn(voiceOver, nvda) { + if (voiceOver) { + // Voiceover isn't working with tab in pipeline + test.skip(); + } + + await nvda?.press('Tab'); + await nvda?.clearSpokenPhraseLog(); + await nvda?.press('Shift+Tab'); + await nvda?.press('Tab'); + await nvda?.press('Tab'); + } + }); +}); diff --git a/showcases/screen-reader/tests/input.spec.ts b/showcases/screen-reader/tests/input.spec.ts new file mode 100644 index 00000000000..67ac4e5779d --- /dev/null +++ b/showcases/screen-reader/tests/input.spec.ts @@ -0,0 +1,41 @@ +import { NVDAKeyCodeCommands } from '@guidepup/guidepup'; +import { getTest, testDefault } from '../default'; + +const test = getTest(); +test.describe('DBInput', () => { + testDefault({ + test, + title: 'should have message and label (next)', + url: './#/03/input?page=variant%20helper%20message', + async testFn(voiceOver, nvda) { + if (nvda) { + // Nvda doesn't have a next if the element is an input + test.skip(); + } + + // We are on the label after loading + // Every element (input, label) will be read as single element + await voiceOver?.next(); + await voiceOver?.next(); + await voiceOver?.next(); + await voiceOver?.next(); + } + }); + // We don't test default "next" here because we will be locked inside the textarea + testDefault({ + test, + title: 'should have message and label (tab)', + url: './#/03/input?page=variant%20helper%20message', + async testFn(voiceOver, nvda) { + if (voiceOver) { + // Voiceover isn't working with tab in pipeline + test.skip(); + } + + await nvda?.press('Tab'); + await nvda?.clearSpokenPhraseLog(); + await nvda?.press('Shift+Tab'); + await nvda?.press('Tab'); + } + }); +}); diff --git a/showcases/screen-reader/tests/radio.spec.ts b/showcases/screen-reader/tests/radio.spec.ts new file mode 100644 index 00000000000..c732100998f --- /dev/null +++ b/showcases/screen-reader/tests/radio.spec.ts @@ -0,0 +1,45 @@ +import { getTest, testDefault } from '../default'; + +const test = getTest(); +test.describe('DBRadio', () => { + testDefault({ + test, + title: 'should label duplicated (next)', + url: './#/03/radio?page=density', + async testFn(voiceOver, nvda) { + if (nvda) { + await nvda?.next(); + await nvda?.clearSpokenPhraseLog(); + await nvda?.previous(); + await nvda?.next(); + await nvda?.next(); + } else if (voiceOver) { + // We are on the radio group after loading + // Every element (radio, label) will be read as single element + await voiceOver?.next(); + await voiceOver?.next(); + await voiceOver?.next(); + await voiceOver?.next(); + await voiceOver?.next(); + await voiceOver?.next(); + } + } + }); + testDefault({ + test, + title: 'should label duplicated (arrows)', + url: './#/03/radio?page=density', + async testFn(voiceOver, nvda) { + if (voiceOver) { + // Voiceover isn't working with tab in pipeline + test.skip(); + } + + await nvda?.press('Left'); + await nvda?.clearSpokenPhraseLog(); + await nvda?.press('Left'); + await nvda?.press('Right'); + await nvda?.press('Right'); + } + }); +}); diff --git a/showcases/screen-reader/tsconfig.json b/showcases/screen-reader/tsconfig.json new file mode 100644 index 00000000000..8d3f226db40 --- /dev/null +++ b/showcases/screen-reader/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true, + "strictNullChecks": true + } +}