diff --git a/examples/playwright-nvda/tests/chromium/chromium.spec.ts b/examples/playwright-nvda/tests/chromium/chromium.spec.ts
index 535b5c9..fb8f631 100644
--- a/examples/playwright-nvda/tests/chromium/chromium.spec.ts
+++ b/examples/playwright-nvda/tests/chromium/chromium.spec.ts
@@ -17,6 +17,8 @@ test.describe("Chromium Playwright 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-${osName}-${osVersion}-${browserName}-${browserVersion}-attempt-${retry}-${+new Date()}.mov`;
@@ -25,6 +27,8 @@ test.describe("Chromium Playwright NVDA", () => {
osVersion,
browserName,
browserVersion,
+ screenReaderName,
+ screenReaderVersion,
retry,
});
diff --git a/examples/playwright-nvda/tests/firefox/firefox.spec.ts b/examples/playwright-nvda/tests/firefox/firefox.spec.ts
index 8748158..f0e1540 100644
--- a/examples/playwright-nvda/tests/firefox/firefox.spec.ts
+++ b/examples/playwright-nvda/tests/firefox/firefox.spec.ts
@@ -17,6 +17,8 @@ test.describe("Firefox Playwright VoiceOver", () => {
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-${osName}-${osVersion}-${browserName}-${browserVersion}-attempt-${retry}-${+new Date()}.mov`;
@@ -25,6 +27,8 @@ test.describe("Firefox Playwright VoiceOver", () => {
osVersion,
browserName,
browserVersion,
+ screenReaderName,
+ screenReaderVersion,
retry,
});
diff --git a/examples/playwright-screenreader/tests/chromium/chromium-live-region.spec.ts b/examples/playwright-screenreader/tests/chromium/chromium-live-region.spec.ts
new file mode 100644
index 0000000..c14ac78
--- /dev/null
+++ b/examples/playwright-screenreader/tests/chromium/chromium-live-region.spec.ts
@@ -0,0 +1,82 @@
+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";
+
+test.describe("Chromium Playwright Screen Reader", () => {
+ test("I can capture 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();
+
+ console.table({
+ osName,
+ osVersion,
+ browserName,
+ browserVersion,
+ screenReaderName,
+ screenReaderVersion,
+ retry,
+ });
+
+ log("Navigating to live region test page.");
+
+ await page.goto("about:blank", {
+ waitUntil: "load",
+ });
+
+ await page.setContent(`
+
+ 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}".`);
+
+ 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");
+ });
+});
diff --git a/examples/playwright-screenreader/tests/chromium/chromium.spec.ts b/examples/playwright-screenreader/tests/chromium/chromium.spec.ts
index b8e413e..529e7df 100644
--- a/examples/playwright-screenreader/tests/chromium/chromium.spec.ts
+++ b/examples/playwright-screenreader/tests/chromium/chromium.spec.ts
@@ -14,6 +14,8 @@ test.describe("Chromium Playwright Screen Reader", () => {
const osName = platform();
const osVersion = release();
const browserVersion = browser.version();
+ const screenReaderName = screenReader.name;
+ const screenReaderVersion = screenReader.version;
const { retry } = test.info();
console.table({
@@ -21,6 +23,8 @@ test.describe("Chromium Playwright Screen Reader", () => {
osVersion,
browserName,
browserVersion,
+ screenReaderName,
+ screenReaderVersion,
retry,
});
diff --git a/examples/playwright-voiceover/tests/chromium/chromium.spec.ts b/examples/playwright-voiceover/tests/chromium/chromium.spec.ts
index 6a4848e..5bc05e9 100644
--- a/examples/playwright-voiceover/tests/chromium/chromium.spec.ts
+++ b/examples/playwright-voiceover/tests/chromium/chromium.spec.ts
@@ -18,6 +18,8 @@ test.describe("Chromium Playwright 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-${osName}-${osVersion}-${browserName}-${browserVersion}-attempt-${retry}-${+new Date()}.mov`;
@@ -26,6 +28,8 @@ test.describe("Chromium Playwright VoiceOver", () => {
osVersion,
browserName,
browserVersion,
+ screenReaderName,
+ screenReaderVersion,
retry,
});
diff --git a/examples/playwright-voiceover/tests/firefox/firefox.spec.ts b/examples/playwright-voiceover/tests/firefox/firefox.spec.ts
index 32371ba..c0aab28 100644
--- a/examples/playwright-voiceover/tests/firefox/firefox.spec.ts
+++ b/examples/playwright-voiceover/tests/firefox/firefox.spec.ts
@@ -18,6 +18,8 @@ test.describe("Firefox Playwright 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-${osName}-${osVersion}-${browserName}-${browserVersion}-attempt-${retry}-${+new Date()}.mov`;
@@ -26,6 +28,8 @@ test.describe("Firefox Playwright VoiceOver", () => {
osVersion,
browserName,
browserVersion,
+ screenReaderName,
+ screenReaderVersion,
retry,
});
diff --git a/examples/playwright-voiceover/tests/webkit/webkit.spec.ts b/examples/playwright-voiceover/tests/webkit/webkit.spec.ts
index 7d71470..3d96364 100644
--- a/examples/playwright-voiceover/tests/webkit/webkit.spec.ts
+++ b/examples/playwright-voiceover/tests/webkit/webkit.spec.ts
@@ -18,6 +18,8 @@ test.describe("Webkit Playwright 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-${osName}-${osVersion}-${browserName}-${browserVersion}-attempt-${retry}-${+new Date()}.mov`;
@@ -26,6 +28,8 @@ test.describe("Webkit Playwright VoiceOver", () => {
osVersion,
browserName,
browserVersion,
+ screenReaderName,
+ screenReaderVersion,
retry,
});
diff --git a/package.json b/package.json
index 123f2b0..43d18e4 100644
--- a/package.json
+++ b/package.json
@@ -44,7 +44,7 @@
"test:voiceover:webkit": "playwright test --config ./examples/playwright-voiceover/webkit.config.ts ./examples/playwright-voiceover/tests/webkit/"
},
"devDependencies": {
- "@guidepup/guidepup": "^0.32.1",
+ "@guidepup/guidepup": "^0.33.0",
"@guidepup/record": "^0.1.0",
"@playwright/test": "^1.60.0",
"@types/node": "^24.10.1",
@@ -58,7 +58,7 @@
"typescript": "^5.9.3"
},
"peerDependencies": {
- "@guidepup/guidepup": ">=0.27.0",
+ "@guidepup/guidepup": ">=0.33.0",
"@playwright/test": "^1.57.0"
},
"resolutions": {
diff --git a/src/nvdaTest.ts b/src/nvdaTest.ts
index 7d94067..7e1cea5 100644
--- a/src/nvdaTest.ts
+++ b/src/nvdaTest.ts
@@ -1,6 +1,12 @@
import { test } from "@playwright/test";
-import { nvda, WindowsKeyCodes, WindowsModifiers } from "@guidepup/guidepup";
-import type { CommandOptions, NVDA } from "@guidepup/guidepup";
+import {
+ type CommandOptions,
+ nvda,
+ type NVDA,
+ type StartOptions,
+ WindowsKeyCodes,
+ WindowsModifiers,
+} from "@guidepup/guidepup";
import { applicationNameMap } from "./applicationNameMap";
import { delay } from "./delay";
@@ -9,6 +15,7 @@ type Prettify = {
} & {};
type CaptureCommandOptions = Prettify>;
+type CaptureStartOptions = Prettify>;
/**
* [API Reference](https://www.guidepup.dev/docs/api/class-nvda)
@@ -39,9 +46,7 @@ export interface NVDAPlaywright extends NVDA {
*
* This command should be used after page navigation.
*/
- navigateToWebContent(
- options?: Pick,
- ): Promise;
+ navigateToWebContent(options?: CaptureCommandOptions): Promise;
}
const nvdaPlaywright: NVDAPlaywright = nvda as NVDAPlaywright;
@@ -149,11 +154,11 @@ export const nvdaTest = test.extend<{
*/
nvda: NVDAPlaywright;
/**
- * [API Reference](https://www.guidepup.dev/docs/api/class-command-options)
+ * [API Reference](https://www.guidepup.dev/docs/api/class-start-options)
*
* Options to start NVDA with, see also [nvda.start([options])](https://www.guidepup.dev/docs/api/class-nvda#nvda-start).
*/
- nvdaStartOptions: CaptureCommandOptions;
+ nvdaStartOptions: CaptureStartOptions;
}>({
nvdaStartOptions: { capture: "initial" },
nvda: async ({ browserName, page, nvdaStartOptions }, use) => {
diff --git a/src/screenReaderTest.ts b/src/screenReaderTest.ts
index 4bff2c8..c73f023 100644
--- a/src/screenReaderTest.ts
+++ b/src/screenReaderTest.ts
@@ -1,12 +1,13 @@
import { test } from "@playwright/test";
import {
- CommandOptions,
+ type CommandOptions,
macOSActivate,
MacOSKeyCodes,
nvda,
NVDAKeyCodeCommands,
screenReader,
type ScreenReader,
+ type StartOptions,
voiceOver,
voiceOverKeyCodeCommands,
WindowsKeyCodes,
@@ -20,6 +21,7 @@ type Prettify = {
} & {};
type CaptureCommandOptions = Prettify>;
+type CaptureStartOptions = Prettify>;
const MAX_APPLICATION_SWITCH_RETRY_COUNT = 10;
@@ -125,9 +127,7 @@ export interface ScreenReaderPlaywright extends ScreenReader {
*
* This command should be used after page navigation.
*/
- navigateToWebContent(
- options?: Pick,
- ): Promise;
+ navigateToWebContent(options?: CaptureCommandOptions): Promise;
}
const screenReaderPlaywright: ScreenReaderPlaywright =
@@ -169,7 +169,7 @@ export const screenReaderTest = test.extend<{
/**
* Options to start the default screen reader with.
*/
- screenReaderStartOptions: CaptureCommandOptions;
+ screenReaderStartOptions: CaptureStartOptions;
}>({
screenReaderStartOptions: { capture: "initial" },
screenReader: async (
@@ -275,51 +275,93 @@ export const screenReaderTest = test.extend<{
await page.bringToFront();
await page.locator("body").waitFor();
- // Open the web item chooser.
- await screenReaderPlaywright.perform(
- voiceOverKeyCodeCommands.openItemChooser,
- { capture: false },
- );
- await delay(500);
-
- // Filter by "web content" - currently web content items for all browsers
- // are suffixed by "web content".
- for (const character of "web content") {
- await screenReaderPlaywright.type(character, { capture: false });
+ 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 the web item chooser.
+ await screenReaderPlaywright.perform(
+ voiceOverKeyCodeCommands.openItemChooser,
+ { capture: false },
+ );
+ await delay(500);
+
+ // Filter by "web content" - currently web content items for all browsers
+ // are suffixed by "web content".
+ for (const character of "web content") {
+ await screenReaderPlaywright.type(character, { capture: false });
+ await delay(100);
+ }
+
+ // Select the web content window spot.
+ await screenReaderPlaywright.perform(
+ { keyCode: MacOSKeyCodes.Enter },
+ { capture: false },
+ );
await delay(100);
- }
- // Select the web content window spot.
- await screenReaderPlaywright.perform(
- { keyCode: MacOSKeyCodes.Enter },
- { capture: false },
- );
- await delay(100);
+ // Navigate into web content.
+ await screenReaderPlaywright.interact({ capture: false });
+ await delay(100);
- // Navigate into web content.
- await screenReaderPlaywright.interact({ capture: false });
- await delay(100);
+ // Navigate to the beginning of the web content.
+ await screenReaderPlaywright.perform(
+ voiceOverKeyCodeCommands.moveToBeginningOfText,
+ { capture: false },
+ );
+ await delay(100);
- // Navigate to the beginning of the web content.
- await screenReaderPlaywright.perform(
- voiceOverKeyCodeCommands.moveToBeginningOfText,
- { capture: false },
- );
- await delay(100);
+ // Cancel auto navigation
+ await screenReaderPlaywright.perform(
+ { keyCode: MacOSKeyCodes.Control },
+ { capture: false },
+ );
+ await delay(100);
- // Cancel auto navigation
- await screenReaderPlaywright.perform(
- { keyCode: MacOSKeyCodes.Control },
- { capture: false },
- );
- await delay(100);
+ // Navigate to the Guidepup marker element at beginning of the web
+ // content.
+ await screenReaderPlaywright.perform(
+ voiceOverKeyCodeCommands.moveToBeginningOfText,
+ { capture: false },
+ );
+ await delay(100);
- // 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(
- voiceOverKeyCodeCommands.moveToBeginningOfText,
- { capture },
- );
+ // 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);
+ }
+ });
+ }
};
} else {
throw new Error("No supported screen reader");
diff --git a/src/voiceOverTest.ts b/src/voiceOverTest.ts
index abeec96..cace6d2 100644
--- a/src/voiceOverTest.ts
+++ b/src/voiceOverTest.ts
@@ -1,6 +1,10 @@
import { test } from "@playwright/test";
import { voiceOver, macOSActivate, MacOSKeyCodes } from "@guidepup/guidepup";
-import type { CommandOptions, VoiceOver } from "@guidepup/guidepup";
+import type {
+ StartOptions,
+ CommandOptions,
+ VoiceOver,
+} from "@guidepup/guidepup";
import { applicationNameMap } from "./applicationNameMap";
import { delay } from "./delay";
@@ -76,11 +80,11 @@ export const voiceOverTest = test.extend<{
*/
voiceOver: VoiceOverPlaywright;
/**
- * [API Reference](https://www.guidepup.dev/docs/api/class-command-options)
+ * [API Reference](https://www.guidepup.dev/docs/api/class-start-options)
*
* Options to start VoiceOver with, see also [voiceOver.start([options])](https://www.guidepup.dev/docs/api/class-voiceover#voiceover-start).
*/
- voiceOverStartOptions: CommandOptions;
+ voiceOverStartOptions: StartOptions;
}>({
voiceOverStartOptions: { capture: "initial" },
voiceOver: async ({ browserName, page, voiceOverStartOptions }, use) => {
@@ -106,51 +110,93 @@ export const voiceOverTest = test.extend<{
await page.bringToFront();
await page.locator("body").waitFor();
- // Open the web item chooser.
- await voiceOverPlaywright.perform(
- voiceOverPlaywright.keyboardCommands.openItemChooser,
- { capture: false },
- );
- await delay(500);
+ 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;
+ `;
- // Filter by "web content" - currently web content items for all browsers
- // are suffixed by "web content".
- for (const character of "web content") {
- await voiceOverPlaywright.type(character, { capture: false });
+ document.body.prepend(marker);
+ });
+
+ // Open the web item chooser.
+ await voiceOverPlaywright.perform(
+ voiceOverPlaywright.keyboardCommands.openItemChooser,
+ { capture: false },
+ );
+ await delay(500);
+
+ // Filter by "web content" - currently web content items for all browsers
+ // are suffixed by "web content".
+ for (const character of "web content") {
+ await voiceOverPlaywright.type(character, { capture: false });
+ await delay(100);
+ }
+
+ // Select the web content window spot.
+ await voiceOverPlaywright.perform(
+ { keyCode: MacOSKeyCodes.Enter },
+ { capture: false },
+ );
await delay(100);
- }
- // Select the web content window spot.
- await voiceOverPlaywright.perform(
- { keyCode: MacOSKeyCodes.Enter },
- { capture: false },
- );
- await delay(100);
+ // Navigate into web content.
+ await voiceOverPlaywright.interact({ capture: false });
+ await delay(100);
- // Navigate into web content.
- await voiceOverPlaywright.interact({ capture: false });
- await delay(100);
+ // Navigate to the beginning of the web content.
+ await voiceOverPlaywright.perform(
+ voiceOverPlaywright.keyboardCommands.moveToBeginningOfText,
+ { capture: false },
+ );
+ await delay(100);
- // Navigate to the beginning of the web content.
- await voiceOverPlaywright.perform(
- voiceOverPlaywright.keyboardCommands.moveToBeginningOfText,
- { capture: false },
- );
- await delay(100);
+ // Cancel auto navigation
+ await voiceOverPlaywright.perform(
+ { keyCode: MacOSKeyCodes.Control },
+ { capture: false },
+ );
+ await delay(100);
- // Cancel auto navigation
- await voiceOverPlaywright.perform(
- { keyCode: MacOSKeyCodes.Control },
- { capture: false },
- );
- await delay(100);
+ // Navigate to the Guidepup marker element at beginning of the web
+ // content.
+ await voiceOverPlaywright.perform(
+ voiceOverPlaywright.keyboardCommands.moveToBeginningOfText,
+ { capture: false },
+ );
+ await delay(100);
- // Navigate to the beginning of the web content, using chosen capture
- // settings, so don't miss announcing the first item on the page.
- await voiceOverPlaywright.perform(
- voiceOverPlaywright.keyboardCommands.moveToBeginningOfText,
- { capture },
- );
+ // 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);
+ }
+ });
+ }
};
await voiceOverPlaywright.start(voiceOverStartOptions);
diff --git a/yarn.lock b/yarn.lock
index 608a9f3..df88107 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -62,10 +62,10 @@
"@shikijs/types" "^3.23.0"
"@shikijs/vscode-textmate" "^10.0.2"
-"@guidepup/guidepup@^0.32.1":
- version "0.32.1"
- resolved "https://registry.yarnpkg.com/@guidepup/guidepup/-/guidepup-0.32.1.tgz#41435ee52d8fbd3d7ceb23d59501c85199f6b60d"
- integrity sha512-oeUIca/Vl5xjEIDsMbE2mkZyYy/jzPNtMs424BWSQKuR5XuRlNK3SGlQKaLmMb1E+R7z4OuvE6B2wzAg1KVSHQ==
+"@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==
dependencies:
plist "^4.0.0"
@@ -825,9 +825,9 @@ isexe@^2.0.0:
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
js-yaml@^4.1.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.0.tgz#d1900572a7f7cf0b5f540c83673e60bad3436592"
- integrity sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.1.tgz#01216c001d67f48e2cd560d708c7af21090a3848"
+ integrity sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==
dependencies:
argparse "^2.0.1"