From 2224383ab9f9850185b9bcaacb5d46b573a605b0 Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 7 Jan 2024 13:45:36 +0000 Subject: [PATCH 01/18] feat: NVDA support --- .github/CODEOWNERS.md | 1 + .github/CONTRIBUTING.md | 49 ++++ .github/ISSUE_TEMPLATE.md | 13 ++ .github/PULL_REQUEST_TEMPLATE.md | 11 + .github/workflows/test-nvda.yml | 35 +++ .github/workflows/test-voiceover.yml | 2 +- CODE_OF_CONDUCT.md | 128 ++++++++++ README.md | 218 ++++++++++++------ SECURITY.md | 43 ++++ examples/delay.ts | 3 + examples/log.ts | 2 + .../logIncludesExpectedPhrases.ts | 0 examples/playwright-nvda/README.md | 18 ++ .../playwright-nvda}/chromium.config.ts | 6 +- .../playwright-nvda}/firefox.config.ts | 6 +- .../tests/chromium/chromium.spec.ts | 48 ++++ .../chromium.spokenPhrase.snapshot.json | 0 .../tests/firefox/firefox.spec.ts | 48 ++++ .../firefox.spokenPhrase.snapshot.json | 0 .../playwright-nvda/tests/headerNavigation.ts | 45 ++++ .../playwright-voiceover}/README.md | 8 +- .../playwright-voiceover/chromium.config.ts | 18 ++ .../playwright-voiceover/firefox.config.ts | 18 ++ .../chromium/chromium.itemText.snapshot.json | 0 .../tests/chromium/chromium.spec.ts | 4 +- .../chromium.spokenPhrase.snapshot.json | 0 .../firefox/firefox.itemText.snapshot.json | 0 .../tests/firefox/firefox.spec.ts | 4 +- .../firefox.spokenPhrase.snapshot.json | 8 + .../tests/headerNavigation.ts | 22 +- .../webkit/webkit.itemText.snapshot.json | 0 .../tests/webkit/webkit.spec.ts | 4 +- .../webkit/webkit.spokenPhrase.snapshot.json | 8 + .../playwright-voiceover}/webkit.config.ts | 6 +- package.json | 23 +- src/applicationNameMap.ts | 10 + src/index.ts | 5 +- src/nvdaTest.ts | 59 +++++ src/screenReaderConfig.ts | 27 +++ src/voConfig.ts | 22 -- src/{voTest.ts => voiceOverTest.ts} | 31 ++- yarn.lock | 134 +++++------ 42 files changed, 871 insertions(+), 216 deletions(-) create mode 100644 .github/CODEOWNERS.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/test-nvda.yml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 SECURITY.md create mode 100644 examples/delay.ts create mode 100644 examples/log.ts rename {example/tests => examples}/logIncludesExpectedPhrases.ts (100%) create mode 100644 examples/playwright-nvda/README.md rename {example => examples/playwright-nvda}/chromium.config.ts (71%) rename {example => examples/playwright-nvda}/firefox.config.ts (71%) create mode 100644 examples/playwright-nvda/tests/chromium/chromium.spec.ts rename {example => examples/playwright-nvda}/tests/chromium/chromium.spokenPhrase.snapshot.json (100%) create mode 100644 examples/playwright-nvda/tests/firefox/firefox.spec.ts rename {example => examples/playwright-nvda}/tests/firefox/firefox.spokenPhrase.snapshot.json (100%) create mode 100644 examples/playwright-nvda/tests/headerNavigation.ts rename {example => examples/playwright-voiceover}/README.md (57%) create mode 100644 examples/playwright-voiceover/chromium.config.ts create mode 100644 examples/playwright-voiceover/firefox.config.ts rename {example => examples/playwright-voiceover}/tests/chromium/chromium.itemText.snapshot.json (100%) rename {example => examples/playwright-voiceover}/tests/chromium/chromium.spec.ts (92%) rename example/tests/webkit/webkit.spokenPhrase.snapshot.json => examples/playwright-voiceover/tests/chromium/chromium.spokenPhrase.snapshot.json (100%) rename {example => examples/playwright-voiceover}/tests/firefox/firefox.itemText.snapshot.json (100%) rename {example => examples/playwright-voiceover}/tests/firefox/firefox.spec.ts (92%) create mode 100644 examples/playwright-voiceover/tests/firefox/firefox.spokenPhrase.snapshot.json rename {example => examples/playwright-voiceover}/tests/headerNavigation.ts (59%) rename {example => examples/playwright-voiceover}/tests/webkit/webkit.itemText.snapshot.json (100%) rename {example => examples/playwright-voiceover}/tests/webkit/webkit.spec.ts (92%) create mode 100644 examples/playwright-voiceover/tests/webkit/webkit.spokenPhrase.snapshot.json rename {example => examples/playwright-voiceover}/webkit.config.ts (71%) create mode 100644 src/applicationNameMap.ts create mode 100644 src/nvdaTest.ts create mode 100644 src/screenReaderConfig.ts delete mode 100644 src/voConfig.ts rename src/{voTest.ts => voiceOverTest.ts} (52%) diff --git a/.github/CODEOWNERS.md b/.github/CODEOWNERS.md new file mode 100644 index 0000000..b71c6aa --- /dev/null +++ b/.github/CODEOWNERS.md @@ -0,0 +1 @@ +* @cmorten diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..a6438ff --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# Contributing to this repository + +First of all, thanks for taking the time to read this document and contributing to our codebase! + +## Getting started + +If you're working on an existing issue then awesome! Let us know by dropping a comment in the issue. + +If it's a new bug fix or feature that you would like to contribute, then please raise an issue so it can be tracked (and to help out others who are experiencing the same issue / want the new thing know that it's being looked at!). Be sure to check for existing issues before raising your own! + +## Working on your feature + +### Branching + +On this project we follow mainline development (or trunk based development), and our default branch is `main`. + +Therefore you need to branch from `main` and merge into `main`. + +### Coding style + +Generally try to match the style and conventions of the code around your changes. Ultimately we want code that is clear, concise, consistent and easy to read. + +We use `eslint` and `prettier` for linting. You can check and correct the code style using the following commands: + +```console +# Check linting +yarn lint + +# Fix linting +yarn lint:fix +``` + +### Tests + +Before opening a PR, please run the following command to make sure your branch will build and pass all the checks and tests: + +```console +yarn test +``` + +## Opening a PR + +Once you're confident your branch is ready to review, open a PR against `main` on this repo. + +Please use the PR template as a guide, but if your change doesn't quite fit it, feel free to customize. + +## Merging and publishing + +When your feature branch / PR has been tested and has an approval, it is then ready to merge. Please contact a maintainer to action the merge. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..86dfee2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,13 @@ +# Issue + +Setup: + +- Node version: +- Guidepup version: +- Guidepup Playwright version: +- OS Platform: +- OS Release: + +## Details + +> Please replace this quote block with the details of the feature / bug you wish to be addressed. If it is a bug please do your best to add steps to reproduce. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..e28ba30 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,11 @@ +# Issue + +Fixes #. + +## Details + +> Please replace this quote block with a brief summary of PR purpose and code changes. + +## CheckList + +- [ ] Has been tested (where required). diff --git a/.github/workflows/test-nvda.yml b/.github/workflows/test-nvda.yml new file mode 100644 index 0000000..dd97692 --- /dev/null +++ b/.github/workflows/test-nvda.yml @@ -0,0 +1,35 @@ +name: Test NVDA + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test-nvda: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-2019, windows-2022] + browser: [chromium, firefox] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 20 + - name: Guidepup Setup + uses: guidepup/setup-action@0.13.0 + with: + record: true + - run: yarn install --frozen-lockfile + - run: yarn pretest + - run: yarn test:nvda:${{ matrix.browser }} + - uses: actions/upload-artifact@v3 + if: always() + continue-on-error: true + with: + name: artifacts + path: | + **/test-results/**/* + **/recordings/**/* diff --git a/.github/workflows/test-voiceover.yml b/.github/workflows/test-voiceover.yml index 38af8ee..5604f45 100644 --- a/.github/workflows/test-voiceover.yml +++ b/.github/workflows/test-voiceover.yml @@ -24,7 +24,7 @@ jobs: record: true - run: yarn install --frozen-lockfile - run: yarn pretest - - run: yarn test:${{ matrix.browser }} + - run: yarn test:voiceover:${{ matrix.browser }} - uses: actions/upload-artifact@v3 if: always() continue-on-error: true diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..7c174dd --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. All complaints will be reviewed and investigated promptly +and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +. Translations are available at +. diff --git a/README.md b/README.md index deb21d5..488aa3c 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,44 @@ -

Guidepup Playwright

-

- Screen reader driver for Playwright tests. -

-

- @guidepup/playwright available on NPM - @guidepup/playwright available on NPM - @guidepup/playwright test workflows - @guidepup/playwright uses the MIT license -

-

- Reliable automation for your screen reader a11y workflows in Playwright supporting: -

-

- VoiceOver on MacOS -

-

- NVDA on Windows - Coming Soon! -

- -## Intro - -A11y static analysis tools [only cover 25% of WCAG](https://karlgroves.com/web-accessibility-testing-what-can-be-tested-and-how/) and don't assure on the quality of the user experience for screen reader users. This means teams need to perform lots of manual tests with multiple screen readers to ensure great UX which can take a lot of time... **not anymore!** - -With [Guidepup](https://www.guidepup.dev/) you can automate your screen reader test workflows the same you as would for mouse or keyboard based scenarios, no sweat! - -## Quick Features - -- **Full Control** - if a screen reader has a keyboard command, then Guidepup supports it. -- **Mirrors Real User Experience** - assert on what users really do and hear when using screen readers. -- **Framework Agnostic** - run with Jest, with Playwright, as an independent script, no vendor lock-in. +# Guidepup Playwright + +@guidepup/playwright available on NPM +@guidepup/playwright test workflows +@guidepup/playwright uses the MIT license + +## [Documentation](https://guidepup.dev) | [API Reference](https://www.guidepup.dev/docs/api/class-guidepup) + +[![MacOS Big Sur Support](https://img.shields.io/badge/macos-Big_Sur-blue.svg?logo=apple)](https://apps.apple.com/id/app/macos-big-sur/id1526878132) +[![MacOS Monetary Support](https://img.shields.io/badge/macos-Monetary-blue.svg?logo=apple)](https://apps.apple.com/us/app/macos-monterey/id1576738294) +[![MacOS Ventura Support](https://img.shields.io/badge/macos-Ventura-blue.svg?logo=apple)](https://apps.apple.com/us/app/macos-ventura/id1638787999) +[![Windows 10 Support](https://img.shields.io/badge/windows-10-blue.svg?logo=windows10)](https://www.microsoft.com/en-gb/software-download/windows10ISO) +[![Windows Server 2019 Support](https://img.shields.io/badge/windows_server-2019-blue.svg?logo=windows)](https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2019) +[![Windows Server 2022 Support](https://img.shields.io/badge/windows_server-2022-blue.svg?logo=windows)](https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2022) + +This package provides [Guidepup](https://github.com/guidepup/guidepup) integration with [Playwright](https://playwright.dev/) for writing screen reader tests that automate VoiceOver on MacOS and NVDA on Windows. + +## Capabilities + +- **Full Control** - If a screen reader has a keyboard command, then Guidepup supports it. +- **Mirrors Real User Experience** - Assert on what users really do and hear when using screen readers. +- **Framework Agnostic** - Run with Jest, with Playwright, as an independent script, no vendor lock-in. ## Getting Started Set up your environment for screen reader automation with [`@guidepup/setup`](https://github.com/guidepup/setup): -```console +```sh npx @guidepup/setup ``` +If you are using GitHub Actions, check out the dedicated [`guidepup/setup-action`](https://github.com/marketplace/actions/guidepup-setup): + +```yaml +- name: Setup Environment + uses: guidepup/setup-action +``` + Install `@guidepup/playwright` to your project: -```console +```sh npm install --save-dev @guidepup/playwright @guidepup/guidepup @playwright/test ``` @@ -48,80 +46,162 @@ Note: you require `@guidepup/guidepup` and `@playwright/test` as they are peer d And get cracking with your first screen reader tests in Playwright! +## Examples + +Head over to the [Guidepup Website](https://www.guidepup.dev/) for guides, real world examples, environment setup, and complete API documentation with examples. + +You can also check out these [awesome examples](https://github.com/guidepup/guidepup/tree/main/examples) to learn how you could use Guidepup with Playwright in your projects. + +Alternatively check out [this project](https://github.com/guidepup/aria-at-tests) which runs several thousand tests to assert screen reader compatibility against [W3C ARIA-AT](https://github.com/w3c/aria-at) test suite. + +### Playwright Config + +In your `playwright.config.ts` add the following for the best results with Guidepup for Screen Reader automation: + +```ts +import { devices, PlaywrightTestConfig } from "@playwright/test"; +import { screenReaderConfig } from "@guidepup/playwright"; + +const config: PlaywrightTestConfig = { + ...screenReaderConfig, + + // ... your custom config +}; + +export default config; +``` + +Check out the configuration this adds [in the `config.ts`` file](./src/config.ts). + +### VoiceOver + +`playwright.config.ts`: + ```ts -import { voTest as test } from "@guidepup/playwright"; +import { devices, PlaywrightTestConfig } from "@playwright/test"; +import { screenReaderConfig } from "@guidepup/playwright"; + +const config: PlaywrightTestConfig = { + ...screenReaderConfig, + reportSlowTests: null, + timeout: 5 * 60 * 1000, + retries: 2, + projects: [ + { + name: "webkit", + use: { ...devices["Desktop Safari"], headless: false }, + }, + ], +}; + +export default config; +``` + +`voiceOver.spec.ts`: + +```ts +import { voiceOverTest as test } from "@guidepup/playwright"; import { expect } from "@playwright/test"; test.describe("Playwright VoiceOver", () => { - test("I can navigate the Guidepup Github page", async ({ + test("I can navigate the Guidepup Github page with VoiceOver", async ({ page, voiceOver, }) => { // Navigate to Guidepup GitHub page await page.goto("https://github.com/guidepup/guidepup", { - waitUntil: "domcontentloaded", + waitUntil: "load", }); - // Wait for page to be ready and interact - await expect(page.locator('header[role="banner"]')).toBeVisible(); + // Wait for page to be ready + const header = page.locator('header[role="banner"]'); + await header.waitFor(); + + // Interact with the page await voiceOver.interact(); + await voiceOver.perform(voiceOver.keyboardCommands.jumpToLeftEdge); // Move across the page menu to the Guidepup heading using VoiceOver while ((await voiceOver.itemText()) !== "Guidepup heading level 1") { - await voiceOver.perform(voiceOver.keyboard.commands.findNextHeading); + await voiceOver.perform(voiceOver.keyboardCommands.findNextHeading); } + + // Assert that the spoken phrases are as expected + expect(JSON.stringify(await voiceOver.spokenPhraseLog())).toMatchSnapshot(); + }); }); ``` -## Playwright Config +### NVDA -In your `playwright.config.ts` add the following for the best results with -Guidepup for VoiceOver automation. +`playwright.config.ts`: ```ts import { devices, PlaywrightTestConfig } from "@playwright/test"; -import { voConfig } from "@guidepup/playwright"; +import { screenReaderConfig } from "@guidepup/playwright"; const config: PlaywrightTestConfig = { - ...voConfig, - - // Your custom config ... + ...screenReaderConfig, + reportSlowTests: null, + timeout: 5 * 60 * 1000, + retries: 2, + projects: [ + { + name: "firefox", + use: { ...devices["Desktop Firefox"], headless: false }, + }, + ], }; export default config; ``` -Check out the configuration this adds [in the voConfig.ts file](./src/voConfig.ts). +`nvda.spec.ts`: -## Environment Setup +```ts +import { nvdaTest as test } from "@guidepup/playwright"; +import { expect } from "@playwright/test"; -Set up your environment for screen-read automation with [`@guidepup/setup`](https://github.com/guidepup/setup): +test.describe("Playwright NVDA", () => { + test("I can navigate the Guidepup Github page with NVDA", async ({ + page, + nvda, + }) => { + // Navigate to Guidepup GitHub page + await page.goto("https://github.com/guidepup/guidepup", { + waitUntil: "load", + }); -```console -npx @guidepup/setup -``` + // Wait for page to be ready and setup + const header = page.locator('header[role="banner"]'); + await header.waitFor(); + await page.locator("a").first().focus(); + await nvda.perform(nvda.keyboardCommands.exitFocusMode); -If you are using GitHub Actions, check out the dedicated [`guidepup/setup-action`](https://github.com/marketplace/actions/guidepup-setup): + // Move across the page menu to the Guidepup heading using NVDA + while ((await nvda.itemText()) !== "Guidepup heading level 1") { + await nvda.perform(nvda.keyboardCommands.moveToNextHeading); + } -```yaml -- name: Setup Environment - uses: guidepup/setup-action@0.13.0 + // Assert that the spoken phrases are as expected + expect(JSON.stringify(await nvda.spokenPhraseLog())).toMatchSnapshot(); + }); +}); ``` -## Documentation - -Head over to the [Guidepup Website](https://www.guidepup.dev/) for guides, real world examples, environment setup, and complete Guidepup API documentation with examples. - -## Example - -Check out [this cross-browser VoiceOver example](./example/). +## Powerful Tooling -## See Also +Check out some of the other Guidepup modules: -Checkout the core [`@guidepup/guidepup`](https://github.com/guidepup/guidepup) -project to learn more about how you can automate your screen reader workflows -using Guidepup. +- [`@guidepup/guidepup`](https://github.com/guidepup/guidepup/) - Reliable automation for your screen reader a11y workflows through JavaScript supporting VoiceOver and NVDA. +- [`@guidepup/setup`](https://github.com/guidepup/setup/) - Set up your local or CI environment for screen reader test automation. +- [`@guidepup/virtual-screen-reader`](https://github.com/guidepup/virtual-screen-reader/) - Reliable unit testing for your screen reader a11y workflows. +- [`@guidepup/jest`](https://github.com/guidepup/jest/) - Jest matchers for reliable unit testing of your screen reader a11y workflows. -## License +## Resources -[MIT](https://github.com/guidepup/guidepup/blob/main/LICENSE) +- [Documentation](https://www.guidepup.dev/docs/example) +- [API Reference](https://www.guidepup.dev/docs/api/class-guidepup) +- [Contributing](.github/CONTRIBUTING.md) +- [Changelog](https://github.com/guidepup/guidepup-playwright/releases) +- [MIT License](https://github.com/guidepup/guidepup-playwright/blob/main/LICENSE) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..9d78d99 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,43 @@ +# Security Policies and Procedures + +This document outlines security procedures and general policies for the Guidepup Playwright +project. + +- [Reporting a Bug](#reporting-a-bug) +- [Disclosure Policy](#disclosure-policy) +- [Comments on this Policy](#comments-on-this-policy) + +## Reporting a Bug + +The Guidepup Playwright maintainers and community take all security bugs in Guidepup Playwright seriously. +Thank you for improving the security of Guidepup Playwright. We appreciate your efforts and +responsible disclosure and will make every effort to acknowledge your +contributions. + +Report security bugs by emailing the lead maintainer in the +[CODEOWNERS](.github/CODEOWNERS.md) file. + +The lead maintainer will acknowledge your email, and will send a response +indicating the next steps in handling your report. After the initial reply +to your report, the maintainer team will endeavor to keep you informed of the +progress towards a fix and full announcement, and may ask for additional +information or guidance. + +Report security bugs in third-party modules to the person or team maintaining +the module. + +## Disclosure Policy + +When the maintainer team receives a security bug report, they will assign it to a +primary handler. This person will coordinate the fix and release process, +involving the following steps: + +- Confirm the problem and determine the affected versions. +- Audit code to find any potential similar problems. +- Prepare fixes for all releases still under maintenance. These fixes will be + released as fast as possible to GitHub. + +## Comments on this Policy + +If you have suggestions on how this process could be improved please submit a +pull request. diff --git a/examples/delay.ts b/examples/delay.ts new file mode 100644 index 0000000..4493928 --- /dev/null +++ b/examples/delay.ts @@ -0,0 +1,3 @@ +export async function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/examples/log.ts b/examples/log.ts new file mode 100644 index 0000000..145e3d1 --- /dev/null +++ b/examples/log.ts @@ -0,0 +1,2 @@ +export const log = (...messages) => + console.log(`[${new Date().toUTCString()}]`, ...messages); diff --git a/example/tests/logIncludesExpectedPhrases.ts b/examples/logIncludesExpectedPhrases.ts similarity index 100% rename from example/tests/logIncludesExpectedPhrases.ts rename to examples/logIncludesExpectedPhrases.ts diff --git a/examples/playwright-nvda/README.md b/examples/playwright-nvda/README.md new file mode 100644 index 0000000..4d6960d --- /dev/null +++ b/examples/playwright-nvda/README.md @@ -0,0 +1,18 @@ +# NVDA Example + +An example demonstrating using Guidepup for testing NVDA automation with [Playwright](https://playwright.dev/). + +Run this example's test with the following from the root directory: + +```console +npm run test +``` + +> Note: please ensure you have [setup you environment](https://www.guidepup.dev/docs/guides/automated-environment-setup) for NVDA automation before running this example. + +## Test flow + +1. The test launches the browser using Playwright +2. Navigates to the GitHub website +3. Moves through the website using NVDA controlled by Guidepup +4. Traverses headings until the Guidepup heading in the README.md is found diff --git a/example/chromium.config.ts b/examples/playwright-nvda/chromium.config.ts similarity index 71% rename from example/chromium.config.ts rename to examples/playwright-nvda/chromium.config.ts index ca01485..bfbb9af 100644 --- a/example/chromium.config.ts +++ b/examples/playwright-nvda/chromium.config.ts @@ -1,15 +1,15 @@ import { devices, PlaywrightTestConfig } from "@playwright/test"; +import { screenReaderConfig } from "../../src"; const config: PlaywrightTestConfig = { + ...screenReaderConfig, reportSlowTests: null, - fullyParallel: false, - workers: 1, timeout: 5 * 60 * 1000, retries: 5, projects: [ { name: "chromium", - use: { ...devices["Desktop Chrome"], headless: false, video: "on" }, + use: { ...devices["Desktop Chrome"], headless: false }, }, ], reporter: process.env.CI ? [["github"], ["html", { open: "never" }]] : "list", diff --git a/example/firefox.config.ts b/examples/playwright-nvda/firefox.config.ts similarity index 71% rename from example/firefox.config.ts rename to examples/playwright-nvda/firefox.config.ts index 9653692..40e4349 100644 --- a/example/firefox.config.ts +++ b/examples/playwright-nvda/firefox.config.ts @@ -1,15 +1,15 @@ import { devices, PlaywrightTestConfig } from "@playwright/test"; +import { screenReaderConfig } from "../../src"; const config: PlaywrightTestConfig = { + ...screenReaderConfig, reportSlowTests: null, - fullyParallel: false, - workers: 1, timeout: 5 * 60 * 1000, retries: 5, projects: [ { name: "firefox", - use: { ...devices["Desktop Firefox"], headless: false, video: "on" }, + use: { ...devices["Desktop Firefox"], headless: false }, }, ], reporter: process.env.CI ? [["github"], ["html", { open: "never" }]] : "list", diff --git a/examples/playwright-nvda/tests/chromium/chromium.spec.ts b/examples/playwright-nvda/tests/chromium/chromium.spec.ts new file mode 100644 index 0000000..377f145 --- /dev/null +++ b/examples/playwright-nvda/tests/chromium/chromium.spec.ts @@ -0,0 +1,48 @@ +import { platform, release } from "os"; +import { headerNavigation } from "../headerNavigation"; +import { logIncludesExpectedPhrases } from "../../../logIncludesExpectedPhrases"; +import { windowsRecord } from "@guidepup/guidepup"; +import spokenPhraseSnapshot from "./chromium.spokenPhrase.snapshot.json"; +import { nvdaTest as test } from "../../../../src"; + +test.describe("Chromium Playwright NVDA", () => { + test("I can navigate the Guidepup Github page", async ({ + browser, + browserName, + page, + nvda, + }) => { + const osName = platform(); + const osVersion = release(); + const browserVersion = browser.version(); + const { retry } = test.info(); + const recordingFilePath = `./recordings/playwright-nvda-${osName}-${osVersion}-${browserName}-${browserVersion}-attempt-${retry}-${+new Date()}.mov`; + + console.table({ + osName, + osVersion, + browserName, + browserVersion, + retry, + }); + + let stopRecording: (() => void) | undefined; + + try { + stopRecording = windowsRecord(recordingFilePath); + + await headerNavigation({ page, nvda }); + + // Assert that we've ended up where we expected and what we were told on + // the way there is as expected. + + const spokenPhraseLog = await nvda.spokenPhraseLog(); + + console.log(JSON.stringify(spokenPhraseLog, undefined, 2)); + + logIncludesExpectedPhrases(spokenPhraseLog, spokenPhraseSnapshot); + } finally { + stopRecording?.(); + } + }); +}); diff --git a/example/tests/chromium/chromium.spokenPhrase.snapshot.json b/examples/playwright-nvda/tests/chromium/chromium.spokenPhrase.snapshot.json similarity index 100% rename from example/tests/chromium/chromium.spokenPhrase.snapshot.json rename to examples/playwright-nvda/tests/chromium/chromium.spokenPhrase.snapshot.json diff --git a/examples/playwright-nvda/tests/firefox/firefox.spec.ts b/examples/playwright-nvda/tests/firefox/firefox.spec.ts new file mode 100644 index 0000000..d6a4b3e --- /dev/null +++ b/examples/playwright-nvda/tests/firefox/firefox.spec.ts @@ -0,0 +1,48 @@ +import { platform, release } from "os"; +import { headerNavigation } from "../headerNavigation"; +import { logIncludesExpectedPhrases } from "../../../logIncludesExpectedPhrases"; +import { windowsRecord } from "@guidepup/guidepup"; +import spokenPhraseSnapshot from "./firefox.spokenPhrase.snapshot.json"; +import { nvdaTest as test } from "../../../../src"; + +test.describe("Firefox Playwright VoiceOver", () => { + test("I can navigate the Guidepup Github page", async ({ + browser, + browserName, + page, + nvda, + }) => { + const osName = platform(); + const osVersion = release(); + const browserVersion = browser.version(); + const { retry } = test.info(); + const recordingFilePath = `./recordings/playwright-nvda-${osName}-${osVersion}-${browserName}-${browserVersion}-attempt-${retry}-${+new Date()}.mov`; + + console.table({ + osName, + osVersion, + browserName, + browserVersion, + retry, + }); + + let stopRecording: (() => void) | undefined; + + try { + stopRecording = windowsRecord(recordingFilePath); + + await headerNavigation({ page, nvda }); + + // Assert that we've ended up where we expected and what we were told on + // the way there is as expected. + + const spokenPhraseLog = await nvda.spokenPhraseLog(); + + console.log(JSON.stringify(spokenPhraseLog, undefined, 2)); + + logIncludesExpectedPhrases(spokenPhraseLog, spokenPhraseSnapshot); + } finally { + stopRecording?.(); + } + }); +}); diff --git a/example/tests/firefox/firefox.spokenPhrase.snapshot.json b/examples/playwright-nvda/tests/firefox/firefox.spokenPhrase.snapshot.json similarity index 100% rename from example/tests/firefox/firefox.spokenPhrase.snapshot.json rename to examples/playwright-nvda/tests/firefox/firefox.spokenPhrase.snapshot.json diff --git a/examples/playwright-nvda/tests/headerNavigation.ts b/examples/playwright-nvda/tests/headerNavigation.ts new file mode 100644 index 0000000..dfbe20c --- /dev/null +++ b/examples/playwright-nvda/tests/headerNavigation.ts @@ -0,0 +1,45 @@ +import type { NVDA } from "@guidepup/guidepup"; +import { Page } from "@playwright/test"; +import { delay } from "../../delay"; +import { log } from "../../log"; + +const MAX_NAVIGATION_LOOP = 10; + +export async function headerNavigation({ + page, + nvda, +}: { + page: Page; + nvda: NVDA; +}) { + // Navigate to Guidepup GitHub page + log("Navigating to URL: https://github.com/guidepup/guidepup."); + await page.goto("https://github.com/guidepup/guidepup", { + waitUntil: "load", + }); + + // Wait for page to be ready and interact + const header = page.locator('header[role="banner"]'); + await header.waitFor(); + await delay(500); + await page.locator("a").first().focus(); + + // Make sure not in focus mode + log(`Performing command: "Escape"`); + await nvda.perform(nvda.keyboardCommands.exitFocusMode); + log(`Screen reader output: "${await nvda.lastSpokenPhrase()}".`); + + let headingCount = 0; + + // Move across the page menu to the Guidepup heading using NVDA + while ( + (await nvda.itemText()) !== "Guidepup heading level 1" && + headingCount <= MAX_NAVIGATION_LOOP + ) { + headingCount++; + + log(`Performing command: "VO+Command+H"`); + await nvda.perform(nvda.keyboardCommands.moveToNextHeading); + log(`Screen reader output: "${await nvda.lastSpokenPhrase()}".`); + } +} diff --git a/example/README.md b/examples/playwright-voiceover/README.md similarity index 57% rename from example/README.md rename to examples/playwright-voiceover/README.md index e79d6b6..35977a1 100644 --- a/example/README.md +++ b/examples/playwright-voiceover/README.md @@ -1,4 +1,4 @@ -# Example +# VoiceOver Example An example demonstrating using Guidepup for testing VoiceOver automation with [Playwright](https://playwright.dev/). @@ -8,7 +8,7 @@ Run this example's test with the following from the root directory: npm run test ``` -> Note: please ensure you have setup the [VoiceOver prerequisites](https://github.com/guidepup/guidepup/tree/main/guides/voiceover-prerequisites/README.md) before running this example. +> Note: please ensure you have [setup you environment](https://www.guidepup.dev/docs/guides/automated-environment-setup) for VoiceOver automation before running this example. ## Test flow @@ -16,7 +16,3 @@ npm run test 2. Navigates to the GitHub website 3. Moves through the website using VoiceOver controlled by Guidepup 4. Traverses headings until the Guidepup heading in the README.md is found - -## See also - -For a dedicated example of using Guidepup with Playwright and CircleCI see . diff --git a/examples/playwright-voiceover/chromium.config.ts b/examples/playwright-voiceover/chromium.config.ts new file mode 100644 index 0000000..bfbb9af --- /dev/null +++ b/examples/playwright-voiceover/chromium.config.ts @@ -0,0 +1,18 @@ +import { devices, PlaywrightTestConfig } from "@playwright/test"; +import { screenReaderConfig } from "../../src"; + +const config: PlaywrightTestConfig = { + ...screenReaderConfig, + reportSlowTests: null, + timeout: 5 * 60 * 1000, + retries: 5, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"], headless: false }, + }, + ], + reporter: process.env.CI ? [["github"], ["html", { open: "never" }]] : "list", +}; + +export default config; diff --git a/examples/playwright-voiceover/firefox.config.ts b/examples/playwright-voiceover/firefox.config.ts new file mode 100644 index 0000000..40e4349 --- /dev/null +++ b/examples/playwright-voiceover/firefox.config.ts @@ -0,0 +1,18 @@ +import { devices, PlaywrightTestConfig } from "@playwright/test"; +import { screenReaderConfig } from "../../src"; + +const config: PlaywrightTestConfig = { + ...screenReaderConfig, + reportSlowTests: null, + timeout: 5 * 60 * 1000, + retries: 5, + projects: [ + { + name: "firefox", + use: { ...devices["Desktop Firefox"], headless: false }, + }, + ], + reporter: process.env.CI ? [["github"], ["html", { open: "never" }]] : "list", +}; + +export default config; diff --git a/example/tests/chromium/chromium.itemText.snapshot.json b/examples/playwright-voiceover/tests/chromium/chromium.itemText.snapshot.json similarity index 100% rename from example/tests/chromium/chromium.itemText.snapshot.json rename to examples/playwright-voiceover/tests/chromium/chromium.itemText.snapshot.json diff --git a/example/tests/chromium/chromium.spec.ts b/examples/playwright-voiceover/tests/chromium/chromium.spec.ts similarity index 92% rename from example/tests/chromium/chromium.spec.ts rename to examples/playwright-voiceover/tests/chromium/chromium.spec.ts index 0a568c2..feed09b 100644 --- a/example/tests/chromium/chromium.spec.ts +++ b/examples/playwright-voiceover/tests/chromium/chromium.spec.ts @@ -1,10 +1,10 @@ import { platform, release } from "os"; import { headerNavigation } from "../headerNavigation"; import itemTextSnapshot from "./chromium.itemText.snapshot.json"; -import { logIncludesExpectedPhrases } from "../logIncludesExpectedPhrases"; +import { logIncludesExpectedPhrases } from "../../../logIncludesExpectedPhrases"; import { macOSRecord } from "@guidepup/guidepup"; import spokenPhraseSnapshot from "./chromium.spokenPhrase.snapshot.json"; -import { voTest as test } from "../../../src"; +import { voiceOverTest as test } from "../../../../src"; test.describe("Chromium Playwright VoiceOver", () => { test("I can navigate the Guidepup Github page", async ({ diff --git a/example/tests/webkit/webkit.spokenPhrase.snapshot.json b/examples/playwright-voiceover/tests/chromium/chromium.spokenPhrase.snapshot.json similarity index 100% rename from example/tests/webkit/webkit.spokenPhrase.snapshot.json rename to examples/playwright-voiceover/tests/chromium/chromium.spokenPhrase.snapshot.json diff --git a/example/tests/firefox/firefox.itemText.snapshot.json b/examples/playwright-voiceover/tests/firefox/firefox.itemText.snapshot.json similarity index 100% rename from example/tests/firefox/firefox.itemText.snapshot.json rename to examples/playwright-voiceover/tests/firefox/firefox.itemText.snapshot.json diff --git a/example/tests/firefox/firefox.spec.ts b/examples/playwright-voiceover/tests/firefox/firefox.spec.ts similarity index 92% rename from example/tests/firefox/firefox.spec.ts rename to examples/playwright-voiceover/tests/firefox/firefox.spec.ts index e967245..e9494c2 100644 --- a/example/tests/firefox/firefox.spec.ts +++ b/examples/playwright-voiceover/tests/firefox/firefox.spec.ts @@ -1,10 +1,10 @@ import { platform, release } from "os"; import { headerNavigation } from "../headerNavigation"; import itemTextSnapshot from "./firefox.itemText.snapshot.json"; -import { logIncludesExpectedPhrases } from "../logIncludesExpectedPhrases"; +import { logIncludesExpectedPhrases } from "../../../logIncludesExpectedPhrases"; import { macOSRecord } from "@guidepup/guidepup"; import spokenPhraseSnapshot from "./firefox.spokenPhrase.snapshot.json"; -import { voTest as test } from "../../../src"; +import { voiceOverTest as test } from "../../../../src"; test.describe("Firefox Playwright VoiceOver", () => { test("I can navigate the Guidepup Github page", async ({ diff --git a/examples/playwright-voiceover/tests/firefox/firefox.spokenPhrase.snapshot.json b/examples/playwright-voiceover/tests/firefox/firefox.spokenPhrase.snapshot.json new file mode 100644 index 0000000..ac558ea --- /dev/null +++ b/examples/playwright-voiceover/tests/firefox/firefox.spokenPhrase.snapshot.json @@ -0,0 +1,8 @@ +[ + "heading level 1 guidepup/guidepup", + "heading level 2 Latest commit", + "heading level 2 Git stats", + "heading level 2 Files", + "heading level 2 link README.md", + "heading level 1 Guidepup" +] diff --git a/example/tests/headerNavigation.ts b/examples/playwright-voiceover/tests/headerNavigation.ts similarity index 59% rename from example/tests/headerNavigation.ts rename to examples/playwright-voiceover/tests/headerNavigation.ts index 8225160..9d9e85d 100644 --- a/example/tests/headerNavigation.ts +++ b/examples/playwright-voiceover/tests/headerNavigation.ts @@ -1,9 +1,7 @@ import type { VoiceOver } from "@guidepup/guidepup"; import { Page } from "@playwright/test"; - -async function delay(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} +import { delay } from "../../delay"; +import { log } from "../../log"; const MAX_NAVIGATION_LOOP = 10; @@ -14,30 +12,38 @@ export async function headerNavigation({ page: Page; voiceOver: VoiceOver; }) { - // Navigate to Guidepup GitHub page 🎉 + // Navigate to Guidepup GitHub page + log("Navigating to URL: https://github.com/guidepup/guidepup."); await page.goto("https://github.com/guidepup/guidepup", { - waitUntil: "domcontentloaded", + waitUntil: "load", }); - // Wait for page to be ready and interact 🙌 + // Wait for page to be ready and interact const header = page.locator('header[role="banner"]'); await header.waitFor(); await delay(500); // Make sure interacting with the web content + log(`Performing command: "VO+Shift+Down Arrow"`); await voiceOver.interact(); + log(`Screen reader output: "${await voiceOver.lastSpokenPhrase()}".`); // Prevent auto-navigation of group + log(`Performing command: "VO+Shift+Left Arrow"`); await voiceOver.perform(voiceOver.keyboardCommands.jumpToLeftEdge); + log(`Screen reader output: "${await voiceOver.lastSpokenPhrase()}".`); let headingCount = 0; - // Move across the page menu to the Guidepup heading using VoiceOver 🔎 + // Move across the page menu to the Guidepup heading using VoiceOver while ( (await voiceOver.itemText()) !== "Guidepup heading level 1" && headingCount <= MAX_NAVIGATION_LOOP ) { headingCount++; + + log(`Performing command: "VO+Command+H"`); await voiceOver.perform(voiceOver.keyboardCommands.findNextHeading); + log(`Screen reader output: "${await voiceOver.lastSpokenPhrase()}".`); } } diff --git a/example/tests/webkit/webkit.itemText.snapshot.json b/examples/playwright-voiceover/tests/webkit/webkit.itemText.snapshot.json similarity index 100% rename from example/tests/webkit/webkit.itemText.snapshot.json rename to examples/playwright-voiceover/tests/webkit/webkit.itemText.snapshot.json diff --git a/example/tests/webkit/webkit.spec.ts b/examples/playwright-voiceover/tests/webkit/webkit.spec.ts similarity index 92% rename from example/tests/webkit/webkit.spec.ts rename to examples/playwright-voiceover/tests/webkit/webkit.spec.ts index 37a0cbc..93483ae 100644 --- a/example/tests/webkit/webkit.spec.ts +++ b/examples/playwright-voiceover/tests/webkit/webkit.spec.ts @@ -1,10 +1,10 @@ import { platform, release } from "os"; import { headerNavigation } from "../headerNavigation"; import itemTextSnapshot from "./webkit.itemText.snapshot.json"; -import { logIncludesExpectedPhrases } from "../logIncludesExpectedPhrases"; +import { logIncludesExpectedPhrases } from "../../../logIncludesExpectedPhrases"; import { macOSRecord } from "@guidepup/guidepup"; import spokenPhraseSnapshot from "./webkit.spokenPhrase.snapshot.json"; -import { voTest as test } from "../../../src"; +import { voiceOverTest as test } from "../../../../src"; test.describe("Webkit Playwright VoiceOver", () => { test("I can navigate the Guidepup Github page", async ({ diff --git a/examples/playwright-voiceover/tests/webkit/webkit.spokenPhrase.snapshot.json b/examples/playwright-voiceover/tests/webkit/webkit.spokenPhrase.snapshot.json new file mode 100644 index 0000000..ac558ea --- /dev/null +++ b/examples/playwright-voiceover/tests/webkit/webkit.spokenPhrase.snapshot.json @@ -0,0 +1,8 @@ +[ + "heading level 1 guidepup/guidepup", + "heading level 2 Latest commit", + "heading level 2 Git stats", + "heading level 2 Files", + "heading level 2 link README.md", + "heading level 1 Guidepup" +] diff --git a/example/webkit.config.ts b/examples/playwright-voiceover/webkit.config.ts similarity index 71% rename from example/webkit.config.ts rename to examples/playwright-voiceover/webkit.config.ts index eece554..9f4d81f 100644 --- a/example/webkit.config.ts +++ b/examples/playwright-voiceover/webkit.config.ts @@ -1,15 +1,15 @@ import { devices, PlaywrightTestConfig } from "@playwright/test"; +import { screenReaderConfig } from "../../src"; const config: PlaywrightTestConfig = { + ...screenReaderConfig, reportSlowTests: null, - fullyParallel: false, - workers: 1, timeout: 5 * 60 * 1000, retries: 5, projects: [ { name: "webkit", - use: { ...devices["Desktop Safari"], headless: false, video: "on" }, + use: { ...devices["Desktop Safari"], headless: false }, }, ], reporter: process.env.CI ? [["github"], ["html", { open: "never" }]] : "list", diff --git a/package.json b/package.json index a08d26b..bf8ecbf 100644 --- a/package.json +++ b/package.json @@ -24,28 +24,31 @@ "scripts": { "build": "yarn clean && yarn compile", "ci": "yarn clean && yarn lint && yarn build", - "clean": "rimraf lib", + "clean": "rimraf lib test-results recordings", "compile": "tsc", "lint": "eslint . --ext .ts", "lint:fix": "yarn lint --fix", "prepublish": "yarn build", "pretest": "npx playwright install chromium firefox webkit", - "test": "yarn test:chromium && yarn test:firefox && yarn test:webkit", - "test:chromium": "playwright test --config ./example/chromium.config.ts ./example/tests/chromium/", - "test:firefox": "playwright test --config ./example/firefox.config.ts ./example/tests/firefox/", - "test:webkit": "playwright test --config ./example/webkit.config.ts ./example/tests/webkit/" + "test:nvda": "yarn test:nvda:chromium && yarn test:nvda:firefox", + "test:voiceover": "yarn test:voiceover:chromium && yarn test:voiceover:firefox && yarn test:voiceover:webkit", + "test:nvda:chromium": "playwright test --config ./examples/playwright-nvda/chromium.config.ts ./examples/playwright-nvda/tests/chromium/", + "test:nvda:firefox": "playwright test --config ./examples/playwright-nvda/firefox.config.ts ./examples/playwright-nvda/tests/firefox/", + "test:voiceover:chromium": "playwright test --config ./examples/playwright-voiceover/chromium.config.ts ./examples/playwright-voiceover/tests/chromium/", + "test:voiceover:firefox": "playwright test --config ./examples/playwright-voiceover/firefox.config.ts ./examples/playwright-voiceover/tests/firefox/", + "test:voiceover:webkit": "playwright test --config ./examples/playwright-voiceover/webkit.config.ts ./examples/playwright-voiceover/tests/webkit/" }, "devDependencies": { - "@guidepup/guidepup": "^0.21.0", + "@guidepup/guidepup": "^0.22.0", "@playwright/test": "^1.40.1", - "@types/node": "^20.10.5", - "@typescript-eslint/eslint-plugin": "^6.16.0", - "@typescript-eslint/parser": "^6.16.0", + "@types/node": "^20.10.6", + "@typescript-eslint/eslint-plugin": "^6.18.0", + "@typescript-eslint/parser": "^6.18.0", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "rimraf": "^5.0.5", "ts-node": "^10.9.2", - "typedoc": "^0.25.4", + "typedoc": "^0.25.6", "typescript": "^5.3.3" }, "peerDependencies": { diff --git a/src/applicationNameMap.ts b/src/applicationNameMap.ts new file mode 100644 index 0000000..baeac7f --- /dev/null +++ b/src/applicationNameMap.ts @@ -0,0 +1,10 @@ +export const applicationNameMap = { + chromium: "Chromium", + chrome: "Google Chrome", + "chrome-beta": "Google Chrome Beta", + msedge: "Microsoft Edge", + "msedge-beta": "Microsoft Edge Beta", + "msedge-dev": "Microsoft Edge Dev", + firefox: "Nightly", + webkit: "Playwright", +}; diff --git a/src/index.ts b/src/index.ts index 5f64980..f58d788 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ -export { voTest } from "./voTest"; -export { voConfig } from "./voConfig"; +export { nvdaTest } from "./nvdaTest"; +export { screenReaderConfig } from "./screenReaderConfig"; +export { voiceOverTest } from "./voiceOverTest"; diff --git a/src/nvdaTest.ts b/src/nvdaTest.ts new file mode 100644 index 0000000..7f5c55d --- /dev/null +++ b/src/nvdaTest.ts @@ -0,0 +1,59 @@ +import { test } from "@playwright/test"; +import { nvda, WindowsKeyCodes, WindowsModifiers } from "@guidepup/guidepup"; +import type { NVDA } from "@guidepup/guidepup"; +import { applicationNameMap } from "./applicationNameMap"; + +/** + * These tests extend the default Playwright environment that launches the + * browser with a running instance of the NVDA screen reader for Windows. + * + * A fresh started NVDA instance `nvda` is provided to each test. + */ +export const nvdaTest = test.extend<{ nvda: NVDA }>({ + nvda: async ({ browserName, page }, use) => { + try { + const applicationName = applicationNameMap[browserName]; + + if (!applicationName) { + throw new Error(`Browser ${browserName} is not installed.`); + } + + await nvda.start(); + + // Make sure the browser window is focused. + await page.goto("about:blank", { waitUntil: "load" }); + + let applicationSwitchRetryCount = 0; + + while (applicationSwitchRetryCount < 10) { + applicationSwitchRetryCount++; + + await nvda.perform({ + keyCode: [WindowsKeyCodes.Tab], + modifiers: [WindowsModifiers.Alt], + }); + + const lastSpokenPhrase = await nvda.lastSpokenPhrase(); + + if (lastSpokenPhrase.includes(applicationName)) { + break; + } + } + + // Make sure not in focus mode. + await nvda.perform(nvda.keyboardCommands.exitFocusMode); + + // Clear the logs. + await nvda.clearItemTextLog(); + await nvda.clearSpokenPhraseLog(); + + await use(nvda); + } finally { + try { + await nvda.stop(); + } catch { + // swallow stop failure + } + } + }, +}); diff --git a/src/screenReaderConfig.ts b/src/screenReaderConfig.ts new file mode 100644 index 0000000..47175be --- /dev/null +++ b/src/screenReaderConfig.ts @@ -0,0 +1,27 @@ +import { PlaywrightTestConfig } from "@playwright/test"; + +export type ScreenReaderPlaywrightTestConfig = PlaywrightTestConfig; + +/** + * Minimal required configuration for Screen Reader tests in Playwright. + */ +export const screenReaderConfig: ScreenReaderPlaywrightTestConfig = { + /** + * We can only run a single instance of Screen Readers, so we must run a + * single test at a time. + */ + workers: 1, + + /** + * We can't parallelize Screen Reader tests as they are singletons, you can't + * start multiple instances at once. + */ + fullyParallel: false, + + use: { + /** + * Screen Readers don't work against headless browsers. + */ + headless: false, + }, +}; diff --git a/src/voConfig.ts b/src/voConfig.ts deleted file mode 100644 index 4c0296f..0000000 --- a/src/voConfig.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { PlaywrightTestConfig } from "@playwright/test"; - -export type VoiceOverPlaywrightTestConfig = PlaywrightTestConfig; - -/** - * Minimal required configuration for VoiceOver tests in Playwright. - */ -export const voConfig: VoiceOverPlaywrightTestConfig = { - /** - * We can only run a single instance of VoiceOver, so we must run a single - * test at a time. - */ - workers: 1, - - use: { - /** - * Although VoiceOver can interact with headless applications, not all - * behaviours work as expected. - */ - headless: false, - }, -}; diff --git a/src/voTest.ts b/src/voiceOverTest.ts similarity index 52% rename from src/voTest.ts rename to src/voiceOverTest.ts index 0e128c9..b7b25c5 100644 --- a/src/voTest.ts +++ b/src/voiceOverTest.ts @@ -1,29 +1,30 @@ import { test } from "@playwright/test"; import { voiceOver, macOSActivate } from "@guidepup/guidepup"; import type { VoiceOver } from "@guidepup/guidepup"; - -const applicationNameMap = { - chromium: "Chromium", - chrome: "Google Chrome", - "chrome-beta": "Google Chrome Beta", - msedge: "Microsoft Edge", - "msedge-beta": "Microsoft Edge Beta", - "msedge-dev": "Microsoft Edge Dev", - firefox: "Nightly", - webkit: "Playwright", -}; +import { applicationNameMap } from "./applicationNameMap"; /** * These tests extend the default Playwright environment that launches the * browser with a running instance of the VoiceOver screen reader for MacOS. * - * A fresh started VoiceOver instance `vo` is provided to each test. + * A fresh started VoiceOver instance `voiceOver` is provided to each test. */ -const voTest = test.extend<{ voiceOver: VoiceOver }>({ +export const voiceOverTest = test.extend<{ voiceOver: VoiceOver }>({ voiceOver: async ({ browserName }, use) => { try { + const applicationName = applicationNameMap[browserName]; + + if (!applicationName) { + throw new Error(`Browser ${browserName} is not installed.`); + } + await voiceOver.start(); - await macOSActivate(applicationNameMap[browserName]); + + await macOSActivate(applicationName); + + await voiceOver.clearItemTextLog(); + await voiceOver.clearSpokenPhraseLog(); + await use(voiceOver); } finally { try { @@ -34,5 +35,3 @@ const voTest = test.extend<{ voiceOver: VoiceOver }>({ } }, }); - -export { voTest }; diff --git a/yarn.lock b/yarn.lock index 767fc1e..60f59fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -56,10 +56,10 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b" integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A== -"@guidepup/guidepup@^0.21.0": - version "0.21.0" - resolved "https://registry.yarnpkg.com/@guidepup/guidepup/-/guidepup-0.21.0.tgz#bccf8bd7c5a08b4f8b70578eba1a8f835bdb8e5d" - integrity sha512-gO7c2rT2nG/VgAhMlk7FmEbU4HI8DoS8FYsohAcCVWaCHZM/I6vUh4FLTHMZ3p+ZTtHubNwqprxAHmzQoxM3Cg== +"@guidepup/guidepup@^0.22.0": + version "0.22.0" + resolved "https://registry.yarnpkg.com/@guidepup/guidepup/-/guidepup-0.22.0.tgz#f3fc757c1f40dcf8920027c5dfeff2486a589686" + integrity sha512-AOXe7WaFdeY3Dwr/CKrq0aS6TAor9Jq8nz550i740z9yD3DqpJkEW4hYoU5HinbsMNGK2Iqzv5AcFfJixEiPoQ== dependencies: ffmpeg-static "^5.2.0" regedit "5.1.2" @@ -178,10 +178,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== -"@types/node@^20.10.5": - version "20.10.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.5.tgz#47ad460b514096b7ed63a1dae26fad0914ed3ab2" - integrity sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw== +"@types/node@^20.10.6": + version "20.10.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.6.tgz#a3ec84c22965802bf763da55b2394424f22bfbb5" + integrity sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw== dependencies: undici-types "~5.26.4" @@ -190,16 +190,16 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339" integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A== -"@typescript-eslint/eslint-plugin@^6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.16.0.tgz#cc29fbd208ea976de3db7feb07755bba0ce8d8bc" - integrity sha512-O5f7Kv5o4dLWQtPX4ywPPa+v9G+1q1x8mz0Kr0pXUtKsevo+gIJHLkGc8RxaZWtP8RrhwhSNIWThnW42K9/0rQ== +"@typescript-eslint/eslint-plugin@^6.18.0": + version "6.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.18.0.tgz#94b86f3c25b468c714a04bd490017ecec2fd3746" + integrity sha512-3lqEvQUdCozi6d1mddWqd+kf8KxmGq2Plzx36BlkjuQe3rSTm/O98cLf0A4uDO+a5N1KD2SeEEl6fW97YHY+6w== dependencies: "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "6.16.0" - "@typescript-eslint/type-utils" "6.16.0" - "@typescript-eslint/utils" "6.16.0" - "@typescript-eslint/visitor-keys" "6.16.0" + "@typescript-eslint/scope-manager" "6.18.0" + "@typescript-eslint/type-utils" "6.18.0" + "@typescript-eslint/utils" "6.18.0" + "@typescript-eslint/visitor-keys" "6.18.0" debug "^4.3.4" graphemer "^1.4.0" ignore "^5.2.4" @@ -207,47 +207,47 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/parser@^6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.16.0.tgz#36f39f63b126aa25af2ad2df13d9891e9fd5b40c" - integrity sha512-H2GM3eUo12HpKZU9njig3DF5zJ58ja6ahj1GoHEHOgQvYxzoFJJEvC1MQ7T2l9Ha+69ZSOn7RTxOdpC/y3ikMw== +"@typescript-eslint/parser@^6.18.0": + version "6.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.18.0.tgz#d494161d64832e869f0a6acc6000a2cdff858383" + integrity sha512-v6uR68SFvqhNQT41frCMCQpsP+5vySy6IdgjlzUWoo7ALCnpaWYcz/Ij2k4L8cEsL0wkvOviCMpjmtRtHNOKzA== dependencies: - "@typescript-eslint/scope-manager" "6.16.0" - "@typescript-eslint/types" "6.16.0" - "@typescript-eslint/typescript-estree" "6.16.0" - "@typescript-eslint/visitor-keys" "6.16.0" + "@typescript-eslint/scope-manager" "6.18.0" + "@typescript-eslint/types" "6.18.0" + "@typescript-eslint/typescript-estree" "6.18.0" + "@typescript-eslint/visitor-keys" "6.18.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.16.0.tgz#f3e9a00fbc1d0701356359cd56489c54d9e37168" - integrity sha512-0N7Y9DSPdaBQ3sqSCwlrm9zJwkpOuc6HYm7LpzLAPqBL7dmzAUimr4M29dMkOP/tEwvOCC/Cxo//yOfJD3HUiw== +"@typescript-eslint/scope-manager@6.18.0": + version "6.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.18.0.tgz#24ca6fc1f4a2afa71122dcfca9282878687d9997" + integrity sha512-o/UoDT2NgOJ2VfHpfr+KBY2ErWvCySNUIX/X7O9g8Zzt/tXdpfEU43qbNk8LVuWUT2E0ptzTWXh79i74PP0twA== dependencies: - "@typescript-eslint/types" "6.16.0" - "@typescript-eslint/visitor-keys" "6.16.0" + "@typescript-eslint/types" "6.18.0" + "@typescript-eslint/visitor-keys" "6.18.0" -"@typescript-eslint/type-utils@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.16.0.tgz#5f21c3e49e540ad132dc87fc99af463c184d5ed1" - integrity sha512-ThmrEOcARmOnoyQfYkHw/DX2SEYBalVECmoldVuH6qagKROp/jMnfXpAU/pAIWub9c4YTxga+XwgAkoA0pxfmg== +"@typescript-eslint/type-utils@6.18.0": + version "6.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.18.0.tgz#a492da599da5c38c70aa9ff9bfb473961b8ae663" + integrity sha512-ZeMtrXnGmTcHciJN1+u2CigWEEXgy1ufoxtWcHORt5kGvpjjIlK9MUhzHm4RM8iVy6dqSaZA/6PVkX6+r+ChjQ== dependencies: - "@typescript-eslint/typescript-estree" "6.16.0" - "@typescript-eslint/utils" "6.16.0" + "@typescript-eslint/typescript-estree" "6.18.0" + "@typescript-eslint/utils" "6.18.0" debug "^4.3.4" ts-api-utils "^1.0.1" -"@typescript-eslint/types@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.16.0.tgz#a3abe0045737d44d8234708d5ed8fef5d59dc91e" - integrity sha512-hvDFpLEvTJoHutVl87+MG/c5C8I6LOgEx05zExTSJDEVU7hhR3jhV8M5zuggbdFCw98+HhZWPHZeKS97kS3JoQ== +"@typescript-eslint/types@6.18.0": + version "6.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.18.0.tgz#ffce610a1540c17cf7d8ecf2bb34b8b0e2e77101" + integrity sha512-/RFVIccwkwSdW/1zeMx3hADShWbgBxBnV/qSrex6607isYjj05t36P6LyONgqdUrNLl5TYU8NIKdHUYpFvExkA== -"@typescript-eslint/typescript-estree@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.16.0.tgz#d6e0578e4f593045f0df06c4b3a22bd6f13f2d03" - integrity sha512-VTWZuixh/vr7nih6CfrdpmFNLEnoVBF1skfjdyGnNwXOH1SLeHItGdZDHhhAIzd3ACazyY2Fg76zuzOVTaknGA== +"@typescript-eslint/typescript-estree@6.18.0": + version "6.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.18.0.tgz#1c357c3ca435c3cfa2af6b9daf45ca0bc2bb059a" + integrity sha512-klNvl+Ql4NsBNGB4W9TZ2Od03lm7aGvTbs0wYaFYsplVPhr+oeXjlPZCDI4U9jgJIDK38W1FKhacCFzCC+nbIg== dependencies: - "@typescript-eslint/types" "6.16.0" - "@typescript-eslint/visitor-keys" "6.16.0" + "@typescript-eslint/types" "6.18.0" + "@typescript-eslint/visitor-keys" "6.18.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -255,25 +255,25 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/utils@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.16.0.tgz#1c291492d34670f9210d2b7fcf6b402bea3134ae" - integrity sha512-T83QPKrBm6n//q9mv7oiSvy/Xq/7Hyw9SzSEhMHJwznEmQayfBM87+oAlkNAMEO7/MjIwKyOHgBJbxB0s7gx2A== +"@typescript-eslint/utils@6.18.0": + version "6.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.18.0.tgz#4d07c9c08f84b9939a1aca7aef98c8f378936142" + integrity sha512-wiKKCbUeDPGaYEYQh1S580dGxJ/V9HI7K5sbGAVklyf+o5g3O+adnS4UNJajplF4e7z2q0uVBaTdT/yLb4XAVA== dependencies: "@eslint-community/eslint-utils" "^4.4.0" "@types/json-schema" "^7.0.12" "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.16.0" - "@typescript-eslint/types" "6.16.0" - "@typescript-eslint/typescript-estree" "6.16.0" + "@typescript-eslint/scope-manager" "6.18.0" + "@typescript-eslint/types" "6.18.0" + "@typescript-eslint/typescript-estree" "6.18.0" semver "^7.5.4" -"@typescript-eslint/visitor-keys@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.16.0.tgz#d50da18a05d91318ed3e7e8889bda0edc35f3a10" - integrity sha512-QSFQLruk7fhs91a/Ep/LqRdbJCZ1Rq03rqBdKT5Ky17Sz8zRLUksqIe9DW0pKtg/Z35/ztbLQ6qpOCN6rOC11A== +"@typescript-eslint/visitor-keys@6.18.0": + version "6.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.0.tgz#3c8733737786fa6c78a347b4fa306ae7155b560f" + integrity sha512-1wetAlSZpewRDb2h9p/Q8kRjdGuqdTAQbkJIOUMLug2LBLG+QOjiWoSj6/3B/hA9/tVTFFdtiKvAYoYnSRW/RA== dependencies: - "@typescript-eslint/types" "6.16.0" + "@typescript-eslint/types" "6.18.0" eslint-visitor-keys "^3.4.1" "@ungap/structured-clone@^1.2.0": @@ -1229,10 +1229,10 @@ shelljs@^0.8.5: interpret "^1.0.0" rechoir "^0.6.2" -shiki@^0.14.1: - version "0.14.6" - resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.14.6.tgz#908a9cd5439f7e87279c6623e7c60a3b0a2df85c" - integrity sha512-R4koBBlQP33cC8cpzX0hAoOURBHJILp4Aaduh2eYi+Vj8ZBqtK/5SWNEHBS3qwUMu8dqOtI/ftno3ESfNeVW9g== +shiki@^0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.14.7.tgz#c3c9e1853e9737845f1d2ef81b31bcfb07056d4e" + integrity sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg== dependencies: ansi-sequence-parser "^1.1.0" jsonc-parser "^3.2.0" @@ -1369,15 +1369,15 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typedoc@^0.25.4: - version "0.25.4" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.25.4.tgz#5c2c0677881f504e41985f29d9aef0dbdb6f1e6f" - integrity sha512-Du9ImmpBCw54bX275yJrxPVnjdIyJO/84co0/L9mwe0R3G4FSR6rQ09AlXVRvZEGMUg09+z/usc8mgygQ1aidA== +typedoc@^0.25.6: + version "0.25.6" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.25.6.tgz#1505538aecea511dd669652c71d042a2427bd4fc" + integrity sha512-1rdionQMpOkpA58qfym1J+YD+ukyA1IEIa4VZahQI2ZORez7dhOvEyUotQL/8rSoMBopdzOS+vAIsORpQO4cTA== dependencies: lunr "^2.3.9" marked "^4.3.0" minimatch "^9.0.3" - shiki "^0.14.1" + shiki "^0.14.7" typescript@^5.3.3: version "5.3.3" From bb07b41930fffbb39c401384665cdee2aa5e7d84 Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 7 Jan 2024 13:51:57 +0000 Subject: [PATCH 02/18] fix: NVDA tests have different snapshot outputs :facepalm: --- .../chromium/chromium.spokenPhrase.snapshot.json | 12 ++++++------ .../tests/firefox/firefox.spokenPhrase.snapshot.json | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/playwright-nvda/tests/chromium/chromium.spokenPhrase.snapshot.json b/examples/playwright-nvda/tests/chromium/chromium.spokenPhrase.snapshot.json index ac558ea..acbba02 100644 --- a/examples/playwright-nvda/tests/chromium/chromium.spokenPhrase.snapshot.json +++ b/examples/playwright-nvda/tests/chromium/chromium.spokenPhrase.snapshot.json @@ -1,8 +1,8 @@ [ - "heading level 1 guidepup/guidepup", - "heading level 2 Latest commit", - "heading level 2 Git stats", - "heading level 2 Files", - "heading level 2 link README.md", - "heading level 1 Guidepup" + "guidepup slash guidepup, heading, level 1", + "Latest commit, heading, level 2", + "Git stats, heading, level 2", + "Files, heading, level 2", + "README dot md, link, heading, level 2", + "Guidepup, heading, level 1" ] diff --git a/examples/playwright-nvda/tests/firefox/firefox.spokenPhrase.snapshot.json b/examples/playwright-nvda/tests/firefox/firefox.spokenPhrase.snapshot.json index ac558ea..acbba02 100644 --- a/examples/playwright-nvda/tests/firefox/firefox.spokenPhrase.snapshot.json +++ b/examples/playwright-nvda/tests/firefox/firefox.spokenPhrase.snapshot.json @@ -1,8 +1,8 @@ [ - "heading level 1 guidepup/guidepup", - "heading level 2 Latest commit", - "heading level 2 Git stats", - "heading level 2 Files", - "heading level 2 link README.md", - "heading level 1 Guidepup" + "guidepup slash guidepup, heading, level 1", + "Latest commit, heading, level 2", + "Git stats, heading, level 2", + "Files, heading, level 2", + "README dot md, link, heading, level 2", + "Guidepup, heading, level 1" ] From c5816c085450c03faee81c4a7cbca5d995209d11 Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 7 Jan 2024 15:48:32 +0000 Subject: [PATCH 03/18] feat: add new utility method `.navigateToWebContent()` to make page load nav easier --- README.md | 12 +- examples/delay.ts | 3 - .../playwright-nvda/tests/headerNavigation.ts | 15 +-- .../tests/headerNavigation.ts | 15 +-- src/index.ts | 2 + src/nvdaTest.ts | 126 ++++++++++++++---- src/voiceOverTest.ts | 94 +++++++++++-- 7 files changed, 206 insertions(+), 61 deletions(-) delete mode 100644 examples/delay.ts diff --git a/README.md b/README.md index 488aa3c..d72956d 100644 --- a/README.md +++ b/README.md @@ -118,8 +118,7 @@ test.describe("Playwright VoiceOver", () => { await header.waitFor(); // Interact with the page - await voiceOver.interact(); - await voiceOver.perform(voiceOver.keyboardCommands.jumpToLeftEdge); + await voiceOver.navigateToWebContent(); // Move across the page menu to the Guidepup heading using VoiceOver while ((await voiceOver.itemText()) !== "Guidepup heading level 1") { @@ -175,11 +174,14 @@ test.describe("Playwright NVDA", () => { // Wait for page to be ready and setup const header = page.locator('header[role="banner"]'); await header.waitFor(); - await page.locator("a").first().focus(); - await nvda.perform(nvda.keyboardCommands.exitFocusMode); + + // Interact with the page + await nvda.navigateToWebContent(); // Move across the page menu to the Guidepup heading using NVDA - while ((await nvda.itemText()) !== "Guidepup heading level 1") { + while ( + !(await nvda.lastSpokenPhrase()).includes("Guidepup, heading, level 1") + ) { await nvda.perform(nvda.keyboardCommands.moveToNextHeading); } diff --git a/examples/delay.ts b/examples/delay.ts deleted file mode 100644 index 4493928..0000000 --- a/examples/delay.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function delay(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/examples/playwright-nvda/tests/headerNavigation.ts b/examples/playwright-nvda/tests/headerNavigation.ts index dfbe20c..aded78f 100644 --- a/examples/playwright-nvda/tests/headerNavigation.ts +++ b/examples/playwright-nvda/tests/headerNavigation.ts @@ -1,7 +1,6 @@ -import type { NVDA } from "@guidepup/guidepup"; import { Page } from "@playwright/test"; -import { delay } from "../../delay"; import { log } from "../../log"; +import type { NVDAPlaywright } from "../../../src"; const MAX_NAVIGATION_LOOP = 10; @@ -10,7 +9,7 @@ export async function headerNavigation({ nvda, }: { page: Page; - nvda: NVDA; + nvda: NVDAPlaywright; }) { // Navigate to Guidepup GitHub page log("Navigating to URL: https://github.com/guidepup/guidepup."); @@ -21,19 +20,15 @@ export async function headerNavigation({ // Wait for page to be ready and interact const header = page.locator('header[role="banner"]'); await header.waitFor(); - await delay(500); - await page.locator("a").first().focus(); - // Make sure not in focus mode - log(`Performing command: "Escape"`); - await nvda.perform(nvda.keyboardCommands.exitFocusMode); - log(`Screen reader output: "${await nvda.lastSpokenPhrase()}".`); + // Make sure interacting with the web content + await nvda.navigateToWebContent(); let headingCount = 0; // Move across the page menu to the Guidepup heading using NVDA while ( - (await nvda.itemText()) !== "Guidepup heading level 1" && + !(await nvda.lastSpokenPhrase()).includes("Guidepup, heading, level 1") && headingCount <= MAX_NAVIGATION_LOOP ) { headingCount++; diff --git a/examples/playwright-voiceover/tests/headerNavigation.ts b/examples/playwright-voiceover/tests/headerNavigation.ts index 9d9e85d..2173bfc 100644 --- a/examples/playwright-voiceover/tests/headerNavigation.ts +++ b/examples/playwright-voiceover/tests/headerNavigation.ts @@ -1,7 +1,6 @@ -import type { VoiceOver } from "@guidepup/guidepup"; import { Page } from "@playwright/test"; -import { delay } from "../../delay"; import { log } from "../../log"; +import type { VoiceOverPlaywright } from "../../../src"; const MAX_NAVIGATION_LOOP = 10; @@ -10,7 +9,7 @@ export async function headerNavigation({ voiceOver, }: { page: Page; - voiceOver: VoiceOver; + voiceOver: VoiceOverPlaywright; }) { // Navigate to Guidepup GitHub page log("Navigating to URL: https://github.com/guidepup/guidepup."); @@ -21,17 +20,9 @@ export async function headerNavigation({ // Wait for page to be ready and interact const header = page.locator('header[role="banner"]'); await header.waitFor(); - await delay(500); // Make sure interacting with the web content - log(`Performing command: "VO+Shift+Down Arrow"`); - await voiceOver.interact(); - log(`Screen reader output: "${await voiceOver.lastSpokenPhrase()}".`); - - // Prevent auto-navigation of group - log(`Performing command: "VO+Shift+Left Arrow"`); - await voiceOver.perform(voiceOver.keyboardCommands.jumpToLeftEdge); - log(`Screen reader output: "${await voiceOver.lastSpokenPhrase()}".`); + await voiceOver.navigateToWebContent(); let headingCount = 0; diff --git a/src/index.ts b/src/index.ts index f58d788..ef5d858 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,5 @@ export { nvdaTest } from "./nvdaTest"; +export type { NVDAPlaywright } from "./nvdaTest"; export { screenReaderConfig } from "./screenReaderConfig"; export { voiceOverTest } from "./voiceOverTest"; +export type { VoiceOverPlaywright } from "./voiceOverTest"; diff --git a/src/nvdaTest.ts b/src/nvdaTest.ts index 7f5c55d..8ca6c93 100644 --- a/src/nvdaTest.ts +++ b/src/nvdaTest.ts @@ -3,13 +3,85 @@ import { nvda, WindowsKeyCodes, WindowsModifiers } from "@guidepup/guidepup"; import type { NVDA } from "@guidepup/guidepup"; import { applicationNameMap } from "./applicationNameMap"; +/** + * [API Reference](https://www.guidepup.dev/docs/api/class-nvda) + * + * This object can be used to launch and control NVDA. + * + * Here's a typical example: + * + * ```ts + * import { nvda } from "@guidepup/guidepup"; + * + * (async () => { + * // Start NVDA. + * await nvda.start(); + * + * // Move to the next item. + * await nvda.next(); + * + * // Stop NVDA. + * await nvda.stop(); + * })(); + * ``` + */ +export interface NVDAPlaywright extends NVDA { + /** + * Guidepup Playwright specific command that navigates NVDA to the beginning + * of the browser's web content. + * + * This command should be used after page navigation. + * + * Note: this command clears all logs. + */ + navigateToWebContent(): Promise; +} + +const nvdaPlaywright: NVDAPlaywright = nvda as NVDAPlaywright; + +const MAX_APPLICATION_SWITCH_RETRY_COUNT = 10; + +const SWITCH_APPLICATION = { + keyCode: [WindowsKeyCodes.Tab], + modifiers: [WindowsModifiers.Alt], +}; + +const MOVE_TO_TOP_OF_PAGE = { + keyCode: [WindowsKeyCodes.Home], + modifiers: [WindowsModifiers.Control], +}; + /** * These tests extend the default Playwright environment that launches the * browser with a running instance of the NVDA screen reader for Windows. * * A fresh started NVDA instance `nvda` is provided to each test. */ -export const nvdaTest = test.extend<{ nvda: NVDA }>({ +export const nvdaTest = test.extend<{ + /** + * [API Reference](https://www.guidepup.dev/docs/api/class-nvda) + * + * This object can be used to launch and control NVDA. + * + * Here's a typical example: + * + * ```ts + * import { nvda } from "@guidepup/guidepup"; + * + * (async () => { + * // Start NVDA. + * await nvda.start(); + * + * // Move to the next item. + * await nvda.next(); + * + * // Stop NVDA. + * await nvda.stop(); + * })(); + * ``` + */ + nvda: NVDAPlaywright; +}>({ nvda: async ({ browserName, page }, use) => { try { const applicationName = applicationNameMap[browserName]; @@ -18,39 +90,47 @@ export const nvdaTest = test.extend<{ nvda: NVDA }>({ throw new Error(`Browser ${browserName} is not installed.`); } - await nvda.start(); + nvdaPlaywright.navigateToWebContent = async () => { + // Ensure application is brought to front and focused. + let applicationSwitchRetryCount = 0; - // Make sure the browser window is focused. - await page.goto("about:blank", { waitUntil: "load" }); + while ( + applicationSwitchRetryCount < MAX_APPLICATION_SWITCH_RETRY_COUNT + ) { + applicationSwitchRetryCount++; - let applicationSwitchRetryCount = 0; + await nvdaPlaywright.perform(SWITCH_APPLICATION); - while (applicationSwitchRetryCount < 10) { - applicationSwitchRetryCount++; + const lastSpokenPhrase = await nvdaPlaywright.lastSpokenPhrase(); - await nvda.perform({ - keyCode: [WindowsKeyCodes.Tab], - modifiers: [WindowsModifiers.Alt], - }); + if (lastSpokenPhrase.includes(applicationName)) { + break; + } + } - const lastSpokenPhrase = await nvda.lastSpokenPhrase(); + // Ensure the document is ready and focused. + await page.locator("body").waitFor(); + await page.locator("body").focus(); - if (lastSpokenPhrase.includes(applicationName)) { - break; - } - } + // Make sure NVDA is not in focus mode. + await nvdaPlaywright.perform( + nvdaPlaywright.keyboardCommands.exitFocusMode + ); + + // Navigate to the beginning of the web content. + await nvdaPlaywright.perform(MOVE_TO_TOP_OF_PAGE); - // Make sure not in focus mode. - await nvda.perform(nvda.keyboardCommands.exitFocusMode); + // Clear out logs. + await nvdaPlaywright.clearItemTextLog(); + await nvdaPlaywright.clearSpokenPhraseLog(); + }; - // Clear the logs. - await nvda.clearItemTextLog(); - await nvda.clearSpokenPhraseLog(); + await nvdaPlaywright.start(); - await use(nvda); + await use(nvdaPlaywright); } finally { try { - await nvda.stop(); + await nvdaPlaywright.stop(); } catch { // swallow stop failure } diff --git a/src/voiceOverTest.ts b/src/voiceOverTest.ts index b7b25c5..5a2c31d 100644 --- a/src/voiceOverTest.ts +++ b/src/voiceOverTest.ts @@ -3,14 +3,79 @@ import { voiceOver, macOSActivate } from "@guidepup/guidepup"; import type { VoiceOver } from "@guidepup/guidepup"; import { applicationNameMap } from "./applicationNameMap"; +/** + * [API Reference](https://www.guidepup.dev/docs/api/class-voiceover) + * + * This object can be used to launch and control VoiceOver. + * + * Here's a typical example: + * + * ```ts + * import { voiceOver } from "@guidepup/guidepup"; + * + * (async () => { + * // Start VoiceOver. + * await voiceOver.start(); + * + * // Move to the next item. + * await voiceOver.next(); + * + * // ... perform some commands. + * + * // Stop VoiceOver. + * await voiceOver.stop(); + * })(); + * ``` + */ +export interface VoiceOverPlaywright extends VoiceOver { + /** + * Guidepup Playwright specific command that navigates VoiceOver to the beginning + * of the browser's web content. + * + * This command should be used after page navigation. + * + * Note: this command clears all logs. + */ + navigateToWebContent(): Promise; +} + +const voiceOverPlaywright: VoiceOverPlaywright = + voiceOver as VoiceOverPlaywright; + /** * These tests extend the default Playwright environment that launches the * browser with a running instance of the VoiceOver screen reader for MacOS. * * A fresh started VoiceOver instance `voiceOver` is provided to each test. */ -export const voiceOverTest = test.extend<{ voiceOver: VoiceOver }>({ - voiceOver: async ({ browserName }, use) => { +export const voiceOverTest = test.extend<{ + /** + * [API Reference](https://www.guidepup.dev/docs/api/class-voiceover) + * + * This object can be used to launch and control VoiceOver. + * + * Here's a typical example: + * + * ```ts + * import { voiceOver } from "@guidepup/guidepup"; + * + * (async () => { + * // Start VoiceOver. + * await voiceOver.start(); + * + * // Move to the next item. + * await voiceOver.next(); + * + * // ... perform some commands. + * + * // Stop VoiceOver. + * await voiceOver.stop(); + * })(); + * ``` + */ + voiceOver: VoiceOverPlaywright; +}>({ + voiceOver: async ({ browserName, page }, use) => { try { const applicationName = applicationNameMap[browserName]; @@ -18,17 +83,30 @@ export const voiceOverTest = test.extend<{ voiceOver: VoiceOver }>({ throw new Error(`Browser ${browserName} is not installed.`); } - await voiceOver.start(); + voiceOverPlaywright.navigateToWebContent = async () => { + // Ensure application is brought to front and focused. + await macOSActivate(applicationName); - await macOSActivate(applicationName); + // Ensure the document is ready and focused. + await page.locator("body").waitFor(); + await page.locator("body").focus(); + + // Navigate to the beginning of the web content. + await voiceOverPlaywright.perform( + voiceOverPlaywright.keyboardCommands.jumpToLeftEdge + ); - await voiceOver.clearItemTextLog(); - await voiceOver.clearSpokenPhraseLog(); + // Clear out logs. + await voiceOverPlaywright.clearItemTextLog(); + await voiceOverPlaywright.clearSpokenPhraseLog(); + }; - await use(voiceOver); + await voiceOverPlaywright.start(); + await macOSActivate(applicationName); + await use(voiceOverPlaywright); } finally { try { - await voiceOver.stop(); + await voiceOverPlaywright.stop(); } catch { // swallow stop failure } From 9b151396ec29fbeda93d494140ffa005b22b8cb5 Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 7 Jan 2024 15:49:12 +0000 Subject: [PATCH 04/18] refactor: reduce retries down to 1 --- examples/playwright-nvda/chromium.config.ts | 2 +- examples/playwright-nvda/firefox.config.ts | 2 +- examples/playwright-voiceover/chromium.config.ts | 2 +- examples/playwright-voiceover/firefox.config.ts | 2 +- examples/playwright-voiceover/webkit.config.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/playwright-nvda/chromium.config.ts b/examples/playwright-nvda/chromium.config.ts index bfbb9af..def99d5 100644 --- a/examples/playwright-nvda/chromium.config.ts +++ b/examples/playwright-nvda/chromium.config.ts @@ -5,7 +5,7 @@ const config: PlaywrightTestConfig = { ...screenReaderConfig, reportSlowTests: null, timeout: 5 * 60 * 1000, - retries: 5, + retries: 1, projects: [ { name: "chromium", diff --git a/examples/playwright-nvda/firefox.config.ts b/examples/playwright-nvda/firefox.config.ts index 40e4349..92a6e17 100644 --- a/examples/playwright-nvda/firefox.config.ts +++ b/examples/playwright-nvda/firefox.config.ts @@ -5,7 +5,7 @@ const config: PlaywrightTestConfig = { ...screenReaderConfig, reportSlowTests: null, timeout: 5 * 60 * 1000, - retries: 5, + retries: 1, projects: [ { name: "firefox", diff --git a/examples/playwright-voiceover/chromium.config.ts b/examples/playwright-voiceover/chromium.config.ts index bfbb9af..def99d5 100644 --- a/examples/playwright-voiceover/chromium.config.ts +++ b/examples/playwright-voiceover/chromium.config.ts @@ -5,7 +5,7 @@ const config: PlaywrightTestConfig = { ...screenReaderConfig, reportSlowTests: null, timeout: 5 * 60 * 1000, - retries: 5, + retries: 1, projects: [ { name: "chromium", diff --git a/examples/playwright-voiceover/firefox.config.ts b/examples/playwright-voiceover/firefox.config.ts index 40e4349..92a6e17 100644 --- a/examples/playwright-voiceover/firefox.config.ts +++ b/examples/playwright-voiceover/firefox.config.ts @@ -5,7 +5,7 @@ const config: PlaywrightTestConfig = { ...screenReaderConfig, reportSlowTests: null, timeout: 5 * 60 * 1000, - retries: 5, + retries: 1, projects: [ { name: "firefox", diff --git a/examples/playwright-voiceover/webkit.config.ts b/examples/playwright-voiceover/webkit.config.ts index 9f4d81f..b4a8662 100644 --- a/examples/playwright-voiceover/webkit.config.ts +++ b/examples/playwright-voiceover/webkit.config.ts @@ -5,7 +5,7 @@ const config: PlaywrightTestConfig = { ...screenReaderConfig, reportSlowTests: null, timeout: 5 * 60 * 1000, - retries: 5, + retries: 1, projects: [ { name: "webkit", From 78d1ab0451c3d9d6e7869d196c886291204858e9 Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 7 Jan 2024 16:12:15 +0000 Subject: [PATCH 05/18] refactor: try to harden `.navigateToWebContent()` --- src/nvdaTest.ts | 14 +++++++++++--- src/voiceOverTest.ts | 10 ++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/nvdaTest.ts b/src/nvdaTest.ts index 8ca6c93..795dc40 100644 --- a/src/nvdaTest.ts +++ b/src/nvdaTest.ts @@ -91,6 +91,12 @@ export const nvdaTest = test.extend<{ } nvdaPlaywright.navigateToWebContent = async () => { + // Make sure NVDA is not in focus mode. + await nvdaPlaywright.perform( + nvdaPlaywright.keyboardCommands.exitFocusMode + ); + await nvdaPlaywright.lastSpokenPhrase(); + // Ensure application is brought to front and focused. let applicationSwitchRetryCount = 0; @@ -109,16 +115,18 @@ export const nvdaTest = test.extend<{ } // Ensure the document is ready and focused. + await page.bringToFront(); await page.locator("body").waitFor(); await page.locator("body").focus(); - // Make sure NVDA is not in focus mode. + // Navigate to the beginning of the web content. await nvdaPlaywright.perform( - nvdaPlaywright.keyboardCommands.exitFocusMode + nvdaPlaywright.keyboardCommands.moveToFocusObject ); + await nvdaPlaywright.lastSpokenPhrase(); - // Navigate to the beginning of the web content. await nvdaPlaywright.perform(MOVE_TO_TOP_OF_PAGE); + await nvdaPlaywright.lastSpokenPhrase(); // Clear out logs. await nvdaPlaywright.clearItemTextLog(); diff --git a/src/voiceOverTest.ts b/src/voiceOverTest.ts index 5a2c31d..b6bd9b8 100644 --- a/src/voiceOverTest.ts +++ b/src/voiceOverTest.ts @@ -88,13 +88,23 @@ export const voiceOverTest = test.extend<{ await macOSActivate(applicationName); // Ensure the document is ready and focused. + await page.bringToFront(); await page.locator("body").waitFor(); await page.locator("body").focus(); // Navigate to the beginning of the web content. + await voiceOverPlaywright.perform( + voiceOverPlaywright.keyboardCommands.moveCursorToKeyboardFocus + ); + await voiceOverPlaywright.lastSpokenPhrase(); + await voiceOverPlaywright.perform( voiceOverPlaywright.keyboardCommands.jumpToLeftEdge ); + await voiceOverPlaywright.lastSpokenPhrase(); + + await voiceOverPlaywright.previous(); + await voiceOverPlaywright.lastSpokenPhrase(); // Clear out logs. await voiceOverPlaywright.clearItemTextLog(); From 61a42ccfed09ac6f24c3356a3699ff1004975693 Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 7 Jan 2024 16:40:53 +0000 Subject: [PATCH 06/18] fix: improve NVDA web content navigate --- src/nvdaTest.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/nvdaTest.ts b/src/nvdaTest.ts index 795dc40..560d97f 100644 --- a/src/nvdaTest.ts +++ b/src/nvdaTest.ts @@ -40,6 +40,7 @@ export interface NVDAPlaywright extends NVDA { const nvdaPlaywright: NVDAPlaywright = nvda as NVDAPlaywright; const MAX_APPLICATION_SWITCH_RETRY_COUNT = 10; +const MAX_NAVIGATE_TO_WEB_CONTENT_RETRY_COUNT = 10; const SWITCH_APPLICATION = { keyCode: [WindowsKeyCodes.Tab], @@ -120,10 +121,22 @@ export const nvdaTest = test.extend<{ await page.locator("body").focus(); // Navigate to the beginning of the web content. - await nvdaPlaywright.perform( - nvdaPlaywright.keyboardCommands.moveToFocusObject - ); - await nvdaPlaywright.lastSpokenPhrase(); + let navigateToWebContentRetryCount = 0; + + while ( + navigateToWebContentRetryCount < + MAX_NAVIGATE_TO_WEB_CONTENT_RETRY_COUNT + ) { + navigateToWebContentRetryCount++; + + await nvdaPlaywright.next(); + + const lastSpokenPhrase = await nvdaPlaywright.lastSpokenPhrase(); + + if (lastSpokenPhrase.includes("web content")) { + break; + } + } await nvdaPlaywright.perform(MOVE_TO_TOP_OF_PAGE); await nvdaPlaywright.lastSpokenPhrase(); From fc6c9a5bf22f8c202cac48dfbddf0e65728bf962 Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 7 Jan 2024 16:48:48 +0000 Subject: [PATCH 07/18] fix: ensure exit focus mode --- examples/playwright-nvda/tests/headerNavigation.ts | 2 +- src/nvdaTest.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/examples/playwright-nvda/tests/headerNavigation.ts b/examples/playwright-nvda/tests/headerNavigation.ts index aded78f..8af1ee6 100644 --- a/examples/playwright-nvda/tests/headerNavigation.ts +++ b/examples/playwright-nvda/tests/headerNavigation.ts @@ -33,7 +33,7 @@ export async function headerNavigation({ ) { headingCount++; - log(`Performing command: "VO+Command+H"`); + log(`Performing command: "H"`); await nvda.perform(nvda.keyboardCommands.moveToNextHeading); log(`Screen reader output: "${await nvda.lastSpokenPhrase()}".`); } diff --git a/src/nvdaTest.ts b/src/nvdaTest.ts index 560d97f..fcd2f90 100644 --- a/src/nvdaTest.ts +++ b/src/nvdaTest.ts @@ -115,6 +115,12 @@ export const nvdaTest = test.extend<{ } } + // Make sure NVDA is not in focus mode. + await nvdaPlaywright.perform( + nvdaPlaywright.keyboardCommands.exitFocusMode + ); + await nvdaPlaywright.lastSpokenPhrase(); + // Ensure the document is ready and focused. await page.bringToFront(); await page.locator("body").waitFor(); @@ -138,6 +144,12 @@ export const nvdaTest = test.extend<{ } } + // Make sure NVDA is not in focus mode. + await nvdaPlaywright.perform( + nvdaPlaywright.keyboardCommands.exitFocusMode + ); + await nvdaPlaywright.lastSpokenPhrase(); + await nvdaPlaywright.perform(MOVE_TO_TOP_OF_PAGE); await nvdaPlaywright.lastSpokenPhrase(); From 78447705fdf925ae88c174496a89ff569b08b617 Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 7 Jan 2024 16:55:17 +0000 Subject: [PATCH 08/18] fix: different focus vs browse mode command --- src/nvdaTest.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/nvdaTest.ts b/src/nvdaTest.ts index fcd2f90..05eafdb 100644 --- a/src/nvdaTest.ts +++ b/src/nvdaTest.ts @@ -117,7 +117,7 @@ export const nvdaTest = test.extend<{ // Make sure NVDA is not in focus mode. await nvdaPlaywright.perform( - nvdaPlaywright.keyboardCommands.exitFocusMode + nvdaPlaywright.keyboardCommands.toggleBetweenBrowseAndFocusMode ); await nvdaPlaywright.lastSpokenPhrase(); @@ -144,12 +144,6 @@ export const nvdaTest = test.extend<{ } } - // Make sure NVDA is not in focus mode. - await nvdaPlaywright.perform( - nvdaPlaywright.keyboardCommands.exitFocusMode - ); - await nvdaPlaywright.lastSpokenPhrase(); - await nvdaPlaywright.perform(MOVE_TO_TOP_OF_PAGE); await nvdaPlaywright.lastSpokenPhrase(); From 8a70987f9bc539306ae39796fc370ffffc7d69f8 Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 7 Jan 2024 17:28:54 +0000 Subject: [PATCH 09/18] fix: try to avoid alt-tab --- src/nvdaTest.ts | 64 ++++++++++++++++++++----------------------------- 1 file changed, 26 insertions(+), 38 deletions(-) diff --git a/src/nvdaTest.ts b/src/nvdaTest.ts index 05eafdb..a956a87 100644 --- a/src/nvdaTest.ts +++ b/src/nvdaTest.ts @@ -40,16 +40,31 @@ export interface NVDAPlaywright extends NVDA { const nvdaPlaywright: NVDAPlaywright = nvda as NVDAPlaywright; const MAX_APPLICATION_SWITCH_RETRY_COUNT = 10; -const MAX_NAVIGATE_TO_WEB_CONTENT_RETRY_COUNT = 10; const SWITCH_APPLICATION = { keyCode: [WindowsKeyCodes.Tab], modifiers: [WindowsModifiers.Alt], }; -const MOVE_TO_TOP_OF_PAGE = { - keyCode: [WindowsKeyCodes.Home], - modifiers: [WindowsModifiers.Control], +const switchApplications = async ({ + applicationName, +}: { + applicationName: string; +}) => { + // Ensure application is brought to front and focused. + let applicationSwitchRetryCount = 0; + + while (applicationSwitchRetryCount < MAX_APPLICATION_SWITCH_RETRY_COUNT) { + applicationSwitchRetryCount++; + + await nvdaPlaywright.perform(SWITCH_APPLICATION); + + const lastSpokenPhrase = await nvdaPlaywright.lastSpokenPhrase(); + + if (lastSpokenPhrase.includes(applicationName)) { + break; + } + } }; /** @@ -99,25 +114,19 @@ export const nvdaTest = test.extend<{ await nvdaPlaywright.lastSpokenPhrase(); // Ensure application is brought to front and focused. - let applicationSwitchRetryCount = 0; + await nvdaPlaywright.perform( + nvdaPlaywright.keyboardCommands.reportTitle + ); - while ( - applicationSwitchRetryCount < MAX_APPLICATION_SWITCH_RETRY_COUNT + if ( + !(await nvdaPlaywright.lastSpokenPhrase()).includes(applicationName) ) { - applicationSwitchRetryCount++; - - await nvdaPlaywright.perform(SWITCH_APPLICATION); - - const lastSpokenPhrase = await nvdaPlaywright.lastSpokenPhrase(); - - if (lastSpokenPhrase.includes(applicationName)) { - break; - } + await switchApplications({ applicationName }); } // Make sure NVDA is not in focus mode. await nvdaPlaywright.perform( - nvdaPlaywright.keyboardCommands.toggleBetweenBrowseAndFocusMode + nvdaPlaywright.keyboardCommands.exitFocusMode ); await nvdaPlaywright.lastSpokenPhrase(); @@ -126,27 +135,6 @@ export const nvdaTest = test.extend<{ await page.locator("body").waitFor(); await page.locator("body").focus(); - // Navigate to the beginning of the web content. - let navigateToWebContentRetryCount = 0; - - while ( - navigateToWebContentRetryCount < - MAX_NAVIGATE_TO_WEB_CONTENT_RETRY_COUNT - ) { - navigateToWebContentRetryCount++; - - await nvdaPlaywright.next(); - - const lastSpokenPhrase = await nvdaPlaywright.lastSpokenPhrase(); - - if (lastSpokenPhrase.includes("web content")) { - break; - } - } - - await nvdaPlaywright.perform(MOVE_TO_TOP_OF_PAGE); - await nvdaPlaywright.lastSpokenPhrase(); - // Clear out logs. await nvdaPlaywright.clearItemTextLog(); await nvdaPlaywright.clearSpokenPhraseLog(); From 15df306d37831a93d783107a14e1b5bec9b920c1 Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 7 Jan 2024 17:30:20 +0000 Subject: [PATCH 10/18] refactor: reduce vo navigate complexity --- src/voiceOverTest.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/voiceOverTest.ts b/src/voiceOverTest.ts index b6bd9b8..99a80a4 100644 --- a/src/voiceOverTest.ts +++ b/src/voiceOverTest.ts @@ -93,19 +93,11 @@ export const voiceOverTest = test.extend<{ await page.locator("body").focus(); // Navigate to the beginning of the web content. - await voiceOverPlaywright.perform( - voiceOverPlaywright.keyboardCommands.moveCursorToKeyboardFocus - ); - await voiceOverPlaywright.lastSpokenPhrase(); - await voiceOverPlaywright.perform( voiceOverPlaywright.keyboardCommands.jumpToLeftEdge ); await voiceOverPlaywright.lastSpokenPhrase(); - await voiceOverPlaywright.previous(); - await voiceOverPlaywright.lastSpokenPhrase(); - // Clear out logs. await voiceOverPlaywright.clearItemTextLog(); await voiceOverPlaywright.clearSpokenPhraseLog(); From 268f1a2a1176795c9b20d3ce3a3e73131de08602 Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 7 Jan 2024 17:33:00 +0000 Subject: [PATCH 11/18] fix: interact with web content --- src/voiceOverTest.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/voiceOverTest.ts b/src/voiceOverTest.ts index 99a80a4..ac195ac 100644 --- a/src/voiceOverTest.ts +++ b/src/voiceOverTest.ts @@ -93,6 +93,9 @@ export const voiceOverTest = test.extend<{ await page.locator("body").focus(); // Navigate to the beginning of the web content. + await voiceOverPlaywright.interact(); + await voiceOverPlaywright.lastSpokenPhrase(); + await voiceOverPlaywright.perform( voiceOverPlaywright.keyboardCommands.jumpToLeftEdge ); From 663bfda4febd222f6cb0b49fd5038f359b7fd5ba Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 7 Jan 2024 17:50:15 +0000 Subject: [PATCH 12/18] temp: add logs --- package.json | 2 +- src/nvdaTest.ts | 15 ++++++++++----- src/voiceOverTest.ts | 8 ++++++-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index bf8ecbf..aa088ea 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "typescript": "^5.3.3" }, "peerDependencies": { - "@guidepup/guidepup": "^0.21.0", + "@guidepup/guidepup": "^0.22.0", "@playwright/test": "^1.29.2" }, "resolutions": { diff --git a/src/nvdaTest.ts b/src/nvdaTest.ts index a956a87..31eb7f4 100644 --- a/src/nvdaTest.ts +++ b/src/nvdaTest.ts @@ -57,9 +57,11 @@ const switchApplications = async ({ while (applicationSwitchRetryCount < MAX_APPLICATION_SWITCH_RETRY_COUNT) { applicationSwitchRetryCount++; + console.log("alt+tab"); await nvdaPlaywright.perform(SWITCH_APPLICATION); const lastSpokenPhrase = await nvdaPlaywright.lastSpokenPhrase(); + console.log(lastSpokenPhrase); if (lastSpokenPhrase.includes(applicationName)) { break; @@ -108,27 +110,30 @@ export const nvdaTest = test.extend<{ nvdaPlaywright.navigateToWebContent = async () => { // Make sure NVDA is not in focus mode. + console.log("exitFocusMode"); await nvdaPlaywright.perform( nvdaPlaywright.keyboardCommands.exitFocusMode ); - await nvdaPlaywright.lastSpokenPhrase(); + console.log(await nvdaPlaywright.lastSpokenPhrase()); // Ensure application is brought to front and focused. + console.log("reportTitle"); await nvdaPlaywright.perform( nvdaPlaywright.keyboardCommands.reportTitle ); + const windowTitle = await nvdaPlaywright.lastSpokenPhrase(); + console.log(windowTitle); - if ( - !(await nvdaPlaywright.lastSpokenPhrase()).includes(applicationName) - ) { + if (!windowTitle.includes(applicationName)) { await switchApplications({ applicationName }); } // Make sure NVDA is not in focus mode. + console.log("exitFocusMode"); await nvdaPlaywright.perform( nvdaPlaywright.keyboardCommands.exitFocusMode ); - await nvdaPlaywright.lastSpokenPhrase(); + console.log(await nvdaPlaywright.lastSpokenPhrase()); // Ensure the document is ready and focused. await page.bringToFront(); diff --git a/src/voiceOverTest.ts b/src/voiceOverTest.ts index ac195ac..464f002 100644 --- a/src/voiceOverTest.ts +++ b/src/voiceOverTest.ts @@ -85,21 +85,25 @@ export const voiceOverTest = test.extend<{ voiceOverPlaywright.navigateToWebContent = async () => { // Ensure application is brought to front and focused. + console.log("activate"); await macOSActivate(applicationName); // Ensure the document is ready and focused. + console.log("bringToFront"); await page.bringToFront(); await page.locator("body").waitFor(); await page.locator("body").focus(); // Navigate to the beginning of the web content. + console.log("interact"); await voiceOverPlaywright.interact(); - await voiceOverPlaywright.lastSpokenPhrase(); + console.log(await voiceOverPlaywright.lastSpokenPhrase()); + console.log("jumpToLeftEdge"); await voiceOverPlaywright.perform( voiceOverPlaywright.keyboardCommands.jumpToLeftEdge ); - await voiceOverPlaywright.lastSpokenPhrase(); + console.log(await voiceOverPlaywright.lastSpokenPhrase()); // Clear out logs. await voiceOverPlaywright.clearItemTextLog(); From 976637fe50f8c015bc1007ab2ea9070806446bbd Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 7 Jan 2024 18:15:43 +0000 Subject: [PATCH 13/18] feat: re-announce text for voiceover --- .../playwright-nvda/tests/headerNavigation.ts | 2 +- .../tests/headerNavigation.ts | 11 ++++++++++- src/nvdaTest.ts | 16 +++++++--------- src/voiceOverTest.ts | 8 ++------ 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/examples/playwright-nvda/tests/headerNavigation.ts b/examples/playwright-nvda/tests/headerNavigation.ts index 8af1ee6..4e3e898 100644 --- a/examples/playwright-nvda/tests/headerNavigation.ts +++ b/examples/playwright-nvda/tests/headerNavigation.ts @@ -33,7 +33,7 @@ export async function headerNavigation({ ) { headingCount++; - log(`Performing command: "H"`); + log(`Performing command: "H" - "Move to next heading"`); await nvda.perform(nvda.keyboardCommands.moveToNextHeading); log(`Screen reader output: "${await nvda.lastSpokenPhrase()}".`); } diff --git a/examples/playwright-voiceover/tests/headerNavigation.ts b/examples/playwright-voiceover/tests/headerNavigation.ts index 2173bfc..0d226bc 100644 --- a/examples/playwright-voiceover/tests/headerNavigation.ts +++ b/examples/playwright-voiceover/tests/headerNavigation.ts @@ -33,8 +33,17 @@ export async function headerNavigation({ ) { headingCount++; - log(`Performing command: "VO+Command+H"`); + // Navigate to the next heading + log(`Performing command: "VO+Command+H" - "Find the next heading"`); await voiceOver.perform(voiceOver.keyboardCommands.findNextHeading); log(`Screen reader output: "${await voiceOver.lastSpokenPhrase()}".`); + + // Describe the item in the VoiceOver cursor as it can sometimes skip part + // of the spoken phrase if interrupted. + log( + `Performing command: "VO+F3" - "Describe the item in the VoiceOver cursor"` + ); + await voiceOver.perform(voiceOver.keyboardCommands.describeItem); + log(`Screen reader output: "${await voiceOver.lastSpokenPhrase()}".`); } } diff --git a/src/nvdaTest.ts b/src/nvdaTest.ts index 31eb7f4..3781d38 100644 --- a/src/nvdaTest.ts +++ b/src/nvdaTest.ts @@ -57,16 +57,18 @@ const switchApplications = async ({ while (applicationSwitchRetryCount < MAX_APPLICATION_SWITCH_RETRY_COUNT) { applicationSwitchRetryCount++; - console.log("alt+tab"); await nvdaPlaywright.perform(SWITCH_APPLICATION); - const lastSpokenPhrase = await nvdaPlaywright.lastSpokenPhrase(); - console.log(lastSpokenPhrase); if (lastSpokenPhrase.includes(applicationName)) { break; } } + + // Firefox has a bug with NVDA where get's stuck in focus mode, so we restart + // NVDA. + await nvdaPlaywright.stop(); + await nvdaPlaywright.start(); }; /** @@ -110,30 +112,26 @@ export const nvdaTest = test.extend<{ nvdaPlaywright.navigateToWebContent = async () => { // Make sure NVDA is not in focus mode. - console.log("exitFocusMode"); await nvdaPlaywright.perform( nvdaPlaywright.keyboardCommands.exitFocusMode ); - console.log(await nvdaPlaywright.lastSpokenPhrase()); + await nvdaPlaywright.lastSpokenPhrase(); // Ensure application is brought to front and focused. - console.log("reportTitle"); await nvdaPlaywright.perform( nvdaPlaywright.keyboardCommands.reportTitle ); const windowTitle = await nvdaPlaywright.lastSpokenPhrase(); - console.log(windowTitle); if (!windowTitle.includes(applicationName)) { await switchApplications({ applicationName }); } // Make sure NVDA is not in focus mode. - console.log("exitFocusMode"); await nvdaPlaywright.perform( nvdaPlaywright.keyboardCommands.exitFocusMode ); - console.log(await nvdaPlaywright.lastSpokenPhrase()); + await nvdaPlaywright.lastSpokenPhrase(); // Ensure the document is ready and focused. await page.bringToFront(); diff --git a/src/voiceOverTest.ts b/src/voiceOverTest.ts index 464f002..ac195ac 100644 --- a/src/voiceOverTest.ts +++ b/src/voiceOverTest.ts @@ -85,25 +85,21 @@ export const voiceOverTest = test.extend<{ voiceOverPlaywright.navigateToWebContent = async () => { // Ensure application is brought to front and focused. - console.log("activate"); await macOSActivate(applicationName); // Ensure the document is ready and focused. - console.log("bringToFront"); await page.bringToFront(); await page.locator("body").waitFor(); await page.locator("body").focus(); // Navigate to the beginning of the web content. - console.log("interact"); await voiceOverPlaywright.interact(); - console.log(await voiceOverPlaywright.lastSpokenPhrase()); + await voiceOverPlaywright.lastSpokenPhrase(); - console.log("jumpToLeftEdge"); await voiceOverPlaywright.perform( voiceOverPlaywright.keyboardCommands.jumpToLeftEdge ); - console.log(await voiceOverPlaywright.lastSpokenPhrase()); + await voiceOverPlaywright.lastSpokenPhrase(); // Clear out logs. await voiceOverPlaywright.clearItemTextLog(); From d968aacc94e85070aeb927434005e926c9e654b4 Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 7 Jan 2024 18:16:16 +0000 Subject: [PATCH 14/18] chore: peer deps versions --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aa088ea..8b5840d 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ }, "peerDependencies": { "@guidepup/guidepup": "^0.22.0", - "@playwright/test": "^1.29.2" + "@playwright/test": "^1.40.1" }, "resolutions": { "strip-ansi": "6.0.1", From bc8a68af88ff497df94a5c04d1e67d1349b1499d Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 7 Jan 2024 18:28:46 +0000 Subject: [PATCH 15/18] fix: try to workaround firefox being awkward --- src/nvdaTest.ts | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/nvdaTest.ts b/src/nvdaTest.ts index 3781d38..f1888de 100644 --- a/src/nvdaTest.ts +++ b/src/nvdaTest.ts @@ -51,7 +51,11 @@ const switchApplications = async ({ }: { applicationName: string; }) => { - // Ensure application is brought to front and focused. + // Firefox has a bug with NVDA where NVDA get's stuck in focus mode if + // Firefox is the currently focused application. + // REF: https://github.com/nvaccess/nvda/issues/5758 + // We swap to a different application, restart NVDA, and then switch back. + let applicationSwitchRetryCount = 0; while (applicationSwitchRetryCount < MAX_APPLICATION_SWITCH_RETRY_COUNT) { @@ -60,15 +64,26 @@ const switchApplications = async ({ await nvdaPlaywright.perform(SWITCH_APPLICATION); const lastSpokenPhrase = await nvdaPlaywright.lastSpokenPhrase(); - if (lastSpokenPhrase.includes(applicationName)) { + if (!lastSpokenPhrase.includes(applicationName)) { break; } } - // Firefox has a bug with NVDA where get's stuck in focus mode, so we restart - // NVDA. await nvdaPlaywright.stop(); await nvdaPlaywright.start(); + + applicationSwitchRetryCount = 0; + + while (applicationSwitchRetryCount < MAX_APPLICATION_SWITCH_RETRY_COUNT) { + applicationSwitchRetryCount++; + + await nvdaPlaywright.perform(SWITCH_APPLICATION); + const lastSpokenPhrase = await nvdaPlaywright.lastSpokenPhrase(); + + if (lastSpokenPhrase.includes(applicationName)) { + break; + } + } }; /** @@ -123,6 +138,8 @@ export const nvdaTest = test.extend<{ ); const windowTitle = await nvdaPlaywright.lastSpokenPhrase(); + console.log({ windowTitle }); + if (!windowTitle.includes(applicationName)) { await switchApplications({ applicationName }); } From 4746ae59d8864eb387d2339ba3ada0cf090be419 Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 7 Jan 2024 18:42:22 +0000 Subject: [PATCH 16/18] refactor: try to get application switching to work better --- src/nvdaTest.ts | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/src/nvdaTest.ts b/src/nvdaTest.ts index f1888de..057f12a 100644 --- a/src/nvdaTest.ts +++ b/src/nvdaTest.ts @@ -1,4 +1,4 @@ -import { test } from "@playwright/test"; +import { Page, test } from "@playwright/test"; import { nvda, WindowsKeyCodes, WindowsModifiers } from "@guidepup/guidepup"; import type { NVDA } from "@guidepup/guidepup"; import { applicationNameMap } from "./applicationNameMap"; @@ -46,12 +46,21 @@ const SWITCH_APPLICATION = { modifiers: [WindowsModifiers.Alt], }; -const switchApplications = async ({ +const focusBrowser = async ({ applicationName, + page, }: { applicationName: string; + page: Page; }) => { - // Firefox has a bug with NVDA where NVDA get's stuck in focus mode if + await nvdaPlaywright.perform(nvdaPlaywright.keyboardCommands.reportTitle); + let windowTitle = await nvdaPlaywright.lastSpokenPhrase(); + + if (windowTitle.includes(applicationName)) { + return; + } + + // Firefox has a bug with NVDA where NVDA gets stuck in focus mode if // Firefox is the currently focused application. // REF: https://github.com/nvaccess/nvda/issues/5758 // We swap to a different application, restart NVDA, and then switch back. @@ -62,15 +71,24 @@ const switchApplications = async ({ applicationSwitchRetryCount++; await nvdaPlaywright.perform(SWITCH_APPLICATION); - const lastSpokenPhrase = await nvdaPlaywright.lastSpokenPhrase(); + await nvdaPlaywright.perform(nvdaPlaywright.keyboardCommands.reportTitle); + windowTitle = await nvdaPlaywright.lastSpokenPhrase(); - if (!lastSpokenPhrase.includes(applicationName)) { + if (!windowTitle.includes(applicationName)) { break; } } await nvdaPlaywright.stop(); await nvdaPlaywright.start(); + await page.bringToFront(); + + await nvdaPlaywright.perform(nvdaPlaywright.keyboardCommands.reportTitle); + windowTitle = await nvdaPlaywright.lastSpokenPhrase(); + + if (windowTitle.includes(applicationName)) { + return; + } applicationSwitchRetryCount = 0; @@ -78,9 +96,10 @@ const switchApplications = async ({ applicationSwitchRetryCount++; await nvdaPlaywright.perform(SWITCH_APPLICATION); - const lastSpokenPhrase = await nvdaPlaywright.lastSpokenPhrase(); + await nvdaPlaywright.perform(nvdaPlaywright.keyboardCommands.reportTitle); + windowTitle = await nvdaPlaywright.lastSpokenPhrase(); - if (lastSpokenPhrase.includes(applicationName)) { + if (!windowTitle.includes(applicationName)) { break; } } @@ -133,16 +152,7 @@ export const nvdaTest = test.extend<{ await nvdaPlaywright.lastSpokenPhrase(); // Ensure application is brought to front and focused. - await nvdaPlaywright.perform( - nvdaPlaywright.keyboardCommands.reportTitle - ); - const windowTitle = await nvdaPlaywright.lastSpokenPhrase(); - - console.log({ windowTitle }); - - if (!windowTitle.includes(applicationName)) { - await switchApplications({ applicationName }); - } + await focusBrowser({ applicationName, page }); // Make sure NVDA is not in focus mode. await nvdaPlaywright.perform( From 13b91e43d393ec6f564a9b99cd7ac41146043c01 Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 7 Jan 2024 18:48:58 +0000 Subject: [PATCH 17/18] fix: try alt-escape + opening page and closing --- src/nvdaTest.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/nvdaTest.ts b/src/nvdaTest.ts index 057f12a..cf3e9f8 100644 --- a/src/nvdaTest.ts +++ b/src/nvdaTest.ts @@ -42,7 +42,7 @@ const nvdaPlaywright: NVDAPlaywright = nvda as NVDAPlaywright; const MAX_APPLICATION_SWITCH_RETRY_COUNT = 10; const SWITCH_APPLICATION = { - keyCode: [WindowsKeyCodes.Tab], + keyCode: [WindowsKeyCodes.Escape], modifiers: [WindowsModifiers.Alt], }; @@ -144,6 +144,10 @@ export const nvdaTest = test.extend<{ throw new Error(`Browser ${browserName} is not installed.`); } + await page.goto("about:blank", { waitUntil: "load" }); + await page.bringToFront(); + await page.close(); + nvdaPlaywright.navigateToWebContent = async () => { // Make sure NVDA is not in focus mode. await nvdaPlaywright.perform( From 83fbae54dfb8da5afb1b614a76d2d9f0a20a3a11 Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 7 Jan 2024 18:53:32 +0000 Subject: [PATCH 18/18] fix: closing the page fails the test --- src/nvdaTest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/nvdaTest.ts b/src/nvdaTest.ts index cf3e9f8..a196bca 100644 --- a/src/nvdaTest.ts +++ b/src/nvdaTest.ts @@ -146,7 +146,6 @@ export const nvdaTest = test.extend<{ await page.goto("about:blank", { waitUntil: "load" }); await page.bringToFront(); - await page.close(); nvdaPlaywright.navigateToWebContent = async () => { // Make sure NVDA is not in focus mode.