diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 113171b..86be33a 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -36,11 +36,11 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v3
+ uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -51,7 +51,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@v3
+ uses: github/codeql-action/autobuild@v4
# โน๏ธ Command-line programs to run using the OS shell.
# ๐ https://git.io/JvXDl
@@ -65,5 +65,5 @@ jobs:
# make release
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v3
+ uses: github/codeql-action/analyze@v4
continue-on-error: true
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 974ccef..56ba73d 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -12,8 +12,8 @@ jobs:
publish:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
- - uses: actions/setup-node@v6
+ - uses: actions/checkout@v7
+ - uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
registry-url: https://registry.npmjs.org
diff --git a/.github/workflows/test-nvda.yml b/.github/workflows/test-nvda.yml
index 7a6535d..a6e9510 100644
--- a/.github/workflows/test-nvda.yml
+++ b/.github/workflows/test-nvda.yml
@@ -18,16 +18,16 @@ jobs:
os: [windows-2022, windows-2025]
browser: [chromium, firefox]
steps:
- - uses: actions/checkout@v6
- - uses: actions/setup-node@v6
+ - uses: actions/checkout@v7
+ - uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
- - run: npx -y --min-release-age=0 @guidepup/setup@0.24.6 setup --ci --macos-record
+ - run: npx -y --min-release-age=0 --ignore-scripts=false @guidepup/setup@0.29.2 setup --ci
- run: yarn install --frozen-lockfile
- run: yarn pretest
- - run: npx -y --min-release-age=0 @guidepup/setup@0.24.6 install
+ - run: npx -y --min-release-age=0 --ignore-scripts=false @guidepup/setup@0.29.2 install
- run: yarn test:nvda:${{ matrix.browser }}
- - uses: actions/upload-artifact@v4
+ - uses: actions/upload-artifact@v7
if: always()
continue-on-error: true
with:
diff --git a/.github/workflows/test-orca.yml b/.github/workflows/test-orca.yml
new file mode 100644
index 0000000..4a3bd53
--- /dev/null
+++ b/.github/workflows/test-orca.yml
@@ -0,0 +1,37 @@
+name: Test Orca
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+permissions:
+ contents: read
+
+jobs:
+ test-orca:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-26.04]
+ browser: [firefox]
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-node@v7
+ with:
+ node-version-file: .nvmrc
+ - run: npx -y --min-release-age=0 --ignore-scripts=false @guidepup/setup@0.29.2 setup --ci
+ - run: yarn install --frozen-lockfile
+ - run: yarn pretest
+ - run: npx -y --min-release-age=0 --ignore-scripts=false @guidepup/setup@0.29.2 install
+ - run: xvfb-run dbus-run-session -- yarn test:orca:${{ matrix.browser }}
+ - uses: actions/upload-artifact@v7
+ if: always()
+ continue-on-error: true
+ with:
+ name: artifacts-${{ matrix.os }}-${{ matrix.browser }}
+ path: |
+ **/test-results/**/*
+ **/recordings/**/*
diff --git a/.github/workflows/test-screenreader.yml b/.github/workflows/test-screenreader.yml
index 1d8ceda..277a001 100644
--- a/.github/workflows/test-screenreader.yml
+++ b/.github/workflows/test-screenreader.yml
@@ -10,24 +10,50 @@ permissions:
contents: read
jobs:
- test-screenreader:
+ test-screenreader-chromium:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
- os: [macos-14, macos-15, macos-26, windows-2022, windows-2025]
+ os: [macos-15, macos-26, xcode-27, windows-2022, windows-2025]
browser: [chromium]
steps:
- - uses: actions/checkout@v6
- - uses: actions/setup-node@v6
+ - uses: actions/checkout@v7
+ - uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
- - run: npx -y --min-release-age=0 @guidepup/setup@0.24.6 setup --ci --macos-record
+ - run: npx -y --min-release-age=0 --ignore-scripts=false @guidepup/setup@0.29.2 setup --ci --macos-record
- run: yarn install --frozen-lockfile
- run: yarn pretest
- - run: npx -y --min-release-age=0 @guidepup/setup@0.24.6 install
+ - run: npx -y --min-release-age=0 --ignore-scripts=false @guidepup/setup@0.29.2 install
- run: yarn test:screenreader:${{ matrix.browser }}
- - uses: actions/upload-artifact@v4
+ - uses: actions/upload-artifact@v7
+ if: always()
+ continue-on-error: true
+ with:
+ name: artifacts-${{ matrix.os }}-${{ matrix.browser }}
+ path: |
+ **/test-results/**/*
+ **/recordings/**/*
+
+ test-screenreader-orca-firefox:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-26.04]
+ browser: [firefox]
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-node@v7
+ with:
+ node-version-file: .nvmrc
+ - run: npx -y --min-release-age=0 --ignore-scripts=false @guidepup/setup@0.29.2 setup --ci
+ - run: yarn install --frozen-lockfile
+ - run: yarn pretest
+ - run: npx -y --min-release-age=0 --ignore-scripts=false @guidepup/setup@0.29.2 install
+ - run: xvfb-run dbus-run-session -- yarn test:screenreader:${{ matrix.browser }}
+ - uses: actions/upload-artifact@v7
if: always()
continue-on-error: true
with:
diff --git a/.github/workflows/test-voiceover.yml b/.github/workflows/test-voiceover.yml
index bbceec3..fa426db 100644
--- a/.github/workflows/test-voiceover.yml
+++ b/.github/workflows/test-voiceover.yml
@@ -15,19 +15,19 @@ jobs:
strategy:
fail-fast: false
matrix:
- os: [macos-14, macos-15, macos-26]
+ os: [macos-15, macos-26, xcode-27]
browser: [chromium, firefox, webkit]
steps:
- - uses: actions/checkout@v6
- - uses: actions/setup-node@v6
+ - uses: actions/checkout@v7
+ - uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
- - run: npx -y --min-release-age=0 @guidepup/setup@0.24.6 setup --ci --macos-record
+ - run: npx -y --min-release-age=0 --ignore-scripts=false @guidepup/setup@0.29.2 setup --ci --macos-record
- run: yarn install --frozen-lockfile
- run: yarn pretest
- - run: npx -y --min-release-age=0 @guidepup/setup@0.24.6 install
+ - run: npx -y --min-release-age=0 --ignore-scripts=false @guidepup/setup@0.29.2 install
- run: yarn test:voiceover:${{ matrix.browser }}
- - uses: actions/upload-artifact@v4
+ - uses: actions/upload-artifact@v7
if: always()
continue-on-error: true
with:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index cb3934a..905b446 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -13,8 +13,8 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
- - uses: actions/setup-node@v6
+ - uses: actions/checkout@v7
+ - uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
- run: yarn install --frozen-lockfile
diff --git a/README.md b/README.md
index 14678e2..e1b4e1c 100644
--- a/README.md
+++ b/README.md
@@ -11,15 +11,18 @@
## [Documentation](https://guidepup.dev) | [API Reference](https://www.guidepup.dev/docs/api/class-guidepup)
-[](https://apps.apple.com/us/app/macos-sonoma/id6450717509)
-[](https://apps.apple.com/us/app/macos-sequoia/id6596773750)
-[](https://www.apple.com/uk/os/macos/)
+[](https://support.apple.com/en-gb/102662)
+[](https://support.apple.com/en-gb/102662)
+[](https://support.apple.com/en-gb/102662)
+[](https://support.apple.com/en-gb/127455)
[](https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2022)
[](https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2025)
+[](https://releases.ubuntu.com/resolute/)
+[](https://www.debian.org/releases/trixie/)
Guidepup is a screen reader automation library for testing.
-This package provides [Guidepup](https://github.com/guidepup/guidepup) integration with [Playwright](https://playwright.dev/) to enable testing with VoiceOver on MacOS and NVDA on Windows.
+This package provides [Guidepup](https://github.com/guidepup/guidepup) integration with [Playwright](https://playwright.dev/) to enable testing with NVDA on Windows, Orca on Linux, and VoiceOver on macOS.
## Capabilities
@@ -78,7 +81,7 @@ Check out the configuration this adds [in the `config.ts` file](./src/config.ts)
### Web content navigation
-In addition to the Guidepup APIs the `screenReader`, `voiceOver`, and `nvda` instances provided by the Guidepup Playwright setup have an additional utility method `.navigateToWebContent()`.
+In addition to the Guidepup APIs the `screenReader`, `nvda`, `orca`, and `voiceOver` instances provided by the Guidepup Playwright setup have an additional utility method `.navigateToWebContent()`.
This method will navigate the screen reader to the first element of the document body in the browser.
@@ -101,7 +104,7 @@ await screenReader.navigateToWebContent();
### Providing screen reader start options
-The options provided to `screenReader.start([options])`, `nvda.start([options])`, or `voiceOver.start([options])` can be configured using `test.use(config)` as follows:
+The options provided to `screenReader.start([options])`, `nvda.start([options])`, `orca.start([options])`, or `voiceOver.start([options])` can be configured using `test.use(config)` as follows:
```ts
// Screen Reader Example
@@ -112,24 +115,32 @@ test.use({ screenReaderStartOptions: { capture: true } });
```
```ts
-// VoiceOver Example
-import { voiceOverTest as test } from "@guidepup/playwright";
+// NVDA Example
+import { nvdaTest as test } from "@guidepup/playwright";
// Capture all spoken phrases, including usage hints
-test.use({ voiceOverStartOptions: { capture: true } });
+test.use({ nvdaStartOptions: { capture: true } });
```
```ts
-// NVDA Example
-import { nvdaTest as test } from "@guidepup/playwright";
+// Orca Example
+import { orcaTest as test } from "@guidepup/playwright";
// Capture all spoken phrases, including usage hints
-test.use({ nvdaStartOptions: { capture: true } });
+test.use({ orcaStartOptions: { capture: true } });
```
-The default for VoiceOver and NVDA is set to `"initial"`. `true` captures all spoken phrases, including usage hints. `false` disables spoken phrase capture.
+```ts
+// VoiceOver Example
+import { voiceOverTest as test } from "@guidepup/playwright";
-### VoiceOver example
+// Capture all spoken phrases, including usage hints
+test.use({ voiceOverStartOptions: { capture: true } });
+```
+
+The default for NVDA, Orca, and VoiceOver is set to `"initial"`. `true` captures all spoken phrases, including usage hints. `false` disables spoken phrase capture.
+
+### NVDA example
`playwright.config.ts`:
@@ -144,10 +155,10 @@ const config: PlaywrightTestConfig = {
retries: 2,
projects: [
{
- name: "webkit",
+ name: "chrome",
// Take care to ensure all usage is headed - screen readers cannot
// operate against headless browsers.
- use: { ...devices["Desktop Safari"], headless: false },
+ use: { ...devices["Desktop Chrome"], headless: false },
},
],
};
@@ -155,41 +166,43 @@ const config: PlaywrightTestConfig = {
export default config;
```
-`voiceOver.spec.ts`:
+`nvda.spec.ts`:
```ts
-import { voiceOverTest as test } from "@guidepup/playwright";
+import { nvdaTest as test } from "@guidepup/playwright";
import { expect } from "@playwright/test";
-test.describe("Playwright VoiceOver", () => {
- test("I can navigate the Guidepup Github page with VoiceOver", async ({
+test.describe("Playwright NVDA", () => {
+ test("I can navigate the Guidepup Github page with NVDA", async ({
page,
- voiceOver,
+ nvda,
}) => {
// Navigate to Guidepup GitHub page
await page.goto("https://github.com/guidepup/guidepup", {
waitUntil: "load",
});
- // Wait for page to be ready
+ // Wait for page to be ready and setup
const header = page.locator('header[role="banner"]');
await header.waitFor();
// Interact with the page
- await voiceOver.navigateToWebContent();
+ await nvda.navigateToWebContent();
- // Move across the page menu to the Guidepup heading using VoiceOver
- while ((await voiceOver.itemText()) !== "Guidepup heading level 1") {
- await voiceOver.nextHeading();
+ // Move across the page menu to the Guidepup heading using NVDA
+ while (
+ !(await nvda.lastSpokenPhrase()).includes("Guidepup, heading, level 1")
+ ) {
+ await nvda.nextHeading();
}
// Assert that the spoken phrases are as expected
- expect(JSON.stringify(await voiceOver.spokenPhraseLog())).toMatchSnapshot();
+ expect(JSON.stringify(await nvda.spokenPhraseLog())).toMatchSnapshot();
});
});
```
-### NVDA example
+### Orca example
`playwright.config.ts`:
@@ -215,16 +228,16 @@ const config: PlaywrightTestConfig = {
export default config;
```
-`nvda.spec.ts`:
+`orca.spec.ts`:
```ts
-import { nvdaTest as test } from "@guidepup/playwright";
+import { orcaTest as test } from "@guidepup/playwright";
import { expect } from "@playwright/test";
-test.describe("Playwright NVDA", () => {
- test("I can navigate the Guidepup Github page with NVDA", async ({
+test.describe("Playwright Orca", () => {
+ test("I can navigate the Guidepup Github page with Orca", async ({
page,
- nvda,
+ orca,
}) => {
// Navigate to Guidepup GitHub page
await page.goto("https://github.com/guidepup/guidepup", {
@@ -236,17 +249,77 @@ test.describe("Playwright NVDA", () => {
await header.waitFor();
// Interact with the page
- await nvda.navigateToWebContent();
+ await orca.navigateToWebContent();
- // Move across the page menu to the Guidepup heading using NVDA
+ // Move across the page menu to the Guidepup heading using Orca
while (
- !(await nvda.lastSpokenPhrase()).includes("Guidepup, heading, level 1")
+ !(await orca.lastSpokenPhrase()).includes("Guidepup, heading, level 1")
) {
- await nvda.nextHeading();
+ await orca.nextHeading();
}
// Assert that the spoken phrases are as expected
- expect(JSON.stringify(await nvda.spokenPhraseLog())).toMatchSnapshot();
+ expect(JSON.stringify(await orca.spokenPhraseLog())).toMatchSnapshot();
+ });
+});
+```
+
+### VoiceOver example
+
+`playwright.config.ts`:
+
+```ts
+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",
+ // Take care to ensure all usage is headed - screen readers cannot
+ // operate against headless browsers.
+ 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 with VoiceOver", async ({
+ page,
+ voiceOver,
+ }) => {
+ // Navigate to Guidepup GitHub page
+ await page.goto("https://github.com/guidepup/guidepup", {
+ waitUntil: "load",
+ });
+
+ // Wait for page to be ready
+ const header = page.locator('header[role="banner"]');
+ await header.waitFor();
+
+ // Interact with the page
+ await voiceOver.navigateToWebContent();
+
+ // Move across the page menu to the Guidepup heading using VoiceOver
+ while ((await voiceOver.itemText()) !== "Guidepup heading level 1") {
+ await voiceOver.nextHeading();
+ }
+
+ // Assert that the spoken phrases are as expected
+ expect(JSON.stringify(await voiceOver.spokenPhraseLog())).toMatchSnapshot();
});
});
```
@@ -267,14 +340,14 @@ expect(capture.spokenPhrase).toContain("Added to cart");
Check out some of the other Guidepup modules:
-- [`@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/guidepup`](https://github.com/guidepup/guidepup/) - Reliable automation for your screen reader a11y workflows through JavaScript supporting VoiceOver, NVDA, and Orca.
- [`@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.
## Resources
-- [Documentation](https://www.guidepup.dev/docs/example)
+- [Documentation](https://www.guidepup.dev/docs/getting-started/real-world-example)
- [API Reference](https://www.guidepup.dev/docs/api/class-guidepup)
- [Contributing](.github/CONTRIBUTING.md)
- [Changelog](https://github.com/guidepup/guidepup-playwright/releases)
diff --git a/examples/playwright-nvda/README.md b/examples/playwright-nvda/README.md
index 4d6960d..60db23c 100644
--- a/examples/playwright-nvda/README.md
+++ b/examples/playwright-nvda/README.md
@@ -13,6 +13,5 @@ npm run test
## Test flow
1. The test launches the browser using Playwright
-2. Navigates to the GitHub website
+2. Navigates to the Guidepup 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/examples/playwright-nvda/chromium.config.ts b/examples/playwright-nvda/chromium.config.ts
index def99d5..bfbb9af 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: 1,
+ retries: 5,
projects: [
{
name: "chromium",
diff --git a/examples/playwright-nvda/firefox.config.ts b/examples/playwright-nvda/firefox.config.ts
index 92a6e17..40e4349 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: 1,
+ retries: 5,
projects: [
{
name: "firefox",
diff --git a/examples/playwright-nvda/tests/chromium/chromium.spec.ts b/examples/playwright-nvda/tests/chromium/chromium.spec.ts
index fb8f631..63f5187 100644
--- a/examples/playwright-nvda/tests/chromium/chromium.spec.ts
+++ b/examples/playwright-nvda/tests/chromium/chromium.spec.ts
@@ -1,11 +1,46 @@
import { platform, release } from "os";
import { headerNavigation } from "../headerNavigation";
import { logIncludesExpectedPhrases } from "../../../logIncludesExpectedPhrases";
-import { windowsRecord } from "@guidepup/record";
-import spokenPhraseSnapshot from "./chromium.spokenPhrase.snapshot.json";
+import spokenPhraseSnapshotDocs from "./chromium.spokenPhrase.docs.snapshot.json";
+import spokenPhraseSnapshotTextarea from "./chromium.spokenPhrase.textarea.snapshot.json";
import { nvdaTest as test } from "../../../../src";
+import { log } from "../../../log";
+import { delay } from "../../../../src/delay";
+import { expect } from "@playwright/test";
-test.use({ nvdaStartOptions: { capture: "initial" } });
+const record = async (filepath: string) => {
+ try {
+ const { windowsRecord } = await import("@guidepup/record");
+
+ return windowsRecord(filepath);
+ } catch {
+ console.warn(
+ "@guidepup/record not available. Recording will be skipped. This is expected on platforms without ffmpeg support (e.g., Windows ARM64).",
+ );
+ }
+};
+
+test.use({
+ nvdaStartOptions: {
+ capture: "initial",
+ settings: {
+ speech: {
+ oneCore: {
+ voice:
+ "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech_OneCore\\Voices\\Tokens\\MSTTS_V110_enGB_SusanM",
+ },
+ },
+ vision: {
+ NVDAHighlighter: {
+ highlightFocus: false,
+ highlightNavigator: false,
+ highlightBrowseMode: false,
+ enabled: false,
+ },
+ },
+ },
+ },
+});
test.describe("Chromium Playwright NVDA", () => {
test("I can navigate the Guidepup documentation site", async ({
@@ -32,10 +67,10 @@ test.describe("Chromium Playwright NVDA", () => {
retry,
});
- let stopRecording: (() => void) | undefined;
+ let stopRecording: (() => Promise) | undefined;
try {
- stopRecording = windowsRecord(recordingFilePath);
+ stopRecording = await record(recordingFilePath);
await headerNavigation({ page, nvda });
@@ -46,9 +81,72 @@ test.describe("Chromium Playwright NVDA", () => {
console.log(JSON.stringify(spokenPhraseLog, undefined, 2));
- logIncludesExpectedPhrases(spokenPhraseLog, spokenPhraseSnapshot);
+ logIncludesExpectedPhrases(spokenPhraseLog, spokenPhraseSnapshotDocs);
+ } finally {
+ await stopRecording?.();
+ }
+ });
+
+ test("I can type text into text areas and it is announced (https://github.com/guidepup/guidepup/issues/115)", async ({
+ browser,
+ browserName,
+ page,
+ nvda,
+ }) => {
+ const osName = platform();
+ const osVersion = release();
+ const browserVersion = browser.version();
+ const screenReaderName = nvda.name;
+ const screenReaderVersion = nvda.version;
+ const { retry } = test.info();
+ const recordingFilePath = `./recordings/playwright-nvda-textarea-${osName}-${osVersion}-${browserName}-${browserVersion}-attempt-${retry}-${+new Date()}.mov`;
+
+ console.table({
+ osName,
+ osVersion,
+ browserName,
+ browserVersion,
+ screenReaderName,
+ screenReaderVersion,
+ retry,
+ });
+
+ let stopRecording: (() => Promise) | undefined;
+
+ try {
+ stopRecording = await record(recordingFilePath);
+
+ log("Navigating to textarea test page.");
+ await page.goto("about:blank", {
+ waitUntil: "load",
+ });
+
+ await page.setContent(`
+
+
+ `);
+
+ const input = page.locator("#test");
+ await input.waitFor();
+ await delay(500);
+
+ await nvda.navigateToWebContent();
+ await delay(500);
+
+ await nvda.type("Abc");
+ await nvda.press("Control+A");
+ await nvda.press("Delete");
+ await nvda.type("123");
+
+ await expect(input).toHaveValue("123");
+
+ const spokenPhraseLog = await nvda.spokenPhraseLog();
+
+ console.log(JSON.stringify(spokenPhraseLog, undefined, 2));
+
+ logIncludesExpectedPhrases(spokenPhraseLog, spokenPhraseSnapshotTextarea);
} finally {
- stopRecording?.();
+ await stopRecording?.();
}
});
});
diff --git a/examples/playwright-nvda/tests/chromium/chromium.spokenPhrase.docs.snapshot.json b/examples/playwright-nvda/tests/chromium/chromium.spokenPhrase.docs.snapshot.json
new file mode 100644
index 0000000..92dec16
--- /dev/null
+++ b/examples/playwright-nvda/tests/chromium/chromium.spokenPhrase.docs.snapshot.json
@@ -0,0 +1,10 @@
+[
+ "Guidepup, heading, level 1",
+ "Reliable Automation",
+ "Full Control, heading, level 3",
+ "Mirrors Real User Experience, heading, level 3",
+ "Framework Agnostic, heading, level 3",
+ "Run with Jest, with Playwright, as an independent script, no vendor lock-in.",
+ "link, Get Started",
+ "link, Git Hub"
+]
diff --git a/examples/playwright-nvda/tests/chromium/chromium.spokenPhrase.snapshot.json b/examples/playwright-nvda/tests/chromium/chromium.spokenPhrase.snapshot.json
deleted file mode 100644
index 5dda94a..0000000
--- a/examples/playwright-nvda/tests/chromium/chromium.spokenPhrase.snapshot.json
+++ /dev/null
@@ -1,7 +0,0 @@
-[
- "Guidepup, heading, level 1",
- "Reliable Automation",
- "Full Control, heading, level 3",
- "Mirrors Real User Experience, heading, level 3",
- "Framework Agnostic, heading, level 3"
-]
diff --git a/examples/playwright-nvda/tests/chromium/chromium.spokenPhrase.textarea.snapshot.json b/examples/playwright-nvda/tests/chromium/chromium.spokenPhrase.textarea.snapshot.json
new file mode 100644
index 0000000..985cfd3
--- /dev/null
+++ b/examples/playwright-nvda/tests/chromium/chromium.spokenPhrase.textarea.snapshot.json
@@ -0,0 +1 @@
+["A", "b", "c", "Abc selected", "blank", "1", "2", "3"]
diff --git a/examples/playwright-nvda/tests/firefox/firefox.spec.ts b/examples/playwright-nvda/tests/firefox/firefox.spec.ts
index f0e1540..54f26fd 100644
--- a/examples/playwright-nvda/tests/firefox/firefox.spec.ts
+++ b/examples/playwright-nvda/tests/firefox/firefox.spec.ts
@@ -1,13 +1,22 @@
import { platform, release } from "os";
import { headerNavigation } from "../headerNavigation";
import { logIncludesExpectedPhrases } from "../../../logIncludesExpectedPhrases";
-import { windowsRecord } from "@guidepup/record";
import spokenPhraseSnapshot from "./firefox.spokenPhrase.snapshot.json";
import { nvdaTest as test } from "../../../../src";
-test.use({ nvdaStartOptions: { capture: "initial" } });
+const record = async (filepath: string) => {
+ try {
+ const { windowsRecord } = await import("@guidepup/record");
-test.describe("Firefox Playwright VoiceOver", () => {
+ return windowsRecord(filepath);
+ } catch {
+ console.warn(
+ "@guidepup/record not available. Recording will be skipped. This is expected on platforms without ffmpeg support (e.g., Windows ARM64).",
+ );
+ }
+};
+
+test.describe("Firefox Playwright NVDA", () => {
test("I can navigate the Guidepup documentation site", async ({
browser,
browserName,
@@ -32,10 +41,10 @@ test.describe("Firefox Playwright VoiceOver", () => {
retry,
});
- let stopRecording: (() => void) | undefined;
+ let stopRecording: (() => Promise) | undefined;
try {
- stopRecording = windowsRecord(recordingFilePath);
+ stopRecording = await record(recordingFilePath);
await headerNavigation({ page, nvda });
@@ -48,7 +57,7 @@ test.describe("Firefox Playwright VoiceOver", () => {
logIncludesExpectedPhrases(spokenPhraseLog, spokenPhraseSnapshot);
} finally {
- stopRecording?.();
+ await stopRecording?.();
}
});
});
diff --git a/examples/playwright-nvda/tests/firefox/firefox.spokenPhrase.snapshot.json b/examples/playwright-nvda/tests/firefox/firefox.spokenPhrase.snapshot.json
index 5dda94a..92dec16 100644
--- a/examples/playwright-nvda/tests/firefox/firefox.spokenPhrase.snapshot.json
+++ b/examples/playwright-nvda/tests/firefox/firefox.spokenPhrase.snapshot.json
@@ -3,5 +3,8 @@
"Reliable Automation",
"Full Control, heading, level 3",
"Mirrors Real User Experience, heading, level 3",
- "Framework Agnostic, heading, level 3"
+ "Framework Agnostic, heading, level 3",
+ "Run with Jest, with Playwright, as an independent script, no vendor lock-in.",
+ "link, Get Started",
+ "link, Git Hub"
]
diff --git a/examples/playwright-nvda/tests/headerNavigation.ts b/examples/playwright-nvda/tests/headerNavigation.ts
index ebf7ca1..d10d469 100644
--- a/examples/playwright-nvda/tests/headerNavigation.ts
+++ b/examples/playwright-nvda/tests/headerNavigation.ts
@@ -1,7 +1,7 @@
-import { Page } from "@playwright/test";
+import { expect, type Page } from "@playwright/test";
+import { delay } from "../../../src/delay";
import { log } from "../../log";
import type { NVDAPlaywright } from "../../../src";
-import { delay } from "../../../src/delay";
const MAX_NAVIGATION_LOOP = 10;
@@ -40,4 +40,28 @@ export async function headerNavigation({
await nvda.nextHeading();
log(`Screen reader output: "${await nvda.lastSpokenPhrase()}".`);
}
+
+ let tabCount = 0;
+
+ // Move across text and buttons using NVDA
+ while (
+ !(await nvda.lastSpokenPhrase()).replaceAll(/\s/g, "").includes("GitHub") &&
+ tabCount <= MAX_NAVIGATION_LOOP
+ ) {
+ tabCount++;
+
+ log(`Performing command: "Right"`);
+ await nvda.next();
+ log(`Screen reader output: "${await nvda.lastSpokenPhrase()}".`);
+ }
+
+ log(`Performing command: "Left"`);
+ await nvda.previous();
+ log(`Screen reader output: "${await nvda.lastSpokenPhrase()}".`);
+
+ log(`Performing command: "Enter"`);
+ await nvda.act();
+ log(`Screen reader output: "${await nvda.lastSpokenPhrase()}".`);
+
+ await expect(page).toHaveURL("https://www.guidepup.dev/docs/getting-started");
}
diff --git a/examples/playwright-orca/README.md b/examples/playwright-orca/README.md
new file mode 100644
index 0000000..6608567
--- /dev/null
+++ b/examples/playwright-orca/README.md
@@ -0,0 +1,17 @@
+# Orca Example
+
+An example demonstrating using Guidepup for testing Orca automation with [Playwright](https://playwright.dev/).
+
+Run this example's test with the following from the root directory:
+
+```console
+xvfb-run dbus-run-session -- npm run test
+```
+
+> Note: please ensure you have set up the [Orca prerequisites](https://www.guidepup.dev/docs/guides/environment) before running this example.
+
+## Test flow
+
+1. The test launches the browser using Playwright
+2. Navigates to the Guidepup website
+3. Moves through the website using Orca controlled by Guidepup
diff --git a/examples/playwright-orca/firefox.config.ts b/examples/playwright-orca/firefox.config.ts
new file mode 100644
index 0000000..40e4349
--- /dev/null
+++ b/examples/playwright-orca/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/examples/playwright-orca/tests/firefox/firefox.spec.ts b/examples/playwright-orca/tests/firefox/firefox.spec.ts
new file mode 100644
index 0000000..fd50625
--- /dev/null
+++ b/examples/playwright-orca/tests/firefox/firefox.spec.ts
@@ -0,0 +1,65 @@
+import { platform, release } from "os";
+import { headerNavigation } from "../headerNavigation";
+import { logIncludesExpectedPhrases } from "../../../logIncludesExpectedPhrases";
+import spokenPhraseSnapshot from "./firefox.spokenPhrase.snapshot.json";
+import { orcaTest as test } from "../../../../src";
+
+const record = async (filepath: string) => {
+ try {
+ const { linuxRecord } = await import("@guidepup/record");
+
+ return linuxRecord(filepath);
+ } catch {
+ console.warn(
+ "@guidepup/record not available. Recording will be skipped. This is expected on platforms without ffmpeg support (e.g., Windows ARM64).",
+ );
+ }
+};
+
+test.use({ orcaStartOptions: { capture: true } });
+
+test.describe("Firefox Playwright Orca", () => {
+ test("I can navigate the Guidepup documentation site", async ({
+ browser,
+ browserName,
+ page,
+ orca,
+ }) => {
+ const osName = platform();
+ const osVersion = release();
+ const browserVersion = browser.version();
+ const screenReaderName = orca.name;
+ const screenReaderVersion = orca.version;
+ const { retry } = test.info();
+ const recordingFilePath = `./recordings/playwright-orca-${osName}-${osVersion}-${browserName}-${browserVersion}-attempt-${retry}-${+new Date()}.mov`;
+
+ console.table({
+ osName,
+ osVersion,
+ browserName,
+ browserVersion,
+ screenReaderName,
+ screenReaderVersion,
+ retry,
+ });
+
+ let stopRecording: (() => Promise) | undefined;
+
+ try {
+ stopRecording = await record(recordingFilePath);
+
+ await headerNavigation({ page, orca });
+
+ // Assert that we've ended up where we expected and what we were told on
+ // the way there is as expected.
+
+ const spokenPhraseLog = await orca.spokenPhraseLog();
+
+ console.log(JSON.stringify(spokenPhraseLog, undefined, 2));
+
+ logIncludesExpectedPhrases(spokenPhraseLog, spokenPhraseSnapshot);
+ } finally {
+ await stopRecording?.();
+ }
+ });
+});
diff --git a/examples/playwright-orca/tests/firefox/firefox.spokenPhrase.snapshot.json b/examples/playwright-orca/tests/firefox/firefox.spokenPhrase.snapshot.json
new file mode 100644
index 0000000..1f3d87a
--- /dev/null
+++ b/examples/playwright-orca/tests/firefox/firefox.spokenPhrase.snapshot.json
@@ -0,0 +1,11 @@
+[
+ "Guidepup. heading 1",
+ "Reliable Automation For Your Screen Reader A11y Workflows Through JavaScript",
+ "Full Control",
+ "Mirrors Real User Experience",
+ "Framework Agnostic",
+ "Run with Jest, with Playwright, as an. paragraph",
+ "Get Started. link",
+ "GitHub. link",
+ "Get Started. link"
+]
diff --git a/examples/playwright-orca/tests/headerNavigation.ts b/examples/playwright-orca/tests/headerNavigation.ts
new file mode 100644
index 0000000..5b9a715
--- /dev/null
+++ b/examples/playwright-orca/tests/headerNavigation.ts
@@ -0,0 +1,67 @@
+import { expect, type Page } from "@playwright/test";
+import { delay } from "../../../src/delay";
+import { log } from "../../log";
+import type { OrcaPlaywright } from "../../../src";
+
+const MAX_NAVIGATION_LOOP = 10;
+
+export async function headerNavigation({
+ page,
+ orca,
+}: {
+ page: Page;
+ orca: OrcaPlaywright;
+}) {
+ // Navigate to Guidepup Website ๐
+ log("Navigating to URL: https://www.guidepup.dev.");
+ await page.goto("https://www.guidepup.dev", {
+ waitUntil: "load",
+ });
+
+ // Wait for page to be ready and interact ๐
+ const header = page.locator("h1");
+ await header.waitFor();
+ await delay(500);
+
+ // Make sure interacting with the web content
+ await orca.navigateToWebContent();
+ await delay(500);
+
+ let headingCount = 0;
+
+ // Move across the headings using VoiceOver ๐
+ while (
+ !(await orca.lastSpokenPhrase()).includes("Framework Agnostic") &&
+ headingCount <= MAX_NAVIGATION_LOOP
+ ) {
+ headingCount++;
+
+ log(`Performing command: "H" - "Find the next heading"`);
+ await orca.nextHeading();
+ log(`Screen reader output: "${await orca.lastSpokenPhrase()}".`);
+ }
+
+ let tabCount = 0;
+
+ // Move across text and buttons using Orca
+ while (
+ !(await orca.lastSpokenPhrase()).replaceAll(/\s/g, "").includes("GitHub") &&
+ tabCount <= MAX_NAVIGATION_LOOP
+ ) {
+ tabCount++;
+
+ log(`Performing command: "Orca+Ctrl+Right Arrow"`);
+ await orca.next();
+ log(`Screen reader output: "${await orca.lastSpokenPhrase()}".`);
+ }
+
+ log(`Performing command: "Orca+Ctrl+Left Arrow"`);
+ await orca.previous();
+ log(`Screen reader output: "${await orca.lastSpokenPhrase()}".`);
+
+ log(`Performing command: "Orca+Ctrl+Enter"`);
+ await orca.act();
+ log(`Screen reader output: "${await orca.lastSpokenPhrase()}".`);
+
+ await expect(page).toHaveURL("https://www.guidepup.dev/docs/getting-started");
+}
diff --git a/examples/playwright-screenreader/README.md b/examples/playwright-screenreader/README.md
index 3b3065a..8c19cd7 100644
--- a/examples/playwright-screenreader/README.md
+++ b/examples/playwright-screenreader/README.md
@@ -13,6 +13,5 @@ npm run test
## Test flow
1. The test launches the browser using Playwright
-2. Navigates to the GitHub website
-3. Moves through the website using the default screen reader for the environment, controlled by Guidepup
-4. Traverses headings until the Guidepup heading in the README.md is found
+2. Navigates to the Guidepup website
+3. Moves through the website using the default screen reader for the OS, controlled by Guidepup
diff --git a/examples/playwright-screenreader/chromium.config.ts b/examples/playwright-screenreader/chromium.config.ts
index def99d5..bfbb9af 100644
--- a/examples/playwright-screenreader/chromium.config.ts
+++ b/examples/playwright-screenreader/chromium.config.ts
@@ -5,7 +5,7 @@ const config: PlaywrightTestConfig = {
...screenReaderConfig,
reportSlowTests: null,
timeout: 5 * 60 * 1000,
- retries: 1,
+ retries: 5,
projects: [
{
name: "chromium",
diff --git a/examples/playwright-screenreader/firefox.config.ts b/examples/playwright-screenreader/firefox.config.ts
new file mode 100644
index 0000000..40e4349
--- /dev/null
+++ b/examples/playwright-screenreader/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/examples/playwright-screenreader/tests/chromium/chromium-live-region.spec.ts b/examples/playwright-screenreader/tests/chromium/chromium-live-region.spec.ts
index c14ac78..5037710 100644
--- a/examples/playwright-screenreader/tests/chromium/chromium-live-region.spec.ts
+++ b/examples/playwright-screenreader/tests/chromium/chromium-live-region.spec.ts
@@ -4,6 +4,18 @@ import { expect } from "@playwright/test";
import { log } from "../../../log";
import { screenReaderTest as test } from "../../../../src";
+const record = async (filepath: string) => {
+ try {
+ const { record: guidepupRecord } = await import("@guidepup/record");
+
+ return guidepupRecord(filepath);
+ } catch {
+ console.warn(
+ "@guidepup/record not available. Recording will be skipped. This is expected on platforms without ffmpeg support (e.g., Windows ARM64).",
+ );
+ }
+};
+
test.describe("Chromium Playwright Screen Reader", () => {
test("I can capture screen reader output from Playwright commands", async ({
browser,
@@ -17,6 +29,7 @@ test.describe("Chromium Playwright Screen Reader", () => {
const screenReaderName = screenReader.name;
const screenReaderVersion = screenReader.version;
const { retry } = test.info();
+ const recordingFilePath = `./recordings/playwright-screenreader-live-region-${osName}-${osVersion}-${browserName}-${browserVersion}-attempt-${retry}-${+new Date()}.mov`;
console.table({
osName,
@@ -28,55 +41,71 @@ test.describe("Chromium Playwright Screen Reader", () => {
retry,
});
- log("Navigating to live region test page.");
+ let stopRecording: (() => Promise) | undefined;
- await page.goto("about:blank", {
- waitUntil: "load",
- });
+ try {
+ stopRecording = await record(recordingFilePath);
+
+ log("Navigating to live region test page.");
- await page.setContent(`
-
- Example 1
-
-
+ await page.goto("about:blank", {
+ waitUntil: "load",
+ });
-
+ await page.setContent(`
+
+
+
+ Guidepup Live Region Test
+
+
+
+ Example 1
+
+
-
+
+
+
+
+
`);
- const button = page.locator("#trigger");
- await button.waitFor();
- await delay(500);
+ const button = page.locator("#trigger");
+ await button.waitFor();
+ await delay(500);
- await screenReader.navigateToWebContent();
- await delay(500);
+ await screenReader.navigateToWebContent();
+ await delay(500);
- log(`Performing capture: Playwright focus`);
- const { spokenPhrase: focusSpokenPhrase } = await screenReader.capture(() =>
- button.focus(),
- );
- log(`Screen reader output: "${focusSpokenPhrase}".`);
-
- log(`Performing capture: Playwright click`);
- const { spokenPhrase: clickSpokenPhrase } = await screenReader.capture(
- () => button.click(),
- {
- // Capture full output as there is potential for multiple phrases:
- //
- // 1. The button itself
- // 2. And the live region announcement
- //
- // And the default capture of "initial" will cut off the announcement.
- capture: true,
- },
- );
- log(`Screen reader output: "${clickSpokenPhrase}".`);
+ log(`Performing capture: Playwright focus`);
+ const { spokenPhrase: focusSpokenPhrase } = await screenReader.capture(
+ () => button.focus(),
+ );
+ log(`Screen reader output: "${focusSpokenPhrase}".`);
+
+ log(`Performing capture: Playwright click`);
+ const { spokenPhrase: clickSpokenPhrase } = await screenReader.capture(
+ () => button.click(),
+ {
+ // Capture full output as there is potential for multiple phrases:
+ //
+ // 1. The button itself
+ // 2. And the live region announcement
+ //
+ // And the default capture of "initial" will cut off the announcement.
+ capture: true,
+ },
+ );
+ log(`Screen reader output: "${clickSpokenPhrase}".`);
- expect(clickSpokenPhrase).toContain("testing testing 123");
+ expect(clickSpokenPhrase).toContain("testing testing 123");
+ } finally {
+ await stopRecording?.();
+ }
});
});
diff --git a/examples/playwright-screenreader/tests/chromium/chromium.spec.ts b/examples/playwright-screenreader/tests/chromium/chromium.spec.ts
index 529e7df..738cb11 100644
--- a/examples/playwright-screenreader/tests/chromium/chromium.spec.ts
+++ b/examples/playwright-screenreader/tests/chromium/chromium.spec.ts
@@ -4,6 +4,18 @@ import { logIncludesExpectedPhrases } from "../../../logIncludesExpectedPhrases"
import spokenPhraseSnapshot from "./chromium.spokenPhrase.snapshot.json";
import { screenReaderTest as test } from "../../../../src";
+const record = async (filepath: string) => {
+ try {
+ const { record: guidepupRecord } = await import("@guidepup/record");
+
+ return guidepupRecord(filepath);
+ } catch {
+ console.warn(
+ "@guidepup/record not available. Recording will be skipped. This is expected on platforms without ffmpeg support (e.g., Windows ARM64).",
+ );
+ }
+};
+
test.describe("Chromium Playwright Screen Reader", () => {
test("I can navigate the Guidepup documentation site", async ({
browser,
@@ -17,6 +29,7 @@ test.describe("Chromium Playwright Screen Reader", () => {
const screenReaderName = screenReader.name;
const screenReaderVersion = screenReader.version;
const { retry } = test.info();
+ const recordingFilePath = `./recordings/playwright-screenreader-${osName}-${osVersion}-${browserName}-${browserVersion}-attempt-${retry}-${+new Date()}.mov`;
console.table({
osName,
@@ -28,17 +41,25 @@ test.describe("Chromium Playwright Screen Reader", () => {
retry,
});
- await headerNavigation({ page, screenReader });
+ let stopRecording: (() => Promise) | undefined;
+
+ try {
+ stopRecording = await record(recordingFilePath);
+
+ await headerNavigation({ page, screenReader });
- // Assert that we've ended up where we expected and what we were told on
- // the way there is as expected.
+ // Assert that we've ended up where we expected and what we were told on
+ // the way there is as expected.
- const itemTextLog = await screenReader.itemTextLog();
- const spokenPhraseLog = await screenReader.spokenPhraseLog();
+ const itemTextLog = await screenReader.itemTextLog();
+ const spokenPhraseLog = await screenReader.spokenPhraseLog();
- console.log(JSON.stringify(itemTextLog, undefined, 2));
- console.log(JSON.stringify(spokenPhraseLog, undefined, 2));
+ console.log(JSON.stringify(itemTextLog, undefined, 2));
+ console.log(JSON.stringify(spokenPhraseLog, undefined, 2));
- logIncludesExpectedPhrases(spokenPhraseLog, spokenPhraseSnapshot);
+ logIncludesExpectedPhrases(spokenPhraseLog, spokenPhraseSnapshot);
+ } finally {
+ await stopRecording?.();
+ }
});
});
diff --git a/examples/playwright-screenreader/tests/firefox/firefox-live-region.spec.ts b/examples/playwright-screenreader/tests/firefox/firefox-live-region.spec.ts
new file mode 100644
index 0000000..da59d61
--- /dev/null
+++ b/examples/playwright-screenreader/tests/firefox/firefox-live-region.spec.ts
@@ -0,0 +1,331 @@
+import { platform, release } from "os";
+import { delay } from "../../../../src/delay";
+import { expect } from "@playwright/test";
+import { log } from "../../../log";
+import { screenReaderTest as test } from "../../../../src";
+
+const MAX_NAVIGATION_LOOP = 10;
+
+const record = async (filepath: string) => {
+ try {
+ const { record: guidepupRecord } = await import("@guidepup/record");
+
+ return guidepupRecord(filepath);
+ } catch {
+ console.warn(
+ "@guidepup/record not available. Recording will be skipped. This is expected on platforms without ffmpeg support (e.g., Windows ARM64).",
+ );
+ }
+};
+
+test.describe("Firefox Playwright Screen Reader: Orca", () => {
+ test("I can capture live region screen reader output from Orca commands (Object Navigator Click)", async ({
+ browser,
+ browserName,
+ page,
+ screenReader,
+ }) => {
+ const osName = platform();
+ const osVersion = release();
+ const browserVersion = browser.version();
+ const screenReaderName = screenReader.name;
+ const screenReaderVersion = screenReader.version;
+ const { retry } = test.info();
+ const recordingFilePath = `./recordings/playwright-screenreader-live-region-orca-commands-${osName}-${osVersion}-${browserName}-${browserVersion}-attempt-${retry}-${+new Date()}.mov`;
+
+ console.table({
+ osName,
+ osVersion,
+ browserName,
+ browserVersion,
+ screenReaderName,
+ screenReaderVersion,
+ retry,
+ });
+
+ let stopRecording: (() => Promise) | undefined;
+
+ try {
+ stopRecording = await record(recordingFilePath);
+
+ log("Navigating to live region test page.");
+
+ await page.goto("about:blank", {
+ waitUntil: "load",
+ });
+
+ // NOTE: Orca does not support `role="alert"` etc.correctly.
+ // Authors are required to implement `aria-live` explicitly.
+ await page.setContent(`
+
+
+
+ Guidepup Live Region Test
+
+
+
+ Example 1
+
+
+
+
+
+
+
+
+ `);
+
+ await page.locator("#trigger").waitFor();
+ await delay(500);
+
+ await screenReader.navigateToWebContent();
+ await delay(500);
+
+ let navigationCount = 0;
+
+ while (
+ !(await screenReader.itemText())
+ .replaceAll(/\s/g, "")
+ .includes("Update") &&
+ navigationCount <= MAX_NAVIGATION_LOOP
+ ) {
+ navigationCount++;
+
+ log(`Performing command: "Orca+Ctrl+Right Arrow"`);
+ await screenReader.next();
+ log(
+ `Screen reader output: "${await screenReader.lastSpokenPhrase()}".`,
+ );
+ }
+
+ // TODO: potential bug in Orca - using object navigator to place Orca on
+ // a button and then `Orca+Ctrl+Enter` to activate it does not announce a
+ // live region _unless_ focus is first place on the button.
+ // REF: https://github.com/guidepup/guidepup/pull/143#issuecomment-6106014653
+ await page.locator("#trigger").focus();
+ await delay(500);
+
+ log(`Performing command: "Orca+Ctrl+Enter"`);
+ await screenReader.act();
+ const clickSpokenPhrase = await screenReader.lastSpokenPhrase();
+ log(`Screen reader output: "${clickSpokenPhrase}".`);
+
+ expect(clickSpokenPhrase).toContain("testing testing 123");
+ } finally {
+ await stopRecording?.();
+ }
+ });
+
+ test("I can capture live region screen reader output from Orca commands (Playwright Click)", async ({
+ browser,
+ browserName,
+ page,
+ screenReader,
+ }) => {
+ const osName = platform();
+ const osVersion = release();
+ const browserVersion = browser.version();
+ const screenReaderName = screenReader.name;
+ const screenReaderVersion = screenReader.version;
+ const { retry } = test.info();
+ const recordingFilePath = `./recordings/playwright-screenreader-live-region-orca-commands-${osName}-${osVersion}-${browserName}-${browserVersion}-attempt-${retry}-${+new Date()}.mov`;
+
+ console.table({
+ osName,
+ osVersion,
+ browserName,
+ browserVersion,
+ screenReaderName,
+ screenReaderVersion,
+ retry,
+ });
+
+ let stopRecording: (() => Promise) | undefined;
+
+ try {
+ stopRecording = await record(recordingFilePath);
+
+ log("Navigating to live region test page.");
+
+ await page.goto("about:blank", {
+ waitUntil: "load",
+ });
+
+ // NOTE: Orca does not support `role="alert"` etc.correctly.
+ // Authors are required to implement `aria-live` explicitly.
+ await page.setContent(`
+
+
+
+ Guidepup Live Region Test
+
+
+
+ Example 1
+
+
+
+
+
+
+
+
+ `);
+
+ const button = page.locator("#trigger");
+ await button.waitFor();
+ await delay(500);
+
+ await screenReader.navigateToWebContent();
+ await delay(500);
+
+ let navigationCount = 0;
+
+ while (
+ !(await screenReader.itemText())
+ .replaceAll(/\s/g, "")
+ .includes("Update") &&
+ navigationCount <= MAX_NAVIGATION_LOOP
+ ) {
+ navigationCount++;
+
+ log(`Performing command: "Orca+Ctrl+Right Arrow"`);
+ await screenReader.next();
+ log(
+ `Screen reader output: "${await screenReader.lastSpokenPhrase()}".`,
+ );
+ }
+
+ // TODO: potential bug in Orca - using object navigator to place Orca on
+ // a button and then `Orca+Ctrl+Enter` to activate it does not announce a
+ // live region _unless_ focus is first place on the button.
+ // REF: https://github.com/guidepup/guidepup/pull/143#issuecomment-6106014653
+ await page.locator("#trigger").focus();
+ await delay(500);
+
+ log(`Performing capture: Playwright click`);
+ const { spokenPhrase: clickSpokenPhrase } = await screenReader.capture(
+ () => button.click(),
+ {
+ // Capture full output as there is potential for multiple phrases:
+ //
+ // 1. The button itself
+ // 2. And the live region announcement
+ //
+ // And the default capture of "initial" will cut off the announcement.
+ capture: true,
+ },
+ );
+ log(`Screen reader output: "${clickSpokenPhrase}".`);
+
+ expect(clickSpokenPhrase).toContain("testing testing 123");
+ } finally {
+ await stopRecording?.();
+ }
+ });
+
+ test("I can capture live region screen reader output from Playwright commands", async ({
+ browser,
+ browserName,
+ page,
+ screenReader,
+ }) => {
+ const osName = platform();
+ const osVersion = release();
+ const browserVersion = browser.version();
+ const screenReaderName = screenReader.name;
+ const screenReaderVersion = screenReader.version;
+ const { retry } = test.info();
+ const recordingFilePath = `./recordings/playwright-screenreader-live-region-capture-${osName}-${osVersion}-${browserName}-${browserVersion}-attempt-${retry}-${+new Date()}.mov`;
+
+ console.table({
+ osName,
+ osVersion,
+ browserName,
+ browserVersion,
+ screenReaderName,
+ screenReaderVersion,
+ retry,
+ });
+
+ let stopRecording: (() => Promise) | undefined;
+
+ try {
+ stopRecording = await record(recordingFilePath);
+
+ log("Navigating to live region test page.");
+
+ await page.goto("about:blank", {
+ waitUntil: "load",
+ });
+
+ // NOTE: Orca does not support `role="alert"` etc.correctly.
+ // Authors are required to implement `aria-live` explicitly.
+ await page.setContent(`
+
+
+
+ Guidepup Live Region Test
+
+
+
+ Example 1
+
+
+
+
+
+
+
+
+ `);
+
+ const button = page.locator("#trigger");
+ await button.waitFor();
+ await delay(500);
+
+ await screenReader.navigateToWebContent();
+ await delay(500);
+
+ log(`Performing capture: Playwright focus`);
+ const { spokenPhrase: focusSpokenPhrase } = await screenReader.capture(
+ () => button.focus(),
+ );
+ log(`Screen reader output: "${focusSpokenPhrase}".`);
+
+ await delay(500);
+
+ log(`Performing capture: Playwright click`);
+ const { spokenPhrase: clickSpokenPhrase } = await screenReader.capture(
+ () => button.click(),
+ {
+ // Capture full output as there is potential for multiple phrases:
+ //
+ // 1. The button itself
+ // 2. And the live region announcement
+ //
+ // And the default capture of "initial" will cut off the announcement.
+ capture: true,
+ },
+ );
+ log(`Screen reader output: "${clickSpokenPhrase}".`);
+
+ // TODO: support capture from Playwright interactions.
+ expect(clickSpokenPhrase).toContain("testing testing 123");
+ } finally {
+ await stopRecording?.();
+ }
+ });
+});
diff --git a/examples/playwright-screenreader/tests/firefox/firefox-orca.spec.ts b/examples/playwright-screenreader/tests/firefox/firefox-orca.spec.ts
new file mode 100644
index 0000000..03181da
--- /dev/null
+++ b/examples/playwright-screenreader/tests/firefox/firefox-orca.spec.ts
@@ -0,0 +1,105 @@
+import { platform, release } from "os";
+import { delay } from "../../../../src/delay";
+import { log } from "../../../log";
+import { logIncludesExpectedPhrases } from "../../../logIncludesExpectedPhrases";
+import spokenPhraseSnapshot from "./firefox.spokenPhrase.snapshot.json";
+import { screenReaderTest as test } from "../../../../src";
+
+const MAX_NAVIGATION_LOOP = 10;
+
+const record = async (filepath: string) => {
+ try {
+ const { record: guidepupRecord } = await import("@guidepup/record");
+
+ return guidepupRecord(filepath);
+ } catch {
+ console.warn(
+ "@guidepup/record not available. Recording will be skipped. This is expected on platforms without ffmpeg support (e.g., Windows ARM64).",
+ );
+ }
+};
+
+test.describe("Firefox Playwright Screen Reader", () => {
+ test("I can navigate the Guidepup documentation site", async ({
+ browser,
+ browserName,
+ page,
+ screenReader,
+ }) => {
+ const osName = platform();
+ const osVersion = release();
+ const browserVersion = browser.version();
+ const screenReaderName = screenReader.name;
+ const screenReaderVersion = screenReader.version;
+ const { retry } = test.info();
+ const recordingFilePath = `./recordings/playwright-screenreader-${osName}-${osVersion}-${browserName}-${browserVersion}-attempt-${retry}-${+new Date()}.mov`;
+
+ console.table({
+ osName,
+ osVersion,
+ browserName,
+ browserVersion,
+ screenReaderName,
+ screenReaderVersion,
+ retry,
+ });
+
+ let stopRecording: (() => Promise) | undefined;
+
+ try {
+ stopRecording = await record(recordingFilePath);
+
+ log("Navigating to URL: https://www.guidepup.dev.");
+ await page.goto("https://www.guidepup.dev", {
+ waitUntil: "load",
+ });
+
+ const header = page.locator("h1");
+ await header.waitFor();
+ await delay(500);
+
+ await screenReader.navigateToWebContent();
+ await delay(500);
+
+ // Navigate out of the skip to main content and into the navigation bar
+ await screenReader.stopInteracting();
+ await screenReader.next();
+ await screenReader.interact();
+
+ let navigationCount = 0;
+
+ // Move across the header content
+ while (
+ !(await screenReader.itemText())
+ .replaceAll(/\s/g, "")
+ .includes("GitHub") &&
+ navigationCount <= MAX_NAVIGATION_LOOP
+ ) {
+ navigationCount++;
+
+ log(`Performing command: next`);
+ await screenReader.next();
+ log(
+ `Screen reader output: "${await screenReader.lastSpokenPhrase()}".`,
+ );
+ }
+
+ log(`Performing command: act`);
+ await screenReader.act();
+ log(`Screen reader output: "${await screenReader.lastSpokenPhrase()}".`);
+
+ // Assert that we've ended up where we expected and what we were told on
+ // the way there is as expected.
+
+ const itemTextLog = await screenReader.itemTextLog();
+ const spokenPhraseLog = await screenReader.spokenPhraseLog();
+
+ console.log(JSON.stringify(itemTextLog, undefined, 2));
+ console.log(JSON.stringify(spokenPhraseLog, undefined, 2));
+
+ logIncludesExpectedPhrases(spokenPhraseLog, spokenPhraseSnapshot);
+ } finally {
+ await stopRecording?.();
+ }
+ });
+});
diff --git a/examples/playwright-screenreader/tests/firefox/firefox.spokenPhrase.snapshot.json b/examples/playwright-screenreader/tests/firefox/firefox.spokenPhrase.snapshot.json
new file mode 100644
index 0000000..68bd46a
--- /dev/null
+++ b/examples/playwright-screenreader/tests/firefox/firefox.spokenPhrase.snapshot.json
@@ -0,0 +1 @@
+["Guidepup", "Docs", "API", "GitHub"]
diff --git a/examples/playwright-voiceover/README.md b/examples/playwright-voiceover/README.md
index 35977a1..602ec29 100644
--- a/examples/playwright-voiceover/README.md
+++ b/examples/playwright-voiceover/README.md
@@ -13,6 +13,5 @@ npm run test
## Test flow
1. The test launches the browser using Playwright
-2. Navigates to the GitHub website
+2. Navigates to the Guidepup website
3. Moves through the website using VoiceOver controlled by Guidepup
-4. Traverses headings until the Guidepup heading in the README.md is found
diff --git a/examples/playwright-voiceover/chromium.config.ts b/examples/playwright-voiceover/chromium.config.ts
index def99d5..bfbb9af 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: 1,
+ retries: 5,
projects: [
{
name: "chromium",
diff --git a/examples/playwright-voiceover/firefox.config.ts b/examples/playwright-voiceover/firefox.config.ts
index 92a6e17..40e4349 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: 1,
+ retries: 5,
projects: [
{
name: "firefox",
diff --git a/examples/playwright-voiceover/tests/chromium/chromium.itemText.snapshot.json b/examples/playwright-voiceover/tests/chromium/chromium.itemText.snapshot.json
index 238b26c..166b0bc 100644
--- a/examples/playwright-voiceover/tests/chromium/chromium.itemText.snapshot.json
+++ b/examples/playwright-voiceover/tests/chromium/chromium.itemText.snapshot.json
@@ -3,5 +3,8 @@
"Screen Reader A11y Workflows",
"Full Control heading level 3",
"Mirrors Real User Experience heading level 3",
- "Framework Agnostic heading level 3"
+ "Framework Agnostic heading level 3",
+ "Run with Jest, with Playwright, as an independent script, no vendor lock-in.",
+ "Get Started link",
+ "GitHub link"
]
diff --git a/examples/playwright-voiceover/tests/chromium/chromium.spec.ts b/examples/playwright-voiceover/tests/chromium/chromium.spec.ts
index 5bc05e9..0cc6e62 100644
--- a/examples/playwright-voiceover/tests/chromium/chromium.spec.ts
+++ b/examples/playwright-voiceover/tests/chromium/chromium.spec.ts
@@ -2,11 +2,20 @@ import { platform, release } from "os";
import { headerNavigation } from "../headerNavigation";
import itemTextSnapshot from "./chromium.itemText.snapshot.json";
import { logIncludesExpectedPhrases } from "../../../logIncludesExpectedPhrases";
-import { macOSRecord } from "@guidepup/record";
import spokenPhraseSnapshot from "./chromium.spokenPhrase.snapshot.json";
import { voiceOverTest as test } from "../../../../src";
-test.use({ voiceOverStartOptions: { capture: "initial" } });
+const record = async (filepath: string) => {
+ try {
+ const { macOSRecord } = await import("@guidepup/record");
+
+ return macOSRecord(filepath);
+ } catch {
+ console.warn(
+ "@guidepup/record not available. Recording will be skipped. This is expected on platforms without ffmpeg support (e.g., Windows ARM64).",
+ );
+ }
+};
test.describe("Chromium Playwright VoiceOver", () => {
test("I can navigate the Guidepup documentation site", async ({
@@ -33,10 +42,10 @@ test.describe("Chromium Playwright VoiceOver", () => {
retry,
});
- let stopRecording: (() => void) | undefined;
+ let stopRecording: (() => Promise) | undefined;
try {
- stopRecording = macOSRecord(recordingFilePath);
+ stopRecording = await record(recordingFilePath);
await headerNavigation({ page, voiceOver });
@@ -52,7 +61,7 @@ test.describe("Chromium Playwright VoiceOver", () => {
logIncludesExpectedPhrases(itemTextLog, itemTextSnapshot);
logIncludesExpectedPhrases(spokenPhraseLog, spokenPhraseSnapshot);
} finally {
- stopRecording?.();
+ await stopRecording?.();
}
});
});
diff --git a/examples/playwright-voiceover/tests/chromium/chromium.spokenPhrase.snapshot.json b/examples/playwright-voiceover/tests/chromium/chromium.spokenPhrase.snapshot.json
index 238b26c..329dd7c 100644
--- a/examples/playwright-voiceover/tests/chromium/chromium.spokenPhrase.snapshot.json
+++ b/examples/playwright-voiceover/tests/chromium/chromium.spokenPhrase.snapshot.json
@@ -1,7 +1,10 @@
[
- "Guidepup heading level 1",
+ "heading level 1 Guidepup",
"Screen Reader A11y Workflows",
- "Full Control heading level 3",
- "Mirrors Real User Experience heading level 3",
- "Framework Agnostic heading level 3"
+ "heading level 3 Full Control",
+ "heading level 3 Mirrors Real User Experience",
+ "heading level 3 Framework Agnostic",
+ "Run with Jest, with Playwright, as an independent script, no vendor lock-in.",
+ "link Get Started",
+ "link GitHub"
]
diff --git a/examples/playwright-voiceover/tests/firefox/firefox.itemText.snapshot.json b/examples/playwright-voiceover/tests/firefox/firefox.itemText.snapshot.json
index 238b26c..166b0bc 100644
--- a/examples/playwright-voiceover/tests/firefox/firefox.itemText.snapshot.json
+++ b/examples/playwright-voiceover/tests/firefox/firefox.itemText.snapshot.json
@@ -3,5 +3,8 @@
"Screen Reader A11y Workflows",
"Full Control heading level 3",
"Mirrors Real User Experience heading level 3",
- "Framework Agnostic heading level 3"
+ "Framework Agnostic heading level 3",
+ "Run with Jest, with Playwright, as an independent script, no vendor lock-in.",
+ "Get Started link",
+ "GitHub link"
]
diff --git a/examples/playwright-voiceover/tests/firefox/firefox.spec.ts b/examples/playwright-voiceover/tests/firefox/firefox.spec.ts
index c0aab28..f62e8d3 100644
--- a/examples/playwright-voiceover/tests/firefox/firefox.spec.ts
+++ b/examples/playwright-voiceover/tests/firefox/firefox.spec.ts
@@ -2,11 +2,30 @@ import { platform, release } from "os";
import { headerNavigation } from "../headerNavigation";
import itemTextSnapshot from "./firefox.itemText.snapshot.json";
import { logIncludesExpectedPhrases } from "../../../logIncludesExpectedPhrases";
-import { macOSRecord } from "@guidepup/record";
import spokenPhraseSnapshot from "./firefox.spokenPhrase.snapshot.json";
import { voiceOverTest as test } from "../../../../src";
-test.use({ voiceOverStartOptions: { capture: "initial" } });
+const record = async (filepath: string) => {
+ try {
+ const { macOSRecord } = await import("@guidepup/record");
+
+ return macOSRecord(filepath);
+ } catch {
+ console.warn(
+ "@guidepup/record not available. Recording will be skipped. This is expected on platforms without ffmpeg support (e.g., Windows ARM64).",
+ );
+ }
+};
+
+test.use({
+ voiceOverStartOptions: {
+ capture: "initial",
+ settings: {
+ SCRCategories_SCRCategorySystemWide_SCRSpeechLanguages_default_SCRSpeechComponentSettings_SCRVoiceIdentifier:
+ "com.apple.speech.synthesis.voice.custom.siri.martha.compact",
+ },
+ },
+});
test.describe("Firefox Playwright VoiceOver", () => {
test("I can navigate the Guidepup documentation site", async ({
@@ -33,10 +52,10 @@ test.describe("Firefox Playwright VoiceOver", () => {
retry,
});
- let stopRecording: (() => void) | undefined;
+ let stopRecording: (() => Promise) | undefined;
try {
- stopRecording = macOSRecord(recordingFilePath);
+ stopRecording = await record(recordingFilePath);
await headerNavigation({ page, voiceOver });
@@ -52,7 +71,7 @@ test.describe("Firefox Playwright VoiceOver", () => {
logIncludesExpectedPhrases(itemTextLog, itemTextSnapshot);
logIncludesExpectedPhrases(spokenPhraseLog, spokenPhraseSnapshot);
} finally {
- stopRecording?.();
+ await stopRecording?.();
}
});
});
diff --git a/examples/playwright-voiceover/tests/firefox/firefox.spokenPhrase.snapshot.json b/examples/playwright-voiceover/tests/firefox/firefox.spokenPhrase.snapshot.json
index 238b26c..329dd7c 100644
--- a/examples/playwright-voiceover/tests/firefox/firefox.spokenPhrase.snapshot.json
+++ b/examples/playwright-voiceover/tests/firefox/firefox.spokenPhrase.snapshot.json
@@ -1,7 +1,10 @@
[
- "Guidepup heading level 1",
+ "heading level 1 Guidepup",
"Screen Reader A11y Workflows",
- "Full Control heading level 3",
- "Mirrors Real User Experience heading level 3",
- "Framework Agnostic heading level 3"
+ "heading level 3 Full Control",
+ "heading level 3 Mirrors Real User Experience",
+ "heading level 3 Framework Agnostic",
+ "Run with Jest, with Playwright, as an independent script, no vendor lock-in.",
+ "link Get Started",
+ "link GitHub"
]
diff --git a/examples/playwright-voiceover/tests/headerNavigation.ts b/examples/playwright-voiceover/tests/headerNavigation.ts
index 09531e3..2b25c19 100644
--- a/examples/playwright-voiceover/tests/headerNavigation.ts
+++ b/examples/playwright-voiceover/tests/headerNavigation.ts
@@ -1,7 +1,7 @@
-import { Page } from "@playwright/test";
+import { expect, type Page } from "@playwright/test";
+import { delay } from "../../../src/delay";
import { log } from "../../log";
import type { VoiceOverPlaywright } from "../../../src";
-import { delay } from "../../../src/delay";
const MAX_NAVIGATION_LOOP = 10;
@@ -49,4 +49,41 @@ export async function headerNavigation({
await voiceOver.perform(voiceOver.keyboardCommands.describeItem);
log(`Screen reader output: "${await voiceOver.lastSpokenPhrase()}".`);
}
+
+ let tabCount = 0;
+
+ // Move across text and buttons using VoiceOver
+ while (
+ !(await voiceOver.itemText()).includes("GitHub") &&
+ tabCount <= MAX_NAVIGATION_LOOP
+ ) {
+ tabCount++;
+
+ log(`Performing command: "VO+Right Arrow"`);
+ await voiceOver.next();
+ log(`Screen reader output: "${await voiceOver.lastSpokenPhrase()}".`);
+
+ 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()}".`);
+ }
+
+ log(`Performing command: "VO+Left Arrow"`);
+ await voiceOver.previous();
+ log(`Screen reader output: "${await voiceOver.lastSpokenPhrase()}".`);
+
+ 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()}".`);
+
+ // Navigate to the Guidepup Getting Started docs
+ log(`Performing command: "VO+Space bar"`);
+ await voiceOver.act();
+ log(`Screen reader output: "${await voiceOver.lastSpokenPhrase()}".`);
+
+ await expect(page).toHaveURL("https://www.guidepup.dev/docs/getting-started");
}
diff --git a/examples/playwright-voiceover/tests/webkit/webkit.itemText.docs.snapshot.json b/examples/playwright-voiceover/tests/webkit/webkit.itemText.docs.snapshot.json
new file mode 100644
index 0000000..166b0bc
--- /dev/null
+++ b/examples/playwright-voiceover/tests/webkit/webkit.itemText.docs.snapshot.json
@@ -0,0 +1,10 @@
+[
+ "Guidepup heading level 1",
+ "Screen Reader A11y Workflows",
+ "Full Control heading level 3",
+ "Mirrors Real User Experience heading level 3",
+ "Framework Agnostic heading level 3",
+ "Run with Jest, with Playwright, as an independent script, no vendor lock-in.",
+ "Get Started link",
+ "GitHub link"
+]
diff --git a/examples/playwright-voiceover/tests/webkit/webkit.itemText.snapshot.json b/examples/playwright-voiceover/tests/webkit/webkit.itemText.snapshot.json
deleted file mode 100644
index 238b26c..0000000
--- a/examples/playwright-voiceover/tests/webkit/webkit.itemText.snapshot.json
+++ /dev/null
@@ -1,7 +0,0 @@
-[
- "Guidepup heading level 1",
- "Screen Reader A11y Workflows",
- "Full Control heading level 3",
- "Mirrors Real User Experience heading level 3",
- "Framework Agnostic heading level 3"
-]
diff --git a/examples/playwright-voiceover/tests/webkit/webkit.spec.ts b/examples/playwright-voiceover/tests/webkit/webkit.spec.ts
index 3d96364..ea44aaa 100644
--- a/examples/playwright-voiceover/tests/webkit/webkit.spec.ts
+++ b/examples/playwright-voiceover/tests/webkit/webkit.spec.ts
@@ -1,12 +1,24 @@
import { platform, release } from "os";
import { headerNavigation } from "../headerNavigation";
-import itemTextSnapshot from "./webkit.itemText.snapshot.json";
+import itemTextSnapshotDocs from "./webkit.itemText.docs.snapshot.json";
import { logIncludesExpectedPhrases } from "../../../logIncludesExpectedPhrases";
-import { macOSRecord } from "@guidepup/record";
-import spokenPhraseSnapshot from "./webkit.spokenPhrase.snapshot.json";
+import spokenPhraseSnapshotDocs from "./webkit.spokenPhrase.docs.snapshot.json";
+import spokenPhraseSnapshotTextarea from "./webkit.spokenPhrase.textarea.snapshot.json";
import { voiceOverTest as test } from "../../../../src";
+import { log } from "../../../log";
+import { expect } from "@playwright/test";
-test.use({ voiceOverStartOptions: { capture: "initial" } });
+const record = async (filepath: string) => {
+ try {
+ const { macOSRecord } = await import("@guidepup/record");
+
+ return macOSRecord(filepath);
+ } catch {
+ console.warn(
+ "@guidepup/record not available. Recording will be skipped. This is expected on platforms without ffmpeg support (e.g., Windows ARM64).",
+ );
+ }
+};
test.describe("Webkit Playwright VoiceOver", () => {
test("I can navigate the Guidepup documentation site", async ({
@@ -33,10 +45,10 @@ test.describe("Webkit Playwright VoiceOver", () => {
retry,
});
- let stopRecording: (() => void) | undefined;
+ let stopRecording: (() => Promise) | undefined;
try {
- stopRecording = macOSRecord(recordingFilePath);
+ stopRecording = await record(recordingFilePath);
await headerNavigation({ page, voiceOver });
@@ -49,10 +61,85 @@ test.describe("Webkit Playwright VoiceOver", () => {
console.log(JSON.stringify(itemTextLog, undefined, 2));
console.log(JSON.stringify(spokenPhraseLog, undefined, 2));
- logIncludesExpectedPhrases(itemTextLog, itemTextSnapshot);
- logIncludesExpectedPhrases(spokenPhraseLog, spokenPhraseSnapshot);
+ logIncludesExpectedPhrases(itemTextLog, itemTextSnapshotDocs);
+ logIncludesExpectedPhrases(spokenPhraseLog, spokenPhraseSnapshotDocs);
+ } finally {
+ await stopRecording?.();
+ }
+ });
+
+ test("I can type text into text areas and it is announced (https://github.com/guidepup/guidepup/issues/115)", async ({
+ browser,
+ browserName,
+ page,
+ voiceOver,
+ }) => {
+ const osName = platform();
+ const osVersion = release();
+ const browserVersion = browser.version();
+ const screenReaderName = voiceOver.name;
+ const screenReaderVersion = voiceOver.version;
+ const { retry } = test.info();
+ const recordingFilePath = `./recordings/playwright-voiceover-textarea-${osName}-${osVersion}-${browserName}-${browserVersion}-attempt-${retry}-${+new Date()}.mov`;
+
+ console.table({
+ osName,
+ osVersion,
+ browserName,
+ browserVersion,
+ screenReaderName,
+ screenReaderVersion,
+ retry,
+ });
+
+ let stopRecording: (() => Promise) | undefined;
+
+ try {
+ stopRecording = await record(recordingFilePath);
+
+ log("Navigating to textarea test page.");
+ await page.goto("about:blank", {
+ waitUntil: "load",
+ });
+
+ await page.setContent(`
+
+
+ `);
+
+ const input = page.locator("#test");
+ await input.waitFor();
+ await voiceOver.navigateToWebContent();
+
+ log(`Performing command: "VO+Right Arrow"`);
+ await voiceOver.next();
+ log(`Screen reader output: "${await voiceOver.lastSpokenPhrase()}".`);
+
+ log(`Typing: "Abc"`);
+ await voiceOver.type("Abc");
+ log(`Screen reader output: "${await voiceOver.lastSpokenPhrase()}".`);
+
+ log(`Pressing: "Cmd+A"`);
+ await voiceOver.press("Cmd+A");
+ log(`Screen reader output: "${await voiceOver.lastSpokenPhrase()}".`);
+
+ log(`Pressing: "Delete"`);
+ await voiceOver.press("Delete");
+ log(`Screen reader output: "${await voiceOver.lastSpokenPhrase()}".`);
+
+ log(`Typing: "123"`);
+ await voiceOver.type("123");
+ log(`Screen reader output: "${await voiceOver.lastSpokenPhrase()}".`);
+
+ await expect(input).toHaveValue("123");
+
+ const spokenPhraseLog = await voiceOver.spokenPhraseLog();
+
+ console.log(JSON.stringify(spokenPhraseLog, undefined, 2));
+
+ logIncludesExpectedPhrases(spokenPhraseLog, spokenPhraseSnapshotTextarea);
} finally {
- stopRecording?.();
+ await stopRecording?.();
}
});
});
diff --git a/examples/playwright-voiceover/tests/webkit/webkit.spokenPhrase.docs.snapshot.json b/examples/playwright-voiceover/tests/webkit/webkit.spokenPhrase.docs.snapshot.json
new file mode 100644
index 0000000..329dd7c
--- /dev/null
+++ b/examples/playwright-voiceover/tests/webkit/webkit.spokenPhrase.docs.snapshot.json
@@ -0,0 +1,10 @@
+[
+ "heading level 1 Guidepup",
+ "Screen Reader A11y Workflows",
+ "heading level 3 Full Control",
+ "heading level 3 Mirrors Real User Experience",
+ "heading level 3 Framework Agnostic",
+ "Run with Jest, with Playwright, as an independent script, no vendor lock-in.",
+ "link Get Started",
+ "link GitHub"
+]
diff --git a/examples/playwright-voiceover/tests/webkit/webkit.spokenPhrase.snapshot.json b/examples/playwright-voiceover/tests/webkit/webkit.spokenPhrase.snapshot.json
deleted file mode 100644
index 238b26c..0000000
--- a/examples/playwright-voiceover/tests/webkit/webkit.spokenPhrase.snapshot.json
+++ /dev/null
@@ -1,7 +0,0 @@
-[
- "Guidepup heading level 1",
- "Screen Reader A11y Workflows",
- "Full Control heading level 3",
- "Mirrors Real User Experience heading level 3",
- "Framework Agnostic heading level 3"
-]
diff --git a/examples/playwright-voiceover/tests/webkit/webkit.spokenPhrase.textarea.snapshot.json b/examples/playwright-voiceover/tests/webkit/webkit.spokenPhrase.textarea.snapshot.json
new file mode 100644
index 0000000..d68b4f4
--- /dev/null
+++ b/examples/playwright-voiceover/tests/webkit/webkit.spokenPhrase.textarea.snapshot.json
@@ -0,0 +1 @@
+["A", "b", "c", "Abc selected", "1", "2", "3"]
diff --git a/examples/playwright-voiceover/webkit.config.ts b/examples/playwright-voiceover/webkit.config.ts
index b4a8662..9f4d81f 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: 1,
+ retries: 5,
projects: [
{
name: "webkit",
diff --git a/package.json b/package.json
index e70d676..67b8ae4 100644
--- a/package.json
+++ b/package.json
@@ -34,19 +34,22 @@
"prepublish": "yarn build",
"pretest": "npx playwright install chromium firefox webkit",
"test:nvda": "yarn test:nvda:chromium && yarn test:nvda:firefox",
- "test:screenreader": "yarn test:screenreader:chromium",
+ "test:orca": "yarn test:orca:firefox",
+ "test:screenreader": "yarn test:screenreader:chromium && yarn test:screenreader: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:orca:firefox": "playwright test --config ./examples/playwright-orca/firefox.config.ts ./examples/playwright-orca/tests/firefox/",
"test:screenreader:chromium": "playwright test --config ./examples/playwright-screenreader/chromium.config.ts ./examples/playwright-screenreader/tests/chromium/",
+ "test:screenreader:firefox": "playwright test --config ./examples/playwright-screenreader/firefox.config.ts ./examples/playwright-screenreader/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.33.0",
- "@guidepup/record": "^0.1.0",
- "@playwright/test": "^1.60.0",
+ "@guidepup/guidepup": "^0.36.1",
+ "@guidepup/record": "^0.3.0",
+ "@playwright/test": "^1.62.1",
"@types/node": "^24.10.1",
"@typescript-eslint/eslint-plugin": "^8.48.1",
"@typescript-eslint/parser": "^8.48.1",
@@ -58,8 +61,8 @@
"typescript": "^5.9.3"
},
"peerDependencies": {
- "@guidepup/guidepup": ">=0.33.0",
- "@playwright/test": "^1.57.0"
+ "@guidepup/guidepup": ">=0.36.1",
+ "@playwright/test": ">=1.62.1"
},
"resolutions": {
"strip-ansi": "6.0.1",
diff --git a/src/applicationIdMap.ts b/src/applicationIdMap.ts
new file mode 100644
index 0000000..3dff95a
--- /dev/null
+++ b/src/applicationIdMap.ts
@@ -0,0 +1,10 @@
+export const applicationIdMap = {
+ chromium: "com.google.chrome.for.testing",
+ chrome: "com.google.Chrome",
+ "chrome-beta": "com.google.Chrome.beta",
+ msedge: "com.microsoft.edgemac",
+ "msedge-beta": "com.microsoft.edgemac.Beta",
+ "msedge-dev": "com.microsoft.edgemac.Dev",
+ firefox: "org.mozilla.nightlyunofficial",
+ webkit: "org.webkit.Playwright",
+};
diff --git a/src/cleanString.ts b/src/cleanString.ts
new file mode 100644
index 0000000..db20afd
--- /dev/null
+++ b/src/cleanString.ts
@@ -0,0 +1,6 @@
+export const cleanString = (str: string): string =>
+ str
+ .toLowerCase()
+ .replace(/[|ยฆ:;'"`\-โโโยท_()[\]{}\\^~]/g, " ")
+ .replace(/\s+/g, " ")
+ .trim();
diff --git a/src/index.ts b/src/index.ts
index 54e8f4b..a6d105f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,5 +1,7 @@
export { nvdaTest } from "./nvdaTest";
export type { NVDAPlaywright } from "./nvdaTest";
+export { orcaTest } from "./orcaTest";
+export type { OrcaPlaywright } from "./orcaTest";
export { screenReaderConfig } from "./screenReaderConfig";
export { screenReaderTest } from "./screenReaderTest";
export type { ScreenReaderPlaywright } from "./screenReaderTest";
diff --git a/src/navigateNVDAtoWebContent.ts b/src/navigateNVDAtoWebContent.ts
new file mode 100644
index 0000000..3e4769a
--- /dev/null
+++ b/src/navigateNVDAtoWebContent.ts
@@ -0,0 +1,142 @@
+import {
+ type CommandOptions,
+ type NVDA,
+ NVDAKeyCodeCommands,
+ WindowsKeyCodes,
+ WindowsModifiers,
+} from "@guidepup/guidepup";
+import type { Page } from "@playwright/test";
+import { cleanString } from "./cleanString";
+import { delay } from "./delay";
+
+const MAX_APPLICATION_SWITCH_RETRY_COUNT = 10;
+
+const SWITCH_APPLICATION = {
+ keyCode: [WindowsKeyCodes.Escape],
+ modifiers: [WindowsModifiers.Alt],
+};
+
+const MOVE_TO_TOP = {
+ keyCode: [WindowsKeyCodes.Home],
+ modifiers: [WindowsModifiers.Control],
+};
+
+const hasBrowserFocus = ({
+ applicationName,
+ pageTitle,
+ windowTitle,
+}: {
+ applicationName: string;
+ pageTitle: string;
+ windowTitle: string;
+}): boolean => {
+ const cleanedApplicationName = cleanString(applicationName);
+ const cleanedPageTitle = cleanString(pageTitle);
+ const cleanedWindowTitle = cleanString(windowTitle);
+
+ return (
+ (cleanedPageTitle.length > 0 &&
+ cleanedWindowTitle.startsWith(cleanedPageTitle)) ||
+ cleanedWindowTitle.includes(cleanedApplicationName)
+ );
+};
+
+const focusBrowser = async ({
+ nvda,
+ applicationName,
+ pageTitle,
+}: {
+ nvda: NVDA;
+ applicationName: string;
+ pageTitle: string;
+}): Promise => {
+ await nvda.perform(NVDAKeyCodeCommands.reportTitle);
+
+ let windowTitle = await nvda.lastSpokenPhrase();
+
+ if (hasBrowserFocus({ applicationName, pageTitle, windowTitle })) {
+ return;
+ }
+
+ let applicationSwitchRetryCount = 0;
+
+ while (applicationSwitchRetryCount < MAX_APPLICATION_SWITCH_RETRY_COUNT) {
+ applicationSwitchRetryCount++;
+
+ await nvda.perform(SWITCH_APPLICATION, { capture: false });
+ await delay(500);
+
+ await nvda.perform(NVDAKeyCodeCommands.reportTitle);
+
+ windowTitle = await nvda.lastSpokenPhrase();
+
+ if (hasBrowserFocus({ applicationName, pageTitle, windowTitle })) {
+ break;
+ }
+ }
+
+ if (applicationSwitchRetryCount === MAX_APPLICATION_SWITCH_RETRY_COUNT) {
+ throw new Error(
+ `Unable to focus the application window with name "${applicationName}" or title "${pageTitle}" after ${MAX_APPLICATION_SWITCH_RETRY_COUNT} attempts.`,
+ );
+ }
+};
+
+export const navigateNVDAtoWebContent = async ({
+ nvda,
+ page,
+ applicationName,
+ capture,
+}: {
+ nvda: NVDA;
+ page: Page;
+ applicationName: string;
+ capture?: CommandOptions["capture"];
+}): Promise => {
+ const currentSpokenPhraseLog = [...(await nvda.spokenPhraseLog())];
+ const currentItemTextLog = [...(await nvda.itemTextLog())];
+
+ await nvda.perform(NVDAKeyCodeCommands.exitFocusMode, { capture: false });
+ await delay(100);
+
+ const pageTitle = await page.title();
+ await focusBrowser({ nvda, applicationName, pageTitle });
+
+ await page.bringToFront();
+ await page.locator("body").waitFor();
+ await page.locator("body").focus();
+ await page.locator("body").click();
+ await page.locator("body").blur();
+
+ // Firefox can leave NVDA focused on the whole browser after an application
+ // switch. Toggling browse/focus mode after moving to a focusable item restores
+ // browse mode. REF: https://github.com/nvaccess/nvda/issues/5758
+ await nvda.perform(NVDAKeyCodeCommands.readNextFocusableItem, {
+ capture: false,
+ });
+ await delay(100);
+
+ await nvda.perform(NVDAKeyCodeCommands.toggleBetweenBrowseAndFocusMode, {
+ capture: false,
+ });
+ await delay(100);
+
+ await nvda.perform(NVDAKeyCodeCommands.toggleBetweenBrowseAndFocusMode, {
+ capture: false,
+ });
+ await delay(100);
+
+ await nvda.perform(NVDAKeyCodeCommands.exitFocusMode, { capture: false });
+ await delay(100);
+
+ await nvda.clearSpokenPhraseLog();
+ await nvda.clearItemTextLog();
+
+ const spokenPhraseLog = await nvda.spokenPhraseLog();
+ const itemTextLog = await nvda.itemTextLog();
+
+ spokenPhraseLog.push(...currentSpokenPhraseLog);
+ itemTextLog.push(...currentItemTextLog);
+
+ await nvda.perform(MOVE_TO_TOP, { capture });
+};
diff --git a/src/navigateOrcaToWebContent.ts b/src/navigateOrcaToWebContent.ts
new file mode 100644
index 0000000..3c808de
--- /dev/null
+++ b/src/navigateOrcaToWebContent.ts
@@ -0,0 +1,85 @@
+import { execFileSync } from "node:child_process";
+import {
+ type CommandOptions,
+ type Orca,
+ orcaKeyCodeCommands,
+} from "@guidepup/guidepup";
+import type { Page } from "@playwright/test";
+import { cleanString } from "./cleanString";
+import { delay } from "./delay";
+
+const MAX_APPLICATION_SWITCH_RETRY_COUNT = 10;
+
+const focusOrcaBrowser = async ({
+ orca,
+ pageTitle,
+}: {
+ orca: Orca;
+ pageTitle: string;
+}): Promise => {
+ const cleanedPageTitle = cleanString(pageTitle);
+
+ if (!cleanedPageTitle) {
+ throw new Error(
+ "Cannot focus an application window with an empty page title.",
+ );
+ }
+
+ const currentSpokenPhraseLog = [...(await orca.spokenPhraseLog())];
+
+ try {
+ for (
+ let retryCount = 0;
+ retryCount <= MAX_APPLICATION_SWITCH_RETRY_COUNT;
+ retryCount++
+ ) {
+ await orca.clearSpokenPhraseLog();
+ await orca.perform(orcaKeyCodeCommands.PresentTitle, {
+ capture: "initial",
+ });
+
+ const windowTitle = cleanString(await orca.lastSpokenPhrase());
+
+ if (windowTitle.includes(cleanedPageTitle)) {
+ return;
+ }
+
+ if (retryCount < MAX_APPLICATION_SWITCH_RETRY_COUNT) {
+ execFileSync("xdotool", ["key", "--clearmodifiers", "alt+Tab"]);
+ await delay(100);
+ }
+ }
+
+ throw new Error(
+ `Unable to focus the application window with title "${pageTitle}" after ${MAX_APPLICATION_SWITCH_RETRY_COUNT} attempts.`,
+ );
+ } finally {
+ await orca.clearSpokenPhraseLog();
+
+ const spokenPhraseLog = await orca.spokenPhraseLog();
+
+ spokenPhraseLog.push(...currentSpokenPhraseLog);
+ }
+};
+
+export const navigateOrcaToWebContent = async ({
+ orca,
+ page,
+ capture,
+}: {
+ orca: Orca;
+ page: Page;
+ capture?: CommandOptions["capture"];
+}): Promise => {
+ const pageTitle = await page.title();
+
+ await focusOrcaBrowser({ orca, pageTitle });
+
+ await page.bringToFront();
+ await page.locator("body").waitFor();
+ await page.locator("body").focus();
+ await page.locator("body").click();
+ await page.locator("body").blur();
+
+ await orca.perform(orcaKeyCodeCommands.StartOfFile, { capture });
+};
diff --git a/src/navigateVoiceOverToWebContent.ts b/src/navigateVoiceOverToWebContent.ts
new file mode 100644
index 0000000..a656b0a
--- /dev/null
+++ b/src/navigateVoiceOverToWebContent.ts
@@ -0,0 +1,161 @@
+import {
+ type CommandOptions,
+ macOSActivateId,
+ MacOSKeyCodes,
+ type VoiceOver,
+ voiceOverKeyCodeCommands,
+} from "@guidepup/guidepup";
+import type { Page } from "@playwright/test";
+import { delay } from "./delay";
+
+const MAX_WEB_CONTENT_SEARCH_ATTEMPTS = 10;
+
+const cancelCurrentInteraction = async (
+ voiceOver: VoiceOver,
+): Promise => {
+ await voiceOver.perform(
+ { keyCode: MacOSKeyCodes.Control },
+ { capture: false },
+ );
+};
+
+const closeMenus = async (voiceOver: VoiceOver): Promise => {
+ await voiceOver.perform(
+ { keyCode: MacOSKeyCodes.Escape },
+ { capture: false },
+ );
+};
+
+const openItemChooser = async (voiceOver: VoiceOver): Promise => {
+ let lastSpokenPhrase = "";
+
+ while (!lastSpokenPhrase.toLowerCase().includes("item chooser")) {
+ await cancelCurrentInteraction(voiceOver);
+ await delay(100);
+
+ await closeMenus(voiceOver);
+ await delay(100);
+
+ await voiceOver.perform(voiceOverKeyCodeCommands.openItemChooser, {
+ capture: true,
+ });
+
+ lastSpokenPhrase = await voiceOver.lastSpokenPhrase();
+ }
+};
+
+const selectWebContentItem = async (voiceOver: VoiceOver): Promise => {
+ let matched = false;
+
+ for (
+ let attempt = 0;
+ attempt < MAX_WEB_CONTENT_SEARCH_ATTEMPTS && !matched;
+ attempt++
+ ) {
+ for (const character of "web content") {
+ await cancelCurrentInteraction(voiceOver);
+ await delay(100);
+
+ await voiceOver.type(character, { capture: "initial" });
+
+ const lastSpokenPhrase = await voiceOver.lastSpokenPhrase();
+
+ if (lastSpokenPhrase.toLowerCase().includes("web content")) {
+ matched = true;
+
+ break;
+ }
+ }
+
+ if (matched) {
+ break;
+ }
+
+ if (attempt + 1 < MAX_WEB_CONTENT_SEARCH_ATTEMPTS) {
+ await voiceOver.perform(
+ { keyCode: MacOSKeyCodes.Backspace },
+ { capture: false },
+ );
+ await delay(100);
+ }
+ }
+
+ if (!matched) {
+ throw new Error(
+ `Unable to find "web content" in the VoiceOver Item Chooser after ${MAX_WEB_CONTENT_SEARCH_ATTEMPTS} attempts.`,
+ );
+ }
+
+ await voiceOver.perform({ keyCode: MacOSKeyCodes.Enter }, { capture: false });
+};
+
+export const navigateVoiceOverToWebContent = async ({
+ voiceOver,
+ page,
+ applicationId,
+ capture,
+}: {
+ voiceOver: VoiceOver;
+ page: Page;
+ applicationId: string;
+ capture?: CommandOptions["capture"];
+}): Promise => {
+ const currentSpokenPhraseLog = [...(await voiceOver.spokenPhraseLog())];
+ const currentItemTextLog = [...(await voiceOver.itemTextLog())];
+
+ await macOSActivateId(applicationId);
+ await cancelCurrentInteraction(voiceOver);
+ await delay(100);
+
+ await page.bringToFront();
+ await page.locator("body").waitFor();
+
+ try {
+ await page.evaluate(() => {
+ const marker = document.createElement("input");
+
+ marker.id = "__guidepup_marker__";
+ marker.type = "text";
+ marker.value = "Guidepup Marker";
+ marker.readOnly = true;
+ marker.tabIndex = -1;
+ marker.autocomplete = "off";
+ marker.setAttribute("aria-label", "Guidepup Marker");
+ marker.style.cssText = `
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ clip: rect(0 0 0 0);
+ white-space: nowrap;
+ `;
+
+ document.body.prepend(marker);
+ });
+
+ await openItemChooser(voiceOver);
+ await selectWebContentItem(voiceOver);
+
+ await voiceOver.interact({ capture: false });
+ await delay(100);
+
+ await voiceOver.clearSpokenPhraseLog();
+ await voiceOver.clearItemTextLog();
+
+ const spokenPhraseLog = await voiceOver.spokenPhraseLog();
+ const itemTextLog = await voiceOver.itemTextLog();
+
+ spokenPhraseLog.push(...currentSpokenPhraseLog);
+ itemTextLog.push(...currentItemTextLog);
+
+ await voiceOver.next({ capture });
+ } finally {
+ await page.evaluate(() => {
+ const marker = document.querySelector("#__guidepup_marker__");
+
+ if (marker) {
+ document.body.removeChild(marker);
+ }
+ });
+ }
+};
diff --git a/src/nvdaTest.ts b/src/nvdaTest.ts
index 7e1cea5..f8bba31 100644
--- a/src/nvdaTest.ts
+++ b/src/nvdaTest.ts
@@ -4,11 +4,9 @@ import {
nvda,
type NVDA,
type StartOptions,
- WindowsKeyCodes,
- WindowsModifiers,
} from "@guidepup/guidepup";
import { applicationNameMap } from "./applicationNameMap";
-import { delay } from "./delay";
+import { navigateNVDAtoWebContent } from "./navigateNVDAtoWebContent";
type Prettify = {
[K in keyof T]: T[K];
@@ -51,78 +49,6 @@ export interface NVDAPlaywright extends NVDA {
const nvdaPlaywright: NVDAPlaywright = nvda as NVDAPlaywright;
-const MAX_APPLICATION_SWITCH_RETRY_COUNT = 10;
-
-const SWITCH_APPLICATION = {
- keyCode: [WindowsKeyCodes.Escape],
- modifiers: [WindowsModifiers.Alt],
-};
-
-const MOVE_TO_TOP = {
- keyCode: [WindowsKeyCodes.Home],
- modifiers: [WindowsModifiers.Control],
-};
-
-type FocusBrowserParams = {
- applicationName: string;
- pageTitle: string;
-};
-
-const cleanString = (str: string): string =>
- str
- .toLowerCase()
- // REF: https://github.com/nvaccess/nvda/blob/master/source/locale/en/symbols.dic
- .replace(/[|ยฆ:;'"`\-โโโยท_()[\]{}\\^~]/g, " ")
- .replace(/\s+/g, " ")
- .trim();
-
-const hasFocus = ({
- applicationName,
- pageTitle,
- windowTitle,
-}: FocusBrowserParams & { windowTitle: string }) => {
- const cleanedApplicationName = cleanString(applicationName);
- const cleanedPageTitle = cleanString(pageTitle);
- const cleanedWindowTitle = cleanString(windowTitle);
-
- return (
- (cleanedPageTitle.length &&
- cleanedWindowTitle.startsWith(cleanedPageTitle)) ||
- cleanedWindowTitle.includes(cleanedApplicationName)
- );
-};
-
-const focusBrowser = async ({
- applicationName,
- pageTitle,
-}: {
- applicationName: string;
- pageTitle: string;
-}) => {
- await nvdaPlaywright.perform(nvdaPlaywright.keyboardCommands.reportTitle);
- let windowTitle = await nvdaPlaywright.lastSpokenPhrase();
-
- if (hasFocus({ applicationName, pageTitle, windowTitle })) {
- return;
- }
-
- let applicationSwitchRetryCount = 0;
-
- while (applicationSwitchRetryCount < MAX_APPLICATION_SWITCH_RETRY_COUNT) {
- applicationSwitchRetryCount++;
-
- await nvdaPlaywright.perform(SWITCH_APPLICATION, { capture: false });
- await delay(500);
-
- await nvdaPlaywright.perform(nvdaPlaywright.keyboardCommands.reportTitle);
- windowTitle = await nvdaPlaywright.lastSpokenPhrase();
-
- if (hasFocus({ applicationName, pageTitle, windowTitle })) {
- break;
- }
- }
-};
-
/**
* These tests extend the default Playwright environment that launches the
* browser with a running instance of the NVDA screen reader for Windows.
@@ -166,77 +92,16 @@ export const nvdaTest = test.extend<{
const applicationName = applicationNameMap[browserName];
if (!applicationName) {
- throw new Error(`Browser ${browserName} is not installed.`);
+ throw new Error(`Browser ${browserName} is not recognised.`);
}
- nvdaPlaywright.navigateToWebContent = async ({ capture } = {}) => {
- const currentSpokenPhraseLog = [
- ...(await nvdaPlaywright.spokenPhraseLog()),
- ];
- const currentItemTextLog = [...(await nvdaPlaywright.itemTextLog())];
-
- // Make sure NVDA is not in focus mode.
- await nvdaPlaywright.perform(
- nvdaPlaywright.keyboardCommands.exitFocusMode,
- { capture: false },
- );
- await delay(100);
-
- // Ensure application is brought to front and focused.
- const pageTitle = await page.title();
- await focusBrowser({ applicationName, pageTitle });
-
- // Ensure the document is ready and focused.
- await page.bringToFront();
- await page.locator("body").waitFor();
- await page.locator("body").focus();
- await page.locator("body").click();
- await page.locator("body").blur();
-
- // NVDA appears to not work well with Firefox when switching between
- // applications resulting in the entire browser window having NVDA focus
- // with focus mode.
- //
- // One workaround is to tab to the next focusable item. From there we can
- // toggle into (yes although we are already in it...) focus mode and back
- // out. In case this ever transpires to not happen as expect, we then ensure
- // we exit focus mode and move NVDA to the top of the page.
- //
- // REF: https://github.com/nvaccess/nvda/issues/5758
- await nvdaPlaywright.perform(
- nvdaPlaywright.keyboardCommands.readNextFocusableItem,
- { capture: false },
- );
- await delay(100);
- await nvdaPlaywright.perform(
- nvdaPlaywright.keyboardCommands.toggleBetweenBrowseAndFocusMode,
- { capture: false },
- );
- await delay(100);
- await nvdaPlaywright.perform(
- nvdaPlaywright.keyboardCommands.toggleBetweenBrowseAndFocusMode,
- { capture: false },
- );
- await delay(100);
- await nvdaPlaywright.perform(
- nvdaPlaywright.keyboardCommands.exitFocusMode,
- { capture: false },
- );
- await delay(100);
-
- await nvdaPlaywright.clearSpokenPhraseLog();
- await nvdaPlaywright.clearItemTextLog();
-
- const spokenPhraseLog = await nvdaPlaywright.spokenPhraseLog();
- const itemTextLog = await nvdaPlaywright.itemTextLog();
-
- spokenPhraseLog.push(...currentSpokenPhraseLog);
- itemTextLog.push(...currentItemTextLog);
-
- // Navigate to the beginning of the web content, using chosen capture
- // settings, so don't miss announcing the first item on the page.
- await nvdaPlaywright.perform(MOVE_TO_TOP, { capture });
- };
+ nvdaPlaywright.navigateToWebContent = ({ capture } = {}) =>
+ navigateNVDAtoWebContent({
+ nvda: nvdaPlaywright,
+ page,
+ applicationName,
+ capture,
+ });
await nvdaPlaywright.start(nvdaStartOptions);
await use(nvdaPlaywright);
diff --git a/src/orcaTest.ts b/src/orcaTest.ts
new file mode 100644
index 0000000..b03291c
--- /dev/null
+++ b/src/orcaTest.ts
@@ -0,0 +1,107 @@
+import { test } from "@playwright/test";
+import {
+ type CommandOptions,
+ type Orca,
+ type StartOptions,
+ unstable_orca,
+} from "@guidepup/guidepup";
+import { navigateOrcaToWebContent } from "./navigateOrcaToWebContent";
+
+/**
+ * [API Reference](https://www.guidepup.dev/docs/api/class-orca)
+ *
+ * This object can be used to launch and control Orca.
+ *
+ * Here's a typical example:
+ *
+ * ```ts
+ * import { unstable_orca } from "@guidepup/guidepup";
+ *
+ * (async () => {
+ * // Start Orca.
+ * await unstable_orca.start();
+ *
+ * // Move to the next item.
+ * await unstable_orca.next();
+ *
+ * // ... perform some commands.
+ *
+ * // Stop Orca.
+ * await unstable_orca.stop();
+ * })();
+ * ```
+ */
+export interface OrcaPlaywright extends Orca {
+ /**
+ * Guidepup Playwright specific command that navigates Orca to the beginning
+ * of the browser's web content.
+ *
+ * This command should be used after a page navigation has completed.
+ */
+ navigateToWebContent(
+ options?: Pick,
+ ): Promise;
+}
+
+const orcaPlaywright: OrcaPlaywright = unstable_orca as OrcaPlaywright;
+
+/**
+ * These tests extend the default Playwright environment that launches the
+ * browser with a running instance of the Orca screen reader for macOS.
+ *
+ * A fresh started Orca instance `orca` is provided to each test.
+ */
+export const orcaTest = test.extend<{
+ /**
+ * [API Reference](https://www.guidepup.dev/docs/api/class-orca)
+ *
+ * This object can be used to launch and control Orca.
+ *
+ * Here's a typical example:
+ *
+ * ```ts
+ * import { unstable_orca } from "@guidepup/guidepup";
+ *
+ * (async () => {
+ * // Start Orca.
+ * await unstable_orca.start();
+ *
+ * // Move to the next item.
+ * await unstable_orca.next();
+ *
+ * // ... perform some commands.
+ *
+ * // Stop Orca.
+ * await unstable_orca.stop();
+ * })();
+ * ```
+ */
+ orca: OrcaPlaywright;
+ /**
+ * [API Reference](https://www.guidepup.dev/docs/api/class-start-options)
+ *
+ * Options to start Orca with, see also [unstable_orca.start([options])](https://www.guidepup.dev/docs/api/class-orca#unstable-orca-start).
+ */
+ orcaStartOptions: StartOptions;
+}>({
+ orcaStartOptions: { capture: "initial" },
+ orca: async ({ page, orcaStartOptions }, use) => {
+ try {
+ orcaPlaywright.navigateToWebContent = ({ capture } = {}) =>
+ navigateOrcaToWebContent({
+ orca: orcaPlaywright,
+ page,
+ capture,
+ });
+
+ await orcaPlaywright.start(orcaStartOptions);
+ await use(orcaPlaywright);
+ } finally {
+ try {
+ await orcaPlaywright.stop();
+ } catch {
+ // swallow stop failure
+ }
+ }
+ },
+});
diff --git a/src/screenReaderTest.ts b/src/screenReaderTest.ts
index b8f8229..103f896 100644
--- a/src/screenReaderTest.ts
+++ b/src/screenReaderTest.ts
@@ -1,20 +1,18 @@
import { test } from "@playwright/test";
import {
type CommandOptions,
- macOSActivate,
- MacOSKeyCodes,
nvda,
- NVDAKeyCodeCommands,
screenReader,
type ScreenReader,
type StartOptions,
+ unstable_orca,
voiceOver,
- voiceOverKeyCodeCommands,
- WindowsKeyCodes,
- WindowsModifiers,
} from "@guidepup/guidepup";
import { applicationNameMap } from "./applicationNameMap";
-import { delay } from "./delay";
+import { navigateNVDAtoWebContent } from "./navigateNVDAtoWebContent";
+import { navigateOrcaToWebContent } from "./navigateOrcaToWebContent";
+import { navigateVoiceOverToWebContent } from "./navigateVoiceOverToWebContent";
+import { applicationIdMap } from "./applicationIdMap";
type Prettify = {
[K in keyof T]: T[K];
@@ -23,81 +21,9 @@ type Prettify = {
type CaptureCommandOptions = Prettify>;
type CaptureStartOptions = Prettify>;
-const MAX_APPLICATION_SWITCH_RETRY_COUNT = 10;
-
-const SWITCH_APPLICATION = {
- keyCode: [WindowsKeyCodes.Escape],
- modifiers: [WindowsModifiers.Alt],
-};
-
-const MOVE_TO_TOP = {
- keyCode: [WindowsKeyCodes.Home],
- modifiers: [WindowsModifiers.Control],
-};
-
-type FocusBrowserParams = {
- applicationName: string;
- pageTitle: string;
-};
-
-const cleanString = (str: string): string =>
- str
- .toLowerCase()
- // REF: https://github.com/nvaccess/nvda/blob/master/source/locale/en/symbols.dic
- .replace(/[|ยฆ:;'"`\-โโโยท_()[\]{}\\^~]/g, " ")
- .replace(/\s+/g, " ")
- .trim();
-
-const hasFocus = ({
- applicationName,
- pageTitle,
- windowTitle,
-}: FocusBrowserParams & { windowTitle: string }) => {
- const cleanedApplicationName = cleanString(applicationName);
- const cleanedPageTitle = cleanString(pageTitle);
- const cleanedWindowTitle = cleanString(windowTitle);
-
- return (
- (cleanedPageTitle.length &&
- cleanedWindowTitle.startsWith(cleanedPageTitle)) ||
- cleanedWindowTitle.includes(cleanedApplicationName)
- );
-};
-
-const focusBrowser = async ({
- applicationName,
- pageTitle,
-}: {
- applicationName: string;
- pageTitle: string;
-}) => {
- await screenReaderPlaywright.perform(NVDAKeyCodeCommands.reportTitle);
- let windowTitle = await screenReaderPlaywright.lastSpokenPhrase();
-
- if (hasFocus({ applicationName, pageTitle, windowTitle })) {
- return;
- }
-
- let applicationSwitchRetryCount = 0;
-
- while (applicationSwitchRetryCount < MAX_APPLICATION_SWITCH_RETRY_COUNT) {
- applicationSwitchRetryCount++;
-
- await screenReaderPlaywright.perform(SWITCH_APPLICATION, {
- capture: false,
- });
- await delay(500);
-
- await screenReaderPlaywright.perform(NVDAKeyCodeCommands.reportTitle);
- windowTitle = await screenReaderPlaywright.lastSpokenPhrase();
-
- if (hasFocus({ applicationName, pageTitle, windowTitle })) {
- break;
- }
- }
-};
-
/**
+ * [API Reference](https://www.guidepup.dev/docs/api/class-screenreader)
+ *
* This object can be used to launch and control the default screen reader for
* the environment.
*
@@ -130,91 +56,6 @@ export interface ScreenReaderPlaywright extends ScreenReader {
navigateToWebContent(options?: CaptureCommandOptions): Promise;
}
-const closeMenus = async ({
- screenReaderPlaywright,
-}: {
- screenReaderPlaywright: ScreenReaderPlaywright;
-}) => {
- await screenReaderPlaywright.perform(
- { keyCode: MacOSKeyCodes.Escape },
- { capture: false },
- );
-};
-
-const cancelCurrentInteraction = async ({
- screenReaderPlaywright,
-}: {
- screenReaderPlaywright: ScreenReaderPlaywright;
-}) => {
- await screenReaderPlaywright.perform(
- { keyCode: MacOSKeyCodes.Control },
- { capture: false },
- );
-};
-
-const openItemChooser = async ({
- screenReaderPlaywright,
-}: {
- screenReaderPlaywright: ScreenReaderPlaywright;
-}) => {
- let lastSpokenPhrase = "";
-
- while (!lastSpokenPhrase.toLowerCase().includes("item chooser")) {
- await cancelCurrentInteraction({ screenReaderPlaywright });
- await delay(100);
- await closeMenus({ screenReaderPlaywright });
- await delay(100);
-
- await screenReaderPlaywright.perform(
- voiceOverKeyCodeCommands.openItemChooser,
- { capture: true },
- );
-
- lastSpokenPhrase = await screenReaderPlaywright.lastSpokenPhrase();
- }
-};
-
-async function selectWebContentItem({
- screenReaderPlaywright,
-}: {
- screenReaderPlaywright: ScreenReaderPlaywright;
-}) {
- // eslint-disable-next-line no-constant-condition
- while (true) {
- let matched = false;
-
- for (const character of "web content") {
- await cancelCurrentInteraction({ screenReaderPlaywright });
- await delay(100);
-
- await screenReaderPlaywright.type(character, { capture: "initial" });
- const lastSpokenPhrase = await screenReaderPlaywright.lastSpokenPhrase();
-
- if (lastSpokenPhrase.toLowerCase().includes("web content")) {
- matched = true;
-
- break;
- }
- }
-
- if (matched) {
- break;
- }
-
- // Clear the entire Item Chooser search.
- await screenReaderPlaywright.perform(
- { keyCode: MacOSKeyCodes.Backspace },
- { capture: false },
- );
- await delay(100);
- }
-
- await screenReaderPlaywright.perform(
- { keyCode: MacOSKeyCodes.Enter },
- { capture: false },
- );
-}
-
const screenReaderPlaywright: ScreenReaderPlaywright =
screenReader as ScreenReaderPlaywright;
@@ -228,6 +69,8 @@ const screenReaderPlaywright: ScreenReaderPlaywright =
*/
export const screenReaderTest = test.extend<{
/**
+ * [API Reference](https://www.guidepup.dev/docs/api/class-screenreader)
+ *
* This object can be used to launch and control the default screen reader for
* the environment.
*
@@ -252,7 +95,9 @@ export const screenReaderTest = test.extend<{
*/
screenReader: ScreenReaderPlaywright;
/**
- * Options to start the default screen reader with.
+ * [API Reference](https://www.guidepup.dev/docs/api/class-start-options)
+ *
+ * Options to start the default screen reader with, see also [screenReader.start([options])](https://www.guidepup.dev/docs/api/class-screenreader#screenreader-start).
*/
screenReaderStartOptions: CaptureStartOptions;
}>({
@@ -263,166 +108,35 @@ export const screenReaderTest = test.extend<{
) => {
try {
const applicationName = applicationNameMap[browserName];
+ const applicationId = applicationIdMap[browserName];
- if (!applicationName) {
- throw new Error(`Browser ${browserName} is not installed.`);
+ if (!applicationName || !applicationId) {
+ throw new Error(`Browser ${browserName} is not recognised.`);
}
if (nvda.default()) {
- screenReaderPlaywright.navigateToWebContent = async ({
- capture,
- } = {}) => {
- const currentSpokenPhraseLog = [
- ...(await screenReaderPlaywright.spokenPhraseLog()),
- ];
- const currentItemTextLog = [
- ...(await screenReaderPlaywright.itemTextLog()),
- ];
-
- // Make sure NVDA is not in focus mode.
- await screenReaderPlaywright.perform(
- NVDAKeyCodeCommands.exitFocusMode,
- { capture: false },
- );
- await delay(100);
-
- // Ensure application is brought to front and focused.
- const pageTitle = await page.title();
- await focusBrowser({ applicationName, pageTitle });
-
- // Ensure the document is ready and focused.
- await page.bringToFront();
- await page.locator("body").waitFor();
- await page.locator("body").focus();
- await page.locator("body").click();
- await page.locator("body").blur();
-
- // NVDA appears to not work well with Firefox when switching between
- // applications resulting in the entire browser window having NVDA focus
- // with focus mode.
- //
- // One workaround is to tab to the next focusable item. From there we can
- // toggle into (yes although we are already in it...) focus mode and back
- // out. In case this ever transpires to not happen as expect, we then ensure
- // we exit focus mode and move NVDA to the top of the page.
- //
- // REF: https://github.com/nvaccess/nvda/issues/5758
- await screenReaderPlaywright.perform(
- NVDAKeyCodeCommands.readNextFocusableItem,
- { capture: false },
- );
- await delay(100);
- await screenReaderPlaywright.perform(
- NVDAKeyCodeCommands.toggleBetweenBrowseAndFocusMode,
- { capture: false },
- );
- await delay(100);
- await screenReaderPlaywright.perform(
- NVDAKeyCodeCommands.toggleBetweenBrowseAndFocusMode,
- { capture: false },
- );
- await delay(100);
- await screenReaderPlaywright.perform(
- NVDAKeyCodeCommands.exitFocusMode,
- { capture: false },
- );
- await delay(100);
-
- await screenReaderPlaywright.clearSpokenPhraseLog();
- await screenReaderPlaywright.clearItemTextLog();
-
- const spokenPhraseLog =
- await screenReaderPlaywright.spokenPhraseLog();
- const itemTextLog = await screenReaderPlaywright.itemTextLog();
-
- spokenPhraseLog.push(...currentSpokenPhraseLog);
- itemTextLog.push(...currentItemTextLog);
-
- // Navigate to the beginning of the web content, using chosen capture
- // settings, so don't miss announcing the first item on the page.
- await screenReaderPlaywright.perform(MOVE_TO_TOP, { capture });
- };
+ screenReaderPlaywright.navigateToWebContent = ({ capture } = {}) =>
+ navigateNVDAtoWebContent({
+ nvda,
+ page,
+ applicationName,
+ capture,
+ });
} else if (voiceOver.default()) {
- screenReaderPlaywright.navigateToWebContent = async ({
- capture,
- } = {}) => {
- const currentSpokenPhraseLog = [
- ...(await screenReaderPlaywright.spokenPhraseLog()),
- ];
- const currentItemTextLog = [
- ...(await screenReaderPlaywright.itemTextLog()),
- ];
-
- // Ensure application is brought to front and focused.
- await macOSActivate(applicationName);
-
- // Cancel automatic behaviours/previous commands.
- await cancelCurrentInteraction({ screenReaderPlaywright });
- await delay(100);
-
- // Ensure the document is ready and focused.
- await page.bringToFront();
- await page.locator("body").waitFor();
-
- try {
- // Add an interactive marker to the page that will force VoiceOver
- // to listen to events emitted by Playwright interactions when
- // navigated to.
- await page.evaluate(() => {
- const marker = document.createElement("input");
-
- marker.id = "__guidepup_marker__";
- marker.type = "text";
- marker.value = "Guidepup Marker";
- marker.readOnly = true;
- marker.tabIndex = -1;
- marker.autocomplete = "off";
- marker.setAttribute("aria-label", "Guidepup Marker");
- marker.style.cssText = `
- position: absolute;
- width: 1px;
- height: 1px;
- overflow: hidden;
- clip: rect(0 0 0 0);
- white-space: nowrap;
- `;
-
- document.body.prepend(marker);
- });
-
- // Open item chooser and select web content.
- await openItemChooser({ screenReaderPlaywright });
- await selectWebContentItem({ screenReaderPlaywright });
-
- // Navigate into web content.
- await screenReaderPlaywright.interact({ capture: false });
- await delay(100);
-
- await screenReaderPlaywright.clearSpokenPhraseLog();
- await screenReaderPlaywright.clearItemTextLog();
-
- const spokenPhraseLog =
- await screenReaderPlaywright.spokenPhraseLog();
- const itemTextLog = await screenReaderPlaywright.itemTextLog();
-
- spokenPhraseLog.push(...currentSpokenPhraseLog);
- itemTextLog.push(...currentItemTextLog);
-
- // Navigate to the first element of the page using the provided
- // capture settings.
- await screenReaderPlaywright.next({ capture });
- } finally {
- // Remove the temporary Guidepup marker element to restore original
- // page structure.
- await page.evaluate(() => {
- const marker = document.querySelector("#__guidepup_marker__");
-
- if (marker) {
- document.body.removeChild(marker);
- }
- });
- }
- };
+ screenReaderPlaywright.navigateToWebContent = ({ capture } = {}) =>
+ navigateVoiceOverToWebContent({
+ voiceOver,
+ page,
+ applicationId,
+ capture,
+ });
+ } else if (unstable_orca.default()) {
+ screenReaderPlaywright.navigateToWebContent = ({ capture } = {}) =>
+ navigateOrcaToWebContent({
+ orca: unstable_orca,
+ page,
+ capture,
+ });
} else {
throw new Error("No supported screen reader");
}
diff --git a/src/voiceOverTest.ts b/src/voiceOverTest.ts
index f8123e5..b404b1c 100644
--- a/src/voiceOverTest.ts
+++ b/src/voiceOverTest.ts
@@ -1,12 +1,12 @@
-import { test } from "@playwright/test";
-import { voiceOver, macOSActivate, MacOSKeyCodes } from "@guidepup/guidepup";
-import type {
- StartOptions,
- CommandOptions,
- VoiceOver,
+import {
+ type CommandOptions,
+ type StartOptions,
+ type VoiceOver,
+ voiceOver,
} from "@guidepup/guidepup";
-import { applicationNameMap } from "./applicationNameMap";
-import { delay } from "./delay";
+import { applicationIdMap } from "./applicationIdMap";
+import { navigateVoiceOverToWebContent } from "./navigateVoiceOverToWebContent";
+import { test } from "@playwright/test";
/**
* [API Reference](https://www.guidepup.dev/docs/api/class-voiceover)
@@ -44,97 +44,12 @@ export interface VoiceOverPlaywright extends VoiceOver {
): Promise;
}
-const closeMenus = async ({
- voiceOverPlaywright,
-}: {
- voiceOverPlaywright: VoiceOverPlaywright;
-}) => {
- await voiceOverPlaywright.perform(
- { keyCode: MacOSKeyCodes.Escape },
- { capture: false },
- );
-};
-
-const cancelCurrentInteraction = async ({
- voiceOverPlaywright,
-}: {
- voiceOverPlaywright: VoiceOverPlaywright;
-}) => {
- await voiceOverPlaywright.perform(
- { keyCode: MacOSKeyCodes.Control },
- { capture: false },
- );
-};
-
-const openItemChooser = async ({
- voiceOverPlaywright,
-}: {
- voiceOverPlaywright: VoiceOverPlaywright;
-}) => {
- let lastSpokenPhrase = "";
-
- while (!lastSpokenPhrase.toLowerCase().includes("item chooser")) {
- await cancelCurrentInteraction({ voiceOverPlaywright });
- await delay(100);
- await closeMenus({ voiceOverPlaywright });
- await delay(100);
-
- await voiceOverPlaywright.perform(
- voiceOverPlaywright.keyboardCommands.openItemChooser,
- { capture: true },
- );
-
- lastSpokenPhrase = await voiceOverPlaywright.lastSpokenPhrase();
- }
-};
-
-async function selectWebContentItem({
- voiceOverPlaywright,
-}: {
- voiceOverPlaywright: VoiceOverPlaywright;
-}) {
- // eslint-disable-next-line no-constant-condition
- while (true) {
- let matched = false;
-
- for (const character of "web content") {
- await cancelCurrentInteraction({ voiceOverPlaywright });
- await delay(100);
-
- await voiceOverPlaywright.type(character, { capture: "initial" });
- const lastSpokenPhrase = await voiceOverPlaywright.lastSpokenPhrase();
-
- if (lastSpokenPhrase.toLowerCase().includes("web content")) {
- matched = true;
-
- break;
- }
- }
-
- if (matched) {
- break;
- }
-
- // Clear the entire Item Chooser search.
- await voiceOverPlaywright.perform(
- { keyCode: MacOSKeyCodes.Backspace },
- { capture: false },
- );
- await delay(100);
- }
-
- await voiceOverPlaywright.perform(
- { keyCode: MacOSKeyCodes.Enter },
- { capture: false },
- );
-}
-
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.
+ * browser with a running instance of the VoiceOver screen reader for macOS.
*
* A fresh started VoiceOver instance `voiceOver` is provided to each test.
*/
@@ -174,89 +89,19 @@ export const voiceOverTest = test.extend<{
voiceOverStartOptions: { capture: "initial" },
voiceOver: async ({ browserName, page, voiceOverStartOptions }, use) => {
try {
- const applicationName = applicationNameMap[browserName];
+ const applicationId = applicationIdMap[browserName];
- if (!applicationName) {
- throw new Error(`Browser ${browserName} is not installed.`);
+ if (!applicationId) {
+ throw new Error(`Browser ${browserName} is not recognised.`);
}
- voiceOverPlaywright.navigateToWebContent = async ({ capture } = {}) => {
- const currentSpokenPhraseLog = [
- ...(await voiceOverPlaywright.spokenPhraseLog()),
- ];
- const currentItemTextLog = [
- ...(await voiceOverPlaywright.itemTextLog()),
- ];
-
- // Ensure application is brought to front and focused.
- await macOSActivate(applicationName);
-
- // Cancel automatic behaviours/previous commands.
- await cancelCurrentInteraction({ voiceOverPlaywright });
- await delay(100);
-
- // Ensure the document is ready and focused.
- await page.bringToFront();
- await page.locator("body").waitFor();
-
- try {
- // Add an interactive marker to the page that will force VoiceOver
- // to listen to events emitted by Playwright interactions when
- // navigated to.
- await page.evaluate(() => {
- const marker = document.createElement("input");
-
- marker.id = "__guidepup_marker__";
- marker.type = "text";
- marker.value = "Guidepup Marker";
- marker.readOnly = true;
- marker.tabIndex = -1;
- marker.autocomplete = "off";
- marker.setAttribute("aria-label", "Guidepup Marker");
- marker.style.cssText = `
- position: absolute;
- width: 1px;
- height: 1px;
- overflow: hidden;
- clip: rect(0 0 0 0);
- white-space: nowrap;
- `;
-
- document.body.prepend(marker);
- });
-
- // Open item chooser and select web content.
- await openItemChooser({ voiceOverPlaywright });
- await selectWebContentItem({ voiceOverPlaywright });
-
- // Navigate into web content.
- await voiceOverPlaywright.interact({ capture: false });
- await delay(100);
-
- await voiceOverPlaywright.clearSpokenPhraseLog();
- await voiceOverPlaywright.clearItemTextLog();
-
- const spokenPhraseLog = await voiceOverPlaywright.spokenPhraseLog();
- const itemTextLog = await voiceOverPlaywright.itemTextLog();
-
- spokenPhraseLog.push(...currentSpokenPhraseLog);
- itemTextLog.push(...currentItemTextLog);
-
- // Navigate to the first element of the page using the provided
- // capture settings.
- await voiceOverPlaywright.next({ capture });
- } finally {
- // Remove the temporary Guidepup marker element to restore original
- // page structure.
- await page.evaluate(() => {
- const marker = document.querySelector("#__guidepup_marker__");
-
- if (marker) {
- document.body.removeChild(marker);
- }
- });
- }
- };
+ voiceOverPlaywright.navigateToWebContent = ({ capture } = {}) =>
+ navigateVoiceOverToWebContent({
+ voiceOver: voiceOverPlaywright,
+ page,
+ applicationId,
+ capture,
+ });
await voiceOverPlaywright.start(voiceOverStartOptions);
await use(voiceOverPlaywright);
diff --git a/yarn.lock b/yarn.lock
index df88107..2e844af 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -62,17 +62,19 @@
"@shikijs/types" "^3.23.0"
"@shikijs/vscode-textmate" "^10.0.2"
-"@guidepup/guidepup@^0.33.0":
- version "0.33.0"
- resolved "https://registry.yarnpkg.com/@guidepup/guidepup/-/guidepup-0.33.0.tgz#60025daa906f5c055e60a095c27dad1f02359264"
- integrity sha512-CNFMsUtalwr9varDvevUwv8vKs3VWDshsMehlUGuEkxE4BiQ+Dlz2j0A1LSvPcdIN40oNi98sx4sXHygjdfR3g==
+"@guidepup/guidepup@^0.36.1":
+ version "0.36.1"
+ resolved "https://registry.yarnpkg.com/@guidepup/guidepup/-/guidepup-0.36.1.tgz#dff3bcb0233f229f36c17e507a845723987d5d05"
+ integrity sha512-Pucx6sFUeGSeh2Q0bE4JtoH2z0a+BC6yNIFYpBBic6jDI6I1zk5duvg5Jmn0tMN9VSQbGM1KHSXRI//tm9d4rA==
dependencies:
+ dbus-native "^0.15.1"
+ debug "^4.4.3"
plist "^4.0.0"
-"@guidepup/record@^0.1.0":
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/@guidepup/record/-/record-0.1.0.tgz#03acb4286aab208037a4d5ef65fc2f5615ce485f"
- integrity sha512-jtSqZ0CZVdWzWIbLWHYoUi1IbNJVKns20oEYzwcTftw8rY5RXsK1OTXaXFx9Gup5mnmYaFLnON8WwTS+4DYwrQ==
+"@guidepup/record@^0.3.0":
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/@guidepup/record/-/record-0.3.0.tgz#0bf0861d12da27592b0032d04efbb44fad3234f4"
+ integrity sha512-0PyoVl9yt/k34oYCWoo3WUKy452zsJDrz1xOq0SKeQM0RDD8M1emTRNEjuFU7+/BpZHisWaNXRRGIrnGvEkizg==
dependencies:
ffmpeg-static "^5.2.0"
@@ -134,12 +136,12 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
-"@playwright/test@^1.60.0":
- version "1.61.1"
- resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.61.1.tgz#48568dc22af7819e55fa5e8e3bc79b7e6a3e6675"
- integrity sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==
+"@playwright/test@^1.62.1":
+ version "1.64.1"
+ resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.64.1.tgz#03d48b0cd135db5a70a6fbca54b2b91a597eed01"
+ integrity sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==
dependencies:
- playwright "1.61.1"
+ playwright "1.64.1"
"@shikijs/engine-oniguruma@^3.23.0":
version "3.23.0"
@@ -474,6 +476,13 @@ cross-spawn@^7.0.2:
shebang-command "^2.0.0"
which "^2.0.1"
+dbus-native@^0.15.1:
+ version "0.15.2"
+ resolved "https://registry.yarnpkg.com/dbus-native/-/dbus-native-0.15.2.tgz#ac6094e7c95f9e60bdfb7f7229a4adbc9296d861"
+ integrity sha512-J1IBi4LmbqrFhroN66Dj1tCCTP6TVbZgUKhWQZf3uS6t93yRWEcRRDjOZV9N6boGRhOp/BOjLliFm9vWu1QZOA==
+ dependencies:
+ xml2js "^0.6.2"
+
debug@4:
version "4.3.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
@@ -696,11 +705,6 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
-fsevents@2.3.2:
- version "2.3.2"
- resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
- integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
-
glob-parent@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
@@ -1019,19 +1023,17 @@ picomatch@^4.0.4:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.5.tgz#51ea57a17d86f605f81039595fbc40ed06a55fab"
integrity sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==
-playwright-core@1.61.1:
- version "1.61.1"
- resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.61.1.tgz#3c99841307efbbabc9d724c41a88c914705d15fc"
- integrity sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==
+playwright-core@1.64.1:
+ version "1.64.1"
+ resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.64.1.tgz#ca9ec82d7da6ced1b2326ec59ef097aaa9a29f94"
+ integrity sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==
-playwright@1.61.1:
- version "1.61.1"
- resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.61.1.tgz#d8c0c06eb93c28981afc747bace453bdbd5018bc"
- integrity sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==
+playwright@1.64.1:
+ version "1.64.1"
+ resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.64.1.tgz#c174f48e17f5f3fba71726a9bcdeb8534d5c665d"
+ integrity sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==
dependencies:
- playwright-core "1.61.1"
- optionalDependencies:
- fsevents "2.3.2"
+ playwright-core "1.64.1"
plist@^4.0.0:
version "4.0.0"
@@ -1112,6 +1114,11 @@ safe-buffer@~5.2.0:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+sax@>=0.6.0:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.6.1.tgz#4c23cf608c0b693ab54b4b5888e92cfe977b9843"
+ integrity sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==
+
semver@^7.7.3:
version "7.8.5"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.5.tgz#39b646037dd50c14fb451e7e4cac58ed8b863f69"
@@ -1278,11 +1285,24 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+xml2js@^0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.2.tgz#dd0b630083aa09c161e25a4d0901e2b2a929b499"
+ integrity sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==
+ dependencies:
+ sax ">=0.6.0"
+ xmlbuilder "~11.0.0"
+
xmlbuilder@^15.1.1:
version "15.1.1"
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5"
integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==
+xmlbuilder@~11.0.0:
+ version "11.0.1"
+ resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
+ integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==
+
yaml@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.9.0.tgz#78274afd93598a1dfdd6130df6a566defcbf9aa4"