Skip to content

Commit 4aa2dd4

Browse files
committed
test(nvda-smoke): focus browser via NVDA Alt+Esc (foreground-lock safe)
1 parent 5764e96 commit 4aa2dd4

1 file changed

Lines changed: 34 additions & 17 deletions

File tree

‎tests/e2e/a11y-nvda-smoke.ts‎

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
// NVDA smoke: proves the self-hosted Windows runner drives REAL NVDA via
22
// guidepup in CI (the Windows analog of the VoiceOver lane). Self-contained —
33
// no deploy preview / OpenBao, and no extra deps: uses @guidepup/guidepup's
4-
// nvda driver (a repo dependency) + Playwright's bundled chromium. Launches a
5-
// static page, focuses it, walks the NVDA cursor, and asserts NVDA actually
6-
// spoke the page's content.
4+
// nvda driver + Playwright's bundled chromium (both already repo deps).
5+
//
6+
// The load-bearing trick (mirrors @guidepup/playwright's navigateToWebContent):
7+
// under a CI job the runner console holds the OS foreground, so SetForeground-
8+
// Window is blocked. Instead we bring the browser to NVDA's focus with
9+
// NVDA-driven Alt+Esc app-switching, verified by reading the window title —
10+
// which the OS honors regardless of foreground lock.
711
//
812
// Run: npx tsx tests/e2e/a11y-nvda-smoke.ts
913
import { chromium } from "@playwright/test";
10-
import { nvda, windowsActivate } from "@guidepup/guidepup";
14+
import { nvda, WindowsKeyCodes, WindowsModifiers } from "@guidepup/guidepup";
1115
import { mkdirSync, writeFileSync } from "node:fs";
1216

1317
const PAGE =
@@ -23,6 +27,26 @@ const PAGE =
2327
);
2428

2529
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
30+
const clean = (s: string) => s.toLowerCase().replace(/[^a-z0-9 ]+/g, " ").replace(/\s+/g, " ").trim();
31+
32+
// Cycle windows via Alt+Esc until NVDA reports the browser window as focused.
33+
// NVDA reports its window title as e.g. "NVDA smoke - Google Chrome for Testing".
34+
// Alt+Esc is OS-honored regardless of foreground lock (unlike SetForegroundWindow).
35+
async function focusBrowser(pageTitle: string): Promise<{ ok: boolean; seen: string[] }> {
36+
const want = clean(pageTitle);
37+
const SWITCH = { keyCode: [WindowsKeyCodes.Escape], modifiers: [WindowsModifiers.Alt] };
38+
const seen: string[] = [];
39+
for (let i = 0; i < 12; i++) {
40+
await nvda.perform(nvda.keyboardCommands.reportTitle);
41+
await sleep(400); // let NVDA finish speaking before reading the phrase
42+
const title = clean(await nvda.lastSpokenPhrase());
43+
seen.push(title);
44+
if (want.length && (title.startsWith(want) || title.includes("chrom"))) return { ok: true, seen };
45+
await nvda.perform(SWITCH, { capture: false });
46+
await sleep(700);
47+
}
48+
return { ok: false, seen };
49+
}
2650

2751
async function main() {
2852
await nvda.start();
@@ -34,18 +58,12 @@ async function main() {
3458
});
3559
const page = await browser.newPage();
3660
await page.goto(PAGE);
37-
await page.bringToFront();
38-
await sleep(800);
39-
// Force the Chromium window to the OS foreground so NVDA's browse cursor
40-
// lands in it. Required in CI, where the runner's job console otherwise
41-
// holds foreground and NVDA reads nothing (window title = page <title>).
42-
await windowsActivate("chrome.exe", "NVDA smoke").catch(() => {});
43-
await sleep(1200);
44-
await page.locator("h1").click().catch(() => {});
45-
await sleep(800);
61+
await sleep(1000);
4662

63+
const focused = await focusBrowser("NVDA smoke");
64+
console.log("focusBrowser ok=" + focused.ok + " titles seen: " + JSON.stringify(focused.seen));
4765
await nvda.clearSpokenPhraseLog();
48-
await nvda.press("Control+Home"); // jump to top of document, reads the H1
66+
await nvda.press("Control+Home"); // top of document, reads the H1
4967
for (let i = 0; i < 6; i++) {
5068
await nvda.next(); // walk paragraph, link, button
5169
}
@@ -60,9 +78,8 @@ async function main() {
6078
if (nonEmpty.length === 0) {
6179
throw new Error("NVDA produced no spoken content — the AT was not driven");
6280
}
63-
const joined = nonEmpty.join(" | ").toLowerCase();
64-
if (!joined.includes("smoke")) {
65-
throw new Error("NVDA spoke, but did not read the smoke page (H1) — got: " + joined);
81+
if (!nonEmpty.join(" | ").toLowerCase().includes("smoke")) {
82+
throw new Error("NVDA spoke, but did not read the smoke page: " + nonEmpty.join(" | "));
6683
}
6784
console.log("SMOKE PASS: NVDA read the page (H1 + " + (nonEmpty.length - 1) + " more items)");
6885
} finally {

0 commit comments

Comments
 (0)