From 81380cc61f36e6a6221a3a5d0ab47956f48529e1 Mon Sep 17 00:00:00 2001 From: cmorten Date: Mon, 22 Jun 2026 22:09:11 +0100 Subject: [PATCH] feat: os agnostic screen reader class --- .github/workflows/test-screenreader.yml | 39 +++ examples/logIncludesExpectedPhrases.ts | 10 +- .../tests/chromium/chromium.spec.ts | 2 +- .../tests/firefox/firefox.spec.ts | 2 +- examples/playwright-screenreader/README.md | 18 ++ .../chromium.config.ts | 18 ++ .../tests/chromium/chromium.spec.ts | 40 +++ .../chromium.spokenPhrase.snapshot.json | 1 + .../tests/headerNavigation.ts | 50 ++++ .../tests/chromium/chromium.spec.ts | 2 +- .../tests/firefox/firefox.spec.ts | 2 +- .../tests/webkit/webkit.spec.ts | 2 +- package.json | 8 +- src/index.ts | 2 + src/nvdaTest.ts | 3 - src/screenReaderTest.ts | 283 ++++++++++++++++++ src/voiceOverTest.ts | 8 - yarn.lock | 8 +- 18 files changed, 471 insertions(+), 27 deletions(-) create mode 100644 .github/workflows/test-screenreader.yml create mode 100644 examples/playwright-screenreader/README.md create mode 100644 examples/playwright-screenreader/chromium.config.ts create mode 100644 examples/playwright-screenreader/tests/chromium/chromium.spec.ts create mode 100644 examples/playwright-screenreader/tests/chromium/chromium.spokenPhrase.snapshot.json create mode 100644 examples/playwright-screenreader/tests/headerNavigation.ts create mode 100644 src/screenReaderTest.ts diff --git a/.github/workflows/test-screenreader.yml b/.github/workflows/test-screenreader.yml new file mode 100644 index 0000000..6d4bf7f --- /dev/null +++ b/.github/workflows/test-screenreader.yml @@ -0,0 +1,39 @@ +name: Test Screen Reader + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + test-screenreader: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macos-14, macos-15, macos-26, windows-2022, windows-2025] + browser: [chromium] + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + - name: Guidepup Setup + uses: guidepup/setup-action@0.20.0 + with: + record: true + - run: yarn install --frozen-lockfile + - run: yarn pretest + - run: yarn test:screenreader:${{ matrix.browser }} + - uses: actions/upload-artifact@v4 + if: always() + continue-on-error: true + with: + name: artifacts-${{ matrix.os }}-${{ matrix.browser }} + path: | + **/test-results/**/* + **/recordings/**/* diff --git a/examples/logIncludesExpectedPhrases.ts b/examples/logIncludesExpectedPhrases.ts index f2269ca..50a0dac 100644 --- a/examples/logIncludesExpectedPhrases.ts +++ b/examples/logIncludesExpectedPhrases.ts @@ -1,12 +1,14 @@ export const logIncludesExpectedPhrases = ( log: string[], - expectedPhrases: string[] + expectedPhrases: string[], ) => { const failures: string[] = []; for (const expectedPhrase of expectedPhrases) { - const foundExpectedPhrase = !!log.find((logItem) => - logItem.includes(expectedPhrase) + const foundExpectedPhrase = !!log.find( + (logItem) => + logItem.includes(expectedPhrase) || + logItem.replaceAll(/\s/g, "").includes(expectedPhrase), ); if (!foundExpectedPhrase) { @@ -16,7 +18,7 @@ export const logIncludesExpectedPhrases = ( if (failures.length) { throw new Error( - `Did not find the following expected text:\n- ${failures.join("\n- ")}` + `Did not find the following expected text:\n- ${failures.join("\n- ")}`, ); } }; diff --git a/examples/playwright-nvda/tests/chromium/chromium.spec.ts b/examples/playwright-nvda/tests/chromium/chromium.spec.ts index faef603..535b5c9 100644 --- a/examples/playwright-nvda/tests/chromium/chromium.spec.ts +++ b/examples/playwright-nvda/tests/chromium/chromium.spec.ts @@ -8,7 +8,7 @@ import { nvdaTest as test } from "../../../../src"; test.use({ nvdaStartOptions: { capture: "initial" } }); test.describe("Chromium Playwright NVDA", () => { - test("I can navigate the Guidepup Github page", async ({ + test("I can navigate the Guidepup documentation site", async ({ browser, browserName, page, diff --git a/examples/playwright-nvda/tests/firefox/firefox.spec.ts b/examples/playwright-nvda/tests/firefox/firefox.spec.ts index 99e9a4b..8748158 100644 --- a/examples/playwright-nvda/tests/firefox/firefox.spec.ts +++ b/examples/playwright-nvda/tests/firefox/firefox.spec.ts @@ -8,7 +8,7 @@ import { nvdaTest as test } from "../../../../src"; test.use({ nvdaStartOptions: { capture: "initial" } }); test.describe("Firefox Playwright VoiceOver", () => { - test("I can navigate the Guidepup Github page", async ({ + test("I can navigate the Guidepup documentation site", async ({ browser, browserName, page, diff --git a/examples/playwright-screenreader/README.md b/examples/playwright-screenreader/README.md new file mode 100644 index 0000000..3b3065a --- /dev/null +++ b/examples/playwright-screenreader/README.md @@ -0,0 +1,18 @@ +# Screen Reader Example + +An example demonstrating using Guidepup for testing the default screen reader automation with [Playwright](https://playwright.dev/). + +Run this example's test with the following from the root directory: + +```console +npm run test +``` + +> Note: please ensure you have [setup you environment](https://www.guidepup.dev/docs/guides/automated-environment-setup) for NVDA or VoiceOver automation before running this example. + +## Test flow + +1. The test launches the browser using Playwright +2. Navigates to the GitHub website +3. Moves through the website using the default screen reader for the environment, controlled by Guidepup +4. Traverses headings until the Guidepup heading in the README.md is found diff --git a/examples/playwright-screenreader/chromium.config.ts b/examples/playwright-screenreader/chromium.config.ts new file mode 100644 index 0000000..def99d5 --- /dev/null +++ b/examples/playwright-screenreader/chromium.config.ts @@ -0,0 +1,18 @@ +import { devices, PlaywrightTestConfig } from "@playwright/test"; +import { screenReaderConfig } from "../../src"; + +const config: PlaywrightTestConfig = { + ...screenReaderConfig, + reportSlowTests: null, + timeout: 5 * 60 * 1000, + retries: 1, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"], headless: false }, + }, + ], + reporter: process.env.CI ? [["github"], ["html", { open: "never" }]] : "list", +}; + +export default config; diff --git a/examples/playwright-screenreader/tests/chromium/chromium.spec.ts b/examples/playwright-screenreader/tests/chromium/chromium.spec.ts new file mode 100644 index 0000000..b8e413e --- /dev/null +++ b/examples/playwright-screenreader/tests/chromium/chromium.spec.ts @@ -0,0 +1,40 @@ +import { platform, release } from "os"; +import { headerNavigation } from "../headerNavigation"; +import { logIncludesExpectedPhrases } from "../../../logIncludesExpectedPhrases"; +import spokenPhraseSnapshot from "./chromium.spokenPhrase.snapshot.json"; +import { screenReaderTest as test } from "../../../../src"; + +test.describe("Chromium 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 { retry } = test.info(); + + console.table({ + osName, + osVersion, + browserName, + browserVersion, + retry, + }); + + 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. + + 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); + }); +}); diff --git a/examples/playwright-screenreader/tests/chromium/chromium.spokenPhrase.snapshot.json b/examples/playwright-screenreader/tests/chromium/chromium.spokenPhrase.snapshot.json new file mode 100644 index 0000000..68bd46a --- /dev/null +++ b/examples/playwright-screenreader/tests/chromium/chromium.spokenPhrase.snapshot.json @@ -0,0 +1 @@ +["Guidepup", "Docs", "API", "GitHub"] diff --git a/examples/playwright-screenreader/tests/headerNavigation.ts b/examples/playwright-screenreader/tests/headerNavigation.ts new file mode 100644 index 0000000..3b65f55 --- /dev/null +++ b/examples/playwright-screenreader/tests/headerNavigation.ts @@ -0,0 +1,50 @@ +import { log } from "../../log"; +import { Page } from "@playwright/test"; +import type { ScreenReaderPlaywright } from "../../../src"; + +const MAX_NAVIGATION_LOOP = 10; + +export async function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export async function headerNavigation({ + page, + screenReader, +}: { + page: Page; + screenReader: ScreenReaderPlaywright; +}) { + // 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 screenReader.navigateToWebContent(); + await delay(500); + + let navigationCount = 0; + + // Move across the 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()}".`); +} diff --git a/examples/playwright-voiceover/tests/chromium/chromium.spec.ts b/examples/playwright-voiceover/tests/chromium/chromium.spec.ts index cd03ed9..6a4848e 100644 --- a/examples/playwright-voiceover/tests/chromium/chromium.spec.ts +++ b/examples/playwright-voiceover/tests/chromium/chromium.spec.ts @@ -9,7 +9,7 @@ import { voiceOverTest as test } from "../../../../src"; test.use({ voiceOverStartOptions: { capture: "initial" } }); test.describe("Chromium Playwright VoiceOver", () => { - test("I can navigate the Guidepup Github page", async ({ + test("I can navigate the Guidepup documentation site", async ({ browser, browserName, page, diff --git a/examples/playwright-voiceover/tests/firefox/firefox.spec.ts b/examples/playwright-voiceover/tests/firefox/firefox.spec.ts index 18b8edd..32371ba 100644 --- a/examples/playwright-voiceover/tests/firefox/firefox.spec.ts +++ b/examples/playwright-voiceover/tests/firefox/firefox.spec.ts @@ -9,7 +9,7 @@ import { voiceOverTest as test } from "../../../../src"; test.use({ voiceOverStartOptions: { capture: "initial" } }); test.describe("Firefox Playwright VoiceOver", () => { - test("I can navigate the Guidepup Github page", async ({ + test("I can navigate the Guidepup documentation site", async ({ browser, browserName, page, diff --git a/examples/playwright-voiceover/tests/webkit/webkit.spec.ts b/examples/playwright-voiceover/tests/webkit/webkit.spec.ts index 68520c1..7d71470 100644 --- a/examples/playwright-voiceover/tests/webkit/webkit.spec.ts +++ b/examples/playwright-voiceover/tests/webkit/webkit.spec.ts @@ -9,7 +9,7 @@ import { voiceOverTest as test } from "../../../../src"; test.use({ voiceOverStartOptions: { capture: "initial" } }); test.describe("Webkit Playwright VoiceOver", () => { - test("I can navigate the Guidepup Github page", async ({ + test("I can navigate the Guidepup documentation site", async ({ browser, browserName, page, diff --git a/package.json b/package.json index 22b052f..bd7c7d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@guidepup/playwright", - "version": "0.16.3", + "version": "0.17.0", "description": "Screen reader automation library for Playwright testing. ", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -34,15 +34,17 @@ "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: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:screenreader:chromium": "playwright test --config ./examples/playwright-screenreader/chromium.config.ts ./examples/playwright-screenreader/tests/chromium/", "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.25.0", + "@guidepup/guidepup": "^0.27.0", "@guidepup/record": "^0.1.0", "@playwright/test": "^1.60.0", "@types/node": "^24.10.1", @@ -56,7 +58,7 @@ "typescript": "^5.9.3" }, "peerDependencies": { - "@guidepup/guidepup": ">=0.25.0", + "@guidepup/guidepup": ">=0.27.0", "@playwright/test": "^1.57.0" }, "resolutions": { diff --git a/src/index.ts b/src/index.ts index ef5d858..54e8f4b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,7 @@ export { nvdaTest } from "./nvdaTest"; export type { NVDAPlaywright } from "./nvdaTest"; export { screenReaderConfig } from "./screenReaderConfig"; +export { screenReaderTest } from "./screenReaderTest"; +export type { ScreenReaderPlaywright } from "./screenReaderTest"; export { voiceOverTest } from "./voiceOverTest"; export type { VoiceOverPlaywright } from "./voiceOverTest"; diff --git a/src/nvdaTest.ts b/src/nvdaTest.ts index 74b82da..03066a2 100644 --- a/src/nvdaTest.ts +++ b/src/nvdaTest.ts @@ -161,9 +161,6 @@ export const nvdaTest = test.extend<{ throw new Error(`Browser ${browserName} is not installed.`); } - await page.goto("about:blank", { waitUntil: "load" }); - await page.bringToFront(); - nvdaPlaywright.navigateToWebContent = async ( clearLogs: boolean = true, ) => { diff --git a/src/screenReaderTest.ts b/src/screenReaderTest.ts new file mode 100644 index 0000000..e570276 --- /dev/null +++ b/src/screenReaderTest.ts @@ -0,0 +1,283 @@ +import { test } from "@playwright/test"; +import { + CommandOptions, + macOSActivate, + MacOSKeyCodes, + nvda, + NVDAKeyCodeCommands, + screenReader, + type ScreenReader, + voiceOver, + voiceOverKeyCodeCommands, + WindowsKeyCodes, + WindowsModifiers, +} from "@guidepup/guidepup"; +import { applicationNameMap } from "./applicationNameMap"; + +type Prettify = { + [K in keyof T]: T[K]; +} & {}; + +type CaptureCommandOptions = 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); + await screenReaderPlaywright.perform(NVDAKeyCodeCommands.reportTitle); + windowTitle = await screenReaderPlaywright.lastSpokenPhrase(); + + if (hasFocus({ applicationName, pageTitle, windowTitle })) { + break; + } + } +}; + +/** + * This object can be used to launch and control the default screen reader for + * the environment. + * + * Here's a typical example: + * + * ```ts + * import { screenReader } from "@guidepup/guidepup"; + * + * (async () => { + * // Start the screen reader. + * await screenReader.start(); + * + * // Move to the next item. + * await screenReader.next(); + * + * // ... perform some commands. + * + * // Stop the screen reader. + * await screenReader.stop(); + * })(); + * ``` + */ +export interface ScreenReaderPlaywright extends ScreenReader { + /** + * Guidepup Playwright specific command that navigates the screen reader to + * the beginning of the browser's web content. + * + * This command should be used after page navigation. + * + * Note: this command clears all logs by default. + */ + navigateToWebContent(clearLogs?: boolean): Promise; +} + +const screenReaderPlaywright: ScreenReaderPlaywright = + screenReader as ScreenReaderPlaywright; + +/** + * These tests extend the default Playwright environment that launches the + * browser with a running instance of the default screen reader for the current + * OS. + * + * A fresh started screen reader instance `screenReader` is provided to each + * test. + */ +export const screenReaderTest = test.extend<{ + /** + * This object can be used to launch and control the default screen reader for + * the environment. + * + * Here's a typical example: + * + * ```ts + * import { screenReader } from "@guidepup/guidepup"; + * + * (async () => { + * // Start the screen reader. + * await screenReader.start(); + * + * // Move to the next item. + * await screenReader.next(); + * + * // ... perform some commands. + * + * // Stop the screen reader. + * await screenReader.stop(); + * })(); + * ``` + */ + screenReader: ScreenReaderPlaywright; + /** + * Options to start the default screen reader with. + */ + screenReaderStartOptions: CaptureCommandOptions; +}>({ + screenReaderStartOptions: { capture: "initial" }, + screenReader: async ( + { browserName, page, screenReaderStartOptions }, + use, + ) => { + try { + const applicationName = applicationNameMap[browserName]; + + if (!applicationName) { + throw new Error(`Browser ${browserName} is not installed.`); + } + + if (nvda.default()) { + screenReaderPlaywright.navigateToWebContent = async ( + clearLogs: boolean = true, + ) => { + // Make sure NVDA is not in focus mode. + await screenReaderPlaywright.perform( + NVDAKeyCodeCommands.exitFocusMode, + ); + + const pageTitle = await page.title(); + // Ensure application is brought to front and focused. + 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, + ); + await screenReaderPlaywright.perform( + NVDAKeyCodeCommands.toggleBetweenBrowseAndFocusMode, + ); + await screenReaderPlaywright.perform( + NVDAKeyCodeCommands.toggleBetweenBrowseAndFocusMode, + ); + await screenReaderPlaywright.perform( + NVDAKeyCodeCommands.exitFocusMode, + ); + await screenReaderPlaywright.perform(MOVE_TO_TOP); + + if (clearLogs) { + // Clear out logs. + await screenReaderPlaywright.clearItemTextLog(); + await screenReaderPlaywright.clearSpokenPhraseLog(); + } + }; + } else if (voiceOver.default()) { + screenReaderPlaywright.navigateToWebContent = async ( + clearLogs: boolean = true, + ) => { + await macOSActivate(applicationName); + + await screenReaderPlaywright.perform({ + keyCode: MacOSKeyCodes.Control, + }); + + await page.bringToFront(); + await page.locator("body").waitFor(); + + await screenReaderPlaywright.perform( + voiceOverKeyCodeCommands.openItemChooser, + ); + + await screenReaderPlaywright.type("web content"); + + await screenReaderPlaywright.perform({ + keyCode: MacOSKeyCodes.Enter, + }); + + await screenReaderPlaywright.interact(); + + await screenReaderPlaywright.perform( + voiceOverKeyCodeCommands.moveToBeginningOfText, + ); + + await screenReaderPlaywright.perform({ + keyCode: MacOSKeyCodes.Control, + }); + + if (clearLogs) { + await screenReaderPlaywright.clearItemTextLog(); + await screenReaderPlaywright.clearSpokenPhraseLog(); + } + }; + } else { + throw new Error("No supported screen reader"); + } + + await screenReaderPlaywright.start(screenReaderStartOptions); + await use(screenReaderPlaywright); + } finally { + try { + await screenReaderPlaywright.stop(); + } catch { + // swallow stop failure + } + } + }, +}); diff --git a/src/voiceOverTest.ts b/src/voiceOverTest.ts index f157c0d..73b0df4 100644 --- a/src/voiceOverTest.ts +++ b/src/voiceOverTest.ts @@ -134,14 +134,6 @@ export const voiceOverTest = test.extend<{ }; await voiceOverPlaywright.start(voiceOverStartOptions); - await macOSActivate(applicationName); - - // Cancel auto navigation - await voiceOverPlaywright.perform( - { keyCode: MacOSKeyCodes.Control }, - { capture: false }, - ); - await use(voiceOverPlaywright); } finally { try { diff --git a/yarn.lock b/yarn.lock index d3f3761..e812f09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -69,10 +69,10 @@ "@shikijs/types" "^3.19.0" "@shikijs/vscode-textmate" "^10.0.2" -"@guidepup/guidepup@^0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@guidepup/guidepup/-/guidepup-0.25.0.tgz#b7d1c2d7e6ad9bc8a8deacfd09e2dfde1b612e52" - integrity sha512-WDOA1nloFKxTrO3HDMQ5iVgr4UqxB0lqixenwj0V18z+OP/Jiw0icFsHhUW/MnVYCLmvwEsiCdvcT043EDfkRQ== +"@guidepup/guidepup@^0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@guidepup/guidepup/-/guidepup-0.27.0.tgz#1f228496e7bff199760dd4d774a8fd9a98c78a6e" + integrity sha512-ZB2zswfkG4nDMrlmCpR4pYenkG5OEw19eoPNIxirZPIgR5tllTKFmPWV+7e2R+TXtlSQaLgaNYt95SDN7Nr87w== dependencies: regedit "5.1.2" semver "^7.3.8"