From c1bde60c56b6d9e09b47042c72a90ce66be8ba36 Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 9 Aug 2026 20:20:56 +0100 Subject: [PATCH 1/4] feat: support playwright command capture --- package.json | 4 +- src/nvdaTest.ts | 19 +++--- src/screenReaderTest.ts | 130 ++++++++++++++++++++++++++-------------- src/voiceOverTest.ts | 128 ++++++++++++++++++++++++++------------- yarn.lock | 8 +-- 5 files changed, 191 insertions(+), 98 deletions(-) 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..d6ac841 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" From 445de42d0cd0df8bda603ea1266649f0df99da2d Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 9 Aug 2026 20:22:31 +0100 Subject: [PATCH 2/4] test: add coverage for playwright capture and live region --- .../chromium/chromium-live-region.spec.ts | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 examples/playwright-screenreader/tests/chromium/chromium-live-region.spec.ts 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"); + }); +}); From e7defbbf3b3fb57c2705becb8caab9d206361146 Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 9 Aug 2026 20:41:37 +0100 Subject: [PATCH 3/4] test: add screen reader name and version to test logs --- examples/playwright-nvda/tests/chromium/chromium.spec.ts | 4 ++++ examples/playwright-nvda/tests/firefox/firefox.spec.ts | 4 ++++ .../playwright-screenreader/tests/chromium/chromium.spec.ts | 4 ++++ examples/playwright-voiceover/tests/chromium/chromium.spec.ts | 4 ++++ examples/playwright-voiceover/tests/firefox/firefox.spec.ts | 4 ++++ examples/playwright-voiceover/tests/webkit/webkit.spec.ts | 4 ++++ 6 files changed, 24 insertions(+) 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.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, }); From de668718ed535a811919a4c95706a6cfe2186306 Mon Sep 17 00:00:00 2001 From: cmorten Date: Sun, 9 Aug 2026 20:45:08 +0100 Subject: [PATCH 4/4] sec: bump package --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index d6ac841..df88107 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"