From 8d5a011144d1e08c31e7d2d78961563f1ceead41 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:27:17 -0600 Subject: [PATCH 01/28] fix(wdio): support v9 wdio switchFrame and switchWindow --- packages/webdriverio/src/index.ts | 29 ++++--- packages/webdriverio/src/types.ts | 83 ++++++++++++------- packages/webdriverio/src/utils.ts | 52 +++++++++--- .../webdriverio/test/axe-webdriverio.spec.ts | 9 ++ 4 files changed, 120 insertions(+), 53 deletions(-) diff --git a/packages/webdriverio/src/index.ts b/packages/webdriverio/src/index.ts index f2d8e188..a83f9d79 100644 --- a/packages/webdriverio/src/index.ts +++ b/packages/webdriverio/src/index.ts @@ -11,6 +11,9 @@ import { axeFinishRun, axeRunLegacy, configureAllowedOrigins, + clientSwitchFrame, + clientSwitchParentFrame, + clientSwitchWindow, FRAME_LOAD_TIMEOUT } from './utils'; import { getFilename } from 'cross-dirname'; @@ -230,7 +233,7 @@ export default class AxeBuilder { continue; } await this.inject(iframe); - await this.client.switchToParentFrame(); + await clientSwitchParentFrame(this.client); } catch (error) { logOrRethrowError(error); } @@ -253,7 +256,7 @@ export default class AxeBuilder { // ensure we fail quickly if an iframe cannot be loaded (instead of waiting // the default length of 30 seconds) const { pageLoad } = await this.client.getTimeouts(); - (this.client as WebdriverIO.Browser).setTimeout({ + this.client.setTimeout({ pageLoad: FRAME_LOAD_TIMEOUT }); @@ -261,7 +264,7 @@ export default class AxeBuilder { try { partials = await this.runPartialRecursive(context); } finally { - (this.client as WebdriverIO.Browser).setTimeout({ + this.client.setTimeout({ pageLoad }); } @@ -308,12 +311,12 @@ export default class AxeBuilder { * - https://webdriver.io/docs/api/webdriver.html#switchtoframe */ private async setBrowsingContext( - id: null | WdioElement | WdioBrowser = null + id: WdioElement | null = null ): Promise { if (id) { - await this.client.switchToFrame(id); + await clientSwitchFrame(this.client, id); } else { - await this.client.switchToParentFrame(); + await clientSwitchParentFrame(this.client); } } @@ -335,7 +338,7 @@ export default class AxeBuilder { try { const frame = await this.client.$(frameSelector); assert(frame, `Expect frame of "${frameSelector}" to be defined`); - await this.client.switchToFrame(frame); + await clientSwitchFrame(this.client, frame); await axeSourceInject(this.client, this.script); partials.push( ...(await this.runPartialRecursive(frameContext, [ @@ -345,16 +348,16 @@ export default class AxeBuilder { ); } catch { const [topWindow] = await this.client.getWindowHandles(); - await this.client.switchToWindow(topWindow); + await clientSwitchWindow(this.client, topWindow); for (const frameElm of frameStack) { - await this.client.switchToFrame(frameElm); + await clientSwitchFrame(this.client, frameElm); } partials.push(null); } } - await this.client.switchToParentFrame(); + await clientSwitchParentFrame(this.client); return partials; } @@ -368,8 +371,8 @@ export default class AxeBuilder { ); try { - await client.switchToWindow(newWindow.handle); - await (client as WebdriverIO.Browser).url('data:text/html,'); + await clientSwitchWindow(client, newWindow.handle); + await client.url('data:text/html,'); } catch (error) { throw new Error( `switchToWindow failed. Are you using updated browser drivers? \nDriver reported:\n${ @@ -381,7 +384,7 @@ export default class AxeBuilder { const res = await axeFinishRun(client, axeSource, partials, option); // Cleanup await client.closeWindow(); - await client.switchToWindow(win); + await clientSwitchWindow(client, win); return res; } diff --git a/packages/webdriverio/src/types.ts b/packages/webdriverio/src/types.ts index 560f40fe..7d3cdf0c 100644 --- a/packages/webdriverio/src/types.ts +++ b/packages/webdriverio/src/types.ts @@ -1,35 +1,58 @@ import type { AxeResults, BaseSelector } from 'axe-core'; import * as axe from 'axe-core'; -import { type Browser, type Element } from 'webdriverio'; - -/* - This type allows both webdriverio v8 and <=v7 Browser types - to work in the same codebase. The types are incompatible with - each other, but are compatible with the functions that we use. - Every new feature that we use from the Browser type will need - to be added to the Pick list -*/ -export type WdioBrowser = - | Browser - | Pick< - WebdriverIO.Browser, - | '$$' - | '$' - | 'switchToFrame' - | 'switchToParentFrame' - | 'getWindowHandles' - | 'getWindowHandle' - | 'switchToWindow' - | 'createWindow' - | 'url' - | 'getTimeouts' - | 'setTimeout' - | 'closeWindow' - | 'executeAsync' - | 'execute' - >; - -export type WdioElement = Element | WebdriverIO.Element; + +export interface WdioElement { + isExisting(): Promise; +} + +// Shared methods present in all supported WDIO versions. +// Hand-written rather than Pick because: +// - WebdriverIO.Browser.$$ returns ChainablePromiseArray, whose awaited type +// doesn't expose .concat(), breaking usage in index.ts. +// - Several picked methods carry a `this: Browser` context constraint that +// TypeScript enforces even through our narrower union type. +interface WdioBrowserBase { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + $$(selector: string): PromiseLike; + $(selector: string): PromiseLike; + execute( + script: string | ((...args: unknown[]) => T), + ...args: unknown[] + ): Promise; + executeAsync(script: string, ...args: unknown[]): Promise; + getTimeouts(): Promise<{ pageLoad?: number }>; + setTimeout(options: { pageLoad?: number }): Promise; + url(url: string): Promise; + getWindowHandles(): Promise; + getWindowHandle(): Promise; + createWindow(type: 'tab' | 'window'): Promise<{ handle: string }>; + closeWindow(): Promise; +} + +// WDIO v5–v8: frame/window navigation via the legacy API. +interface WdioBrowserLegacy extends WdioBrowserBase { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + switchToFrame(element: any): Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + switchToParentFrame(): Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + switchToWindow(handle: any): Promise; +} + +// WDIO v9: frame/window navigation via the new API. +// switchFrame / switchWindow are not on the v8 global type, so declared manually. +interface WdioBrowserV9 extends WdioBrowserBase { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + switchFrame(element: any): Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + switchWindow(matcher: any): Promise; +} + +/** + * A real WDIO browser object from any supported version (v5–v9). + * The discriminant is the presence of `switchFrame` (v9) vs `switchToFrame` (v5–v8). + */ +export type WdioBrowser = WdioBrowserLegacy | WdioBrowserV9; export type CallbackFunction = ( error: string | null, diff --git a/packages/webdriverio/src/utils.ts b/packages/webdriverio/src/utils.ts index a9e37df7..95612494 100644 --- a/packages/webdriverio/src/utils.ts +++ b/packages/webdriverio/src/utils.ts @@ -8,7 +8,7 @@ import type { SerialSelectorList, SerialContextObject } from 'axe-core'; -import type { WdioBrowser } from './types'; +import type { WdioBrowser, WdioElement } from './types'; export const FRAME_LOAD_TIMEOUT = 1000; @@ -24,7 +24,7 @@ export const isWebdriverClient = (client: WdioBrowser): boolean => { return false; } - if (typeof client.switchToFrame !== 'function') { + if (!('switchFrame' in client) && !('switchToFrame' in client)) { return false; } @@ -81,6 +81,38 @@ const promisify = (thenable: Promise): Promise => { }); }; +export async function clientSwitchFrame( + client: WdioBrowser, + id: WdioElement | null +): Promise { + if ('switchFrame' in client) { + await client.switchFrame(id); + } else { + await client.switchToFrame(id); + } +} + +export async function clientSwitchParentFrame( + client: WdioBrowser +): Promise { + if ('switchFrame' in client) { + await client.switchFrame(null); + } else { + await client.switchToParentFrame(); + } +} + +export async function clientSwitchWindow( + client: WdioBrowser, + handle: string +): Promise { + if ('switchWindow' in client) { + await client.switchWindow(handle); + } else { + await client.switchToWindow(handle); + } +} + export const axeSourceInject = async ( client: WdioBrowser, axeSource: string @@ -89,7 +121,7 @@ export const axeSourceInject = async ( return promisify( // Had to use executeAsync() because we could not use multiline statements in client.execute() // we were able to return a single boolean in a line but not when assigned to a variable. - (client as WebdriverIO.Browser).executeAsync(` + client.executeAsync(` var callback = arguments[arguments.length - 1]; ${axeSource}; window.axe.configure({ @@ -119,7 +151,7 @@ async function assertFrameReady(client: WdioBrowser): Promise { reject(); }, FRAME_LOAD_TIMEOUT); }); - const executePromise = (client as WebdriverIO.Browser).execute(() => { + const executePromise = client.execute(() => { return document.readyState === 'complete'; }); const readyState = await Promise.race([timeoutPromise, executePromise]); @@ -135,7 +167,7 @@ export const axeRunPartial = ( options?: RunOptions ): Promise => { return promisify( - (client as WebdriverIO.Browser) + client .executeAsync( ` var callback = arguments[arguments.length - 1]; @@ -158,7 +190,7 @@ export const axeGetFrameContext = ( return promisify( // Had to use executeAsync() because we could not use multiline statements in client.execute() // we were able to return a single boolean in a line but not when assigned to a variable. - (client as WebdriverIO.Browser).executeAsync(` + client.executeAsync(` var callback = arguments[arguments.length - 1]; var context = ${JSON.stringify(context)}; var frameContexts = window.axe.utils.getFrameContexts(context); @@ -174,7 +206,7 @@ export const axeRunLegacy = ( config?: Spec ): Promise => { return promisify( - (client as WebdriverIO.Browser) + client .executeAsync( `var callback = arguments[arguments.length - 1]; var context = ${JSON.stringify(context)} || document; @@ -207,7 +239,7 @@ export const axeFinishRun = ( function chunkResults(result: string): Promise { const chunk = JSON.stringify(result.substring(0, sizeLimit)); return promisify( - (client as WebdriverIO.Browser).execute( + client.execute( ` window.partialResults ??= ''; window.partialResults += ${chunk}; @@ -224,7 +256,7 @@ export const axeFinishRun = ( return chunkResults(partialString) .then(() => { return promisify( - (client as WebdriverIO.Browser).executeAsync( + client.executeAsync( `var callback = arguments[arguments.length - 1]; ${axeSource}; window.axe.configure({ @@ -244,7 +276,7 @@ export const axeFinishRun = ( export const configureAllowedOrigins = (client: WdioBrowser): Promise => { return promisify( - (client as WebdriverIO.Browser).execute(` + client.execute(` window.axe.configure({ allowedOrigins: [''] }) `) ); diff --git a/packages/webdriverio/test/axe-webdriverio.spec.ts b/packages/webdriverio/test/axe-webdriverio.spec.ts index 3f4db5e5..6744d8bb 100644 --- a/packages/webdriverio/test/axe-webdriverio.spec.ts +++ b/packages/webdriverio/test/axe-webdriverio.spec.ts @@ -191,6 +191,15 @@ describe('@axe-core/webdriverio', () => { ); }); + it('does not throw when client is valid (v9 API)', () => { + assert.doesNotThrow( + () => + new AxeBuilder({ + client: { execute() {}, switchFrame() {} } + } as any) + ); + }); + it('allows client to be a function (@wdio/globals)', () => { const client = () => {}; client.execute = () => {}; From 18baf4ff2fcb9bebce15cf8d19617eae9206e4a6 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:46:31 -0600 Subject: [PATCH 02/28] fix @wdio/global proxy --- packages/webdriverio/src/utils.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/webdriverio/src/utils.ts b/packages/webdriverio/src/utils.ts index 95612494..32e78d4d 100644 --- a/packages/webdriverio/src/utils.ts +++ b/packages/webdriverio/src/utils.ts @@ -24,7 +24,13 @@ export const isWebdriverClient = (client: WdioBrowser): boolean => { return false; } - if (!('switchFrame' in client) && !('switchToFrame' in client)) { + // @wdio/globals browser uses proxies for the functions so using `'switchToFrame' in client` doesn't work + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const c = client as any; + if ( + typeof c.switchToFrame !== 'function' && + typeof c.switchFrame !== 'function' + ) { return false; } From 3b48901c4864db662096a5d706fc182ace00b3f6 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:53:46 -0600 Subject: [PATCH 03/28] update @wdio/globals test to better reflect its API (proxies) --- packages/webdriverio/src/utils.ts | 1 + packages/webdriverio/test/axe-webdriverio.spec.ts | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/webdriverio/src/utils.ts b/packages/webdriverio/src/utils.ts index 32e78d4d..20eedb56 100644 --- a/packages/webdriverio/src/utils.ts +++ b/packages/webdriverio/src/utils.ts @@ -25,6 +25,7 @@ export const isWebdriverClient = (client: WdioBrowser): boolean => { } // @wdio/globals browser uses proxies for the functions so using `'switchToFrame' in client` doesn't work + // @see https://github.com/webdriverio/webdriverio/blob/main/packages/wdio-globals/src/index.ts // eslint-disable-next-line @typescript-eslint/no-explicit-any const c = client as any; if ( diff --git a/packages/webdriverio/test/axe-webdriverio.spec.ts b/packages/webdriverio/test/axe-webdriverio.spec.ts index 6744d8bb..e78c84b2 100644 --- a/packages/webdriverio/test/axe-webdriverio.spec.ts +++ b/packages/webdriverio/test/axe-webdriverio.spec.ts @@ -201,9 +201,15 @@ describe('@axe-core/webdriverio', () => { }); it('allows client to be a function (@wdio/globals)', () => { - const client = () => {}; - client.execute = () => {}; - client.switchToFrame = () => {}; + const target = { + execute() {}, + switchToFrame() {} + }; + const client = new Proxy(target, { + has() { + return false; + } + }); assert.doesNotThrow(() => new AxeBuilder({ client } as any)); }); From 8309803a425d606ca1f37b1a4d6ecf6a594907d2 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:54:18 -0600 Subject: [PATCH 04/28] test title --- packages/webdriverio/test/axe-webdriverio.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webdriverio/test/axe-webdriverio.spec.ts b/packages/webdriverio/test/axe-webdriverio.spec.ts index e78c84b2..e454e4c7 100644 --- a/packages/webdriverio/test/axe-webdriverio.spec.ts +++ b/packages/webdriverio/test/axe-webdriverio.spec.ts @@ -200,7 +200,7 @@ describe('@axe-core/webdriverio', () => { ); }); - it('allows client to be a function (@wdio/globals)', () => { + it('allows client to be an object with proxies (@wdio/globals)', () => { const target = { execute() {}, switchToFrame() {} From ccb969890a7c7c1794c0de32f899bdc321713f55 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:00:24 -0600 Subject: [PATCH 05/28] preserve comment --- packages/webdriverio/src/types.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/webdriverio/src/types.ts b/packages/webdriverio/src/types.ts index 7d3cdf0c..8389017d 100644 --- a/packages/webdriverio/src/types.ts +++ b/packages/webdriverio/src/types.ts @@ -5,7 +5,8 @@ export interface WdioElement { isExisting(): Promise; } -// Shared methods present in all supported WDIO versions. +// Shared methods present in all supported WDIO versions. Every new feature that +// we use from the Browser type will need to be added to the list // Hand-written rather than Pick because: // - WebdriverIO.Browser.$$ returns ChainablePromiseArray, whose awaited type // doesn't expose .concat(), breaking usage in index.ts. From 2bd4462fa650755edc433854cad076d2ae89fbde Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:06:25 -0600 Subject: [PATCH 06/28] move to wdiov9 --- package-lock.json | 738 +----------------------------- packages/webdriverio/package.json | 2 +- packages/webdriverio/src/types.ts | 10 +- 3 files changed, 5 insertions(+), 745 deletions(-) diff --git a/package-lock.json b/package-lock.json index b7fc38d7..742d3c68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5978,18 +5978,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@sindresorhus/is": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", - "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, "node_modules/@sindresorhus/merge-streams": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", @@ -6043,18 +6031,6 @@ "node": ">=4" } }, - "node_modules/@szmarczak/http-timer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", - "dev": true, - "dependencies": { - "defer-to-connect": "^2.0.1" - }, - "engines": { - "node": ">=14.16" - } - }, "node_modules/@testim/chrome-version": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.4.tgz", @@ -9882,45 +9858,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/cacheable-lookup": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", - "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", - "dev": true, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/cacheable-request": { - "version": "10.2.13", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.13.tgz", - "integrity": "sha512-3SD4rrMu1msNGEtNSt8Od6enwdo//U9s4ykmXfA2TD58kcLkCobtCDiby7kNyj7a/Q7lz/mAesAFI54rTdnvBA==", - "dev": true, - "dependencies": { - "@types/http-cache-semantics": "^4.0.1", - "get-stream": "^6.0.1", - "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.3", - "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/cacheable-request/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/caching-transform": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", @@ -14410,15 +14347,6 @@ "node": ">= 6" } }, - "node_modules/form-data-encoder": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", - "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", - "dev": true, - "engines": { - "node": ">= 14.17" - } - }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -15209,43 +15137,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/got": { - "version": "12.6.1", - "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", - "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", - "dev": true, - "dependencies": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/got/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -15576,31 +15467,6 @@ "node": ">=12" } }, - "node_modules/http2-wrapper": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz", - "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==", - "dev": true, - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.2.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/http2-wrapper/node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -17796,18 +17662,6 @@ "node": ">=0.10.0" } }, - "node_modules/ky": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/ky/-/ky-0.33.3.tgz", - "integrity": "sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/ky?sponsor=1" - } - }, "node_modules/lazystream": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", @@ -18837,18 +18691,6 @@ "get-func-name": "^2.0.0" } }, - "node_modules/lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -19288,18 +19130,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mimic-response": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", - "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -20055,18 +19885,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", - "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/npm-bundled": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", @@ -20953,15 +20771,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-cancelable": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", - "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", - "dev": true, - "engines": { - "node": ">=12.20" - } - }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -23004,21 +22813,6 @@ "node": ">=10" } }, - "node_modules/responselike": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", - "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", - "dev": true, - "dependencies": { - "lowercase-keys": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/resq": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/resq/-/resq-1.11.0.tgz", @@ -27531,542 +27325,12 @@ "rimraf": "^6.0.1", "source-map-support": "^0.5.21", "tsup": "^8.0.1", - "webdriverio": "^8.8.2" + "webdriverio": "^9.22.0" }, "peerDependencies": { "webdriverio": "^5 || ^6 || ^7 || ^8 || ^9" } }, - "packages/webdriverio/node_modules/@wdio/config": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.46.0.tgz", - "integrity": "sha512-WrNPCqm22vuNimGJc8UCc6duEcvOy2foY5I8mv2AUaoTtvCZOfVGRrFnPreypOKVdZChubFCaWrKVNqjgMK5RA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@wdio/logger": "8.38.0", - "@wdio/types": "8.41.0", - "@wdio/utils": "8.46.0", - "decamelize": "^6.0.0", - "deepmerge-ts": "^5.0.0", - "glob": "^10.2.2", - "import-meta-resolve": "^4.0.0" - }, - "engines": { - "node": "^16.13 || >=18" - } - }, - "packages/webdriverio/node_modules/@wdio/logger": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz", - "integrity": "sha512-kcHL86RmNbcQP+Gq/vQUGlArfU6IIcbbnNp32rRIraitomZow+iEoc519rdQmSVusDozMS5DZthkgDdxK+vz6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.1.2", - "loglevel": "^1.6.0", - "loglevel-plugin-prefix": "^0.8.4", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": "^16.13 || >=18" - } - }, - "packages/webdriverio/node_modules/@wdio/protocols": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-8.44.0.tgz", - "integrity": "sha512-Do+AW3xuDUHWkrX++LeMBSrX2yRILlDqunRHPMv4adGFEA45m7r4WP8wGCDb+chrHGhXq5TwB9Ne4J7x1dHGng==", - "dev": true, - "license": "MIT" - }, - "packages/webdriverio/node_modules/@wdio/repl": { - "version": "8.40.3", - "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-8.40.3.tgz", - "integrity": "sha512-mWEiBbaC7CgxvSd2/ozpbZWebnRIc8KRu/J81Hlw/txUWio27S7IpXBlZGVvhEsNzq0+cuxB/8gDkkXvMPbesw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "^22.2.0" - }, - "engines": { - "node": "^16.13 || >=18" - } - }, - "packages/webdriverio/node_modules/@wdio/repl/node_modules/@types/node": { - "version": "22.19.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", - "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "packages/webdriverio/node_modules/@wdio/repl/node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "packages/webdriverio/node_modules/@wdio/types": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.41.0.tgz", - "integrity": "sha512-t4NaNTvJZci3Xv/yUZPH4eTL0hxrVTf5wdwNnYIBrzMnlRDbNefjQ0P7FM7ZjQCLaH92AEH6t/XanUId7Webug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "^22.2.0" - }, - "engines": { - "node": "^16.13 || >=18" - } - }, - "packages/webdriverio/node_modules/@wdio/types/node_modules/@types/node": { - "version": "22.19.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", - "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "packages/webdriverio/node_modules/@wdio/types/node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "packages/webdriverio/node_modules/@wdio/utils": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.46.0.tgz", - "integrity": "sha512-C94kJjZhEfPUNbOA69BQr1SgziQYgjNXK8S1GJXQKuwxN/24PQkYCzeBqXstfxyTXyOwoQCcEZAQ/qJccboufQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@puppeteer/browsers": "^1.6.0", - "@wdio/logger": "8.38.0", - "@wdio/types": "8.41.0", - "decamelize": "^6.0.0", - "deepmerge-ts": "^5.1.0", - "edgedriver": "^5.5.0", - "geckodriver": "~4.2.0", - "get-port": "^7.0.0", - "import-meta-resolve": "^4.0.0", - "locate-app": "^2.1.0", - "safaridriver": "^0.1.0", - "split2": "^4.2.0", - "wait-port": "^1.0.4" - }, - "engines": { - "node": "^16.13 || >=18" - } - }, - "packages/webdriverio/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "packages/webdriverio/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "packages/webdriverio/node_modules/chalk": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz", - "integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "packages/webdriverio/node_modules/chromium-bidi": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.5.8.tgz", - "integrity": "sha512-blqh+1cEQbHBKmok3rVJkBlBxt9beKBgOsxbFgs7UJcoVbbeZ+K7+6liAsjgpc8l1Xd55cQUy14fXZdGSb4zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "mitt": "3.0.1", - "urlpattern-polyfill": "10.0.0" - }, - "peerDependencies": { - "devtools-protocol": "*" - } - }, - "packages/webdriverio/node_modules/decamelize": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", - "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/webdriverio/node_modules/devtools-protocol": { - "version": "0.0.1400418", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1400418.tgz", - "integrity": "sha512-U8j75zDOXF8IP3o0Cgb7K4tFA9uUHEOru2Wx64+EUqL4LNOh9dRe1i8WKR1k3mSpjcCe3aIkTDvEwq0YkI4hfw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "packages/webdriverio/node_modules/geckodriver": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-4.2.1.tgz", - "integrity": "sha512-4m/CRk0OI8MaANRuFIahvOxYTSjlNAO2p9JmE14zxueknq6cdtB5M9UGRQ8R9aMV0bLGNVHHDnDXmoXdOwJfWg==", - "dev": true, - "hasInstallScript": true, - "license": "MPL-2.0", - "dependencies": { - "@wdio/logger": "^8.11.0", - "decamelize": "^6.0.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.1", - "tar-fs": "^3.0.4", - "unzipper": "^0.10.14", - "which": "^4.0.0" - }, - "bin": { - "geckodriver": "bin/geckodriver.js" - }, - "engines": { - "node": "^16.13 || >=18 || >=20" - } - }, - "packages/webdriverio/node_modules/get-port": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", - "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/webdriverio/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "packages/webdriverio/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/webdriverio/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "packages/webdriverio/node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "packages/webdriverio/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "packages/webdriverio/node_modules/puppeteer-core": { - "version": "21.11.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-21.11.0.tgz", - "integrity": "sha512-ArbnyA3U5SGHokEvkfWjW+O8hOxV1RSJxOgriX/3A4xZRqixt9ZFHD0yPgZQF05Qj0oAqi8H/7stDorjoHY90Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "1.9.1", - "chromium-bidi": "0.5.8", - "cross-fetch": "4.0.0", - "debug": "4.3.4", - "devtools-protocol": "0.0.1232444", - "ws": "8.16.0" - }, - "engines": { - "node": ">=16.13.2" - } - }, - "packages/webdriverio/node_modules/puppeteer-core/node_modules/devtools-protocol": { - "version": "0.0.1232444", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1232444.tgz", - "integrity": "sha512-pM27vqEfxSxRkTMnF+XCmxSEb6duO5R+t8A9DEEJgy4Wz2RVanje2mmj99B6A3zv2r/qGfYlOvYznUhuokizmg==", - "dev": true, - "license": "BSD-3-Clause" - }, - "packages/webdriverio/node_modules/serialize-error": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.3.tgz", - "integrity": "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^2.12.2" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/webdriverio/node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "packages/webdriverio/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "packages/webdriverio/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/webdriverio/node_modules/webdriver": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.46.0.tgz", - "integrity": "sha512-ucb+ow6QHTBBDAdpV1AAKPY+un2cv23QU/rsSJBmuDZi8lZc5NluWz16qVVbdD1+Hn45PXfpxQcBaAkavStORA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "^22.2.0", - "@types/ws": "^8.5.3", - "@wdio/config": "8.46.0", - "@wdio/logger": "8.38.0", - "@wdio/protocols": "8.44.0", - "@wdio/types": "8.41.0", - "@wdio/utils": "8.46.0", - "deepmerge-ts": "^5.1.0", - "got": "^12.6.1", - "ky": "^0.33.0", - "ws": "^8.8.0" - }, - "engines": { - "node": "^16.13 || >=18" - } - }, - "packages/webdriverio/node_modules/webdriver/node_modules/@types/node": { - "version": "22.19.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", - "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "packages/webdriverio/node_modules/webdriver/node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "packages/webdriverio/node_modules/webdriverio": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.46.0.tgz", - "integrity": "sha512-SyrSVpygEdPzvgpapVZRQCy8XIOecadp56bPQewpfSfo9ypB6wdOUkx13NBu2ANDlUAtJX7KaLJpTtywVHNlVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "^22.2.0", - "@wdio/config": "8.46.0", - "@wdio/logger": "8.38.0", - "@wdio/protocols": "8.44.0", - "@wdio/repl": "8.40.3", - "@wdio/types": "8.41.0", - "@wdio/utils": "8.46.0", - "archiver": "^7.0.0", - "aria-query": "^5.0.0", - "css-shorthand-properties": "^1.1.1", - "css-value": "^0.0.1", - "devtools-protocol": "^0.0.1400418", - "grapheme-splitter": "^1.0.2", - "import-meta-resolve": "^4.0.0", - "is-plain-obj": "^4.1.0", - "jszip": "^3.10.1", - "lodash.clonedeep": "^4.5.0", - "lodash.zip": "^4.2.0", - "minimatch": "^9.0.0", - "puppeteer-core": "^21.11.0", - "query-selector-shadow-dom": "^1.0.0", - "resq": "^1.9.1", - "rgb2hex": "0.2.5", - "serialize-error": "^11.0.1", - "webdriver": "8.46.0" - }, - "engines": { - "node": "^16.13 || >=18" - }, - "peerDependencies": { - "devtools": "^8.14.0" - }, - "peerDependenciesMeta": { - "devtools": { - "optional": true - } - } - }, - "packages/webdriverio/node_modules/webdriverio/node_modules/@types/node": { - "version": "22.19.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", - "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "packages/webdriverio/node_modules/webdriverio/node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "packages/webdriverio/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^16.13.0 || >=18.0.0" - } - }, - "packages/webdriverio/node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "packages/webdriverjs": { "name": "@axe-core/webdriverjs", "version": "4.11.1", diff --git a/packages/webdriverio/package.json b/packages/webdriverio/package.json index 0d6a05ab..df3fab2e 100644 --- a/packages/webdriverio/package.json +++ b/packages/webdriverio/package.json @@ -70,7 +70,7 @@ "rimraf": "^6.0.1", "source-map-support": "^0.5.21", "tsup": "^8.0.1", - "webdriverio": "^8.8.2" + "webdriverio": "^9.22.0" }, "peerDependencies": { "webdriverio": "^5 || ^6 || ^7 || ^8 || ^9" diff --git a/packages/webdriverio/src/types.ts b/packages/webdriverio/src/types.ts index 8389017d..f99f017d 100644 --- a/packages/webdriverio/src/types.ts +++ b/packages/webdriverio/src/types.ts @@ -6,12 +6,12 @@ export interface WdioElement { } // Shared methods present in all supported WDIO versions. Every new feature that -// we use from the Browser type will need to be added to the list +// we use from the Browser type will need to be added to the list. // Hand-written rather than Pick because: // - WebdriverIO.Browser.$$ returns ChainablePromiseArray, whose awaited type // doesn't expose .concat(), breaking usage in index.ts. // - Several picked methods carry a `this: Browser` context constraint that -// TypeScript enforces even through our narrower union type. +// TypeScript enforces even through our narrower union type interface WdioBrowserBase { // eslint-disable-next-line @typescript-eslint/no-explicit-any $$(selector: string): PromiseLike; @@ -41,7 +41,6 @@ interface WdioBrowserLegacy extends WdioBrowserBase { } // WDIO v9: frame/window navigation via the new API. -// switchFrame / switchWindow are not on the v8 global type, so declared manually. interface WdioBrowserV9 extends WdioBrowserBase { // eslint-disable-next-line @typescript-eslint/no-explicit-any switchFrame(element: any): Promise; @@ -49,10 +48,7 @@ interface WdioBrowserV9 extends WdioBrowserBase { switchWindow(matcher: any): Promise; } -/** - * A real WDIO browser object from any supported version (v5–v9). - * The discriminant is the presence of `switchFrame` (v9) vs `switchToFrame` (v5–v8). - */ +// A WDIO browser object from any supported version (v5–v9). export type WdioBrowser = WdioBrowserLegacy | WdioBrowserV9; export type CallbackFunction = ( From d3b32f71bc3a557158c9d0cbcc14471e6b9d7b2c Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:09:12 -0600 Subject: [PATCH 07/28] support @wdio/globals proxy in wrapper functions --- packages/webdriverio/src/utils.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/webdriverio/src/utils.ts b/packages/webdriverio/src/utils.ts index 20eedb56..bda55677 100644 --- a/packages/webdriverio/src/utils.ts +++ b/packages/webdriverio/src/utils.ts @@ -92,20 +92,24 @@ export async function clientSwitchFrame( client: WdioBrowser, id: WdioElement | null ): Promise { - if ('switchFrame' in client) { - await client.switchFrame(id); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const c = client as any; + if (typeof c.switchFrame === 'function') { + await c.switchFrame(id); } else { - await client.switchToFrame(id); + await c.switchToFrame(id); } } export async function clientSwitchParentFrame( client: WdioBrowser ): Promise { - if ('switchFrame' in client) { - await client.switchFrame(null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const c = client as any; + if (typeof c.switchFrame === 'function') { + await c.switchFrame(null); } else { - await client.switchToParentFrame(); + await c.switchToParentFrame(); } } @@ -113,10 +117,12 @@ export async function clientSwitchWindow( client: WdioBrowser, handle: string ): Promise { - if ('switchWindow' in client) { - await client.switchWindow(handle); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const c = client as any; + if (typeof c.switchWindow === 'function') { + await c.switchWindow(handle); } else { - await client.switchToWindow(handle); + await c.switchToWindow(handle); } } From 5e4a0d5fd92aecb404f63fc986ecdfd3870c06c6 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:35:54 -0600 Subject: [PATCH 08/28] loosen type --- packages/webdriverio/src/types.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/webdriverio/src/types.ts b/packages/webdriverio/src/types.ts index f99f017d..2055c41c 100644 --- a/packages/webdriverio/src/types.ts +++ b/packages/webdriverio/src/types.ts @@ -14,8 +14,9 @@ export interface WdioElement { // TypeScript enforces even through our narrower union type interface WdioBrowserBase { // eslint-disable-next-line @typescript-eslint/no-explicit-any - $$(selector: string): PromiseLike; - $(selector: string): PromiseLike; + $$(selector: string): any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + $(selector: string): any; execute( script: string | ((...args: unknown[]) => T), ...args: unknown[] From ebc9fee745e9bd9c5f58fa0ebc9b9344206b5b90 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Mon, 23 Mar 2026 08:57:06 -0600 Subject: [PATCH 09/28] fix types --- packages/webdriverio/src/types.ts | 13 ++++++++----- packages/webdriverio/test/axe-webdriverio.spec.ts | 6 +++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/webdriverio/src/types.ts b/packages/webdriverio/src/types.ts index 2055c41c..428cf9da 100644 --- a/packages/webdriverio/src/types.ts +++ b/packages/webdriverio/src/types.ts @@ -17,14 +17,17 @@ interface WdioBrowserBase { $$(selector: string): any; // eslint-disable-next-line @typescript-eslint/no-explicit-any $(selector: string): any; - execute( - script: string | ((...args: unknown[]) => T), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + execute( + script: string | ((...args: unknown[]) => unknown), ...args: unknown[] - ): Promise; - executeAsync(script: string, ...args: unknown[]): Promise; + ): Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + executeAsync(script: string, ...args: unknown[]): Promise; getTimeouts(): Promise<{ pageLoad?: number }>; setTimeout(options: { pageLoad?: number }): Promise; - url(url: string): Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + url(url: string): Promise; getWindowHandles(): Promise; getWindowHandle(): Promise; createWindow(type: 'tab' | 'window'): Promise<{ handle: string }>; diff --git a/packages/webdriverio/test/axe-webdriverio.spec.ts b/packages/webdriverio/test/axe-webdriverio.spec.ts index e454e4c7..6c061419 100644 --- a/packages/webdriverio/test/axe-webdriverio.spec.ts +++ b/packages/webdriverio/test/axe-webdriverio.spec.ts @@ -114,7 +114,7 @@ describe('@axe-core/webdriverio', () => { // this removes the unnecessary trailing forward slash addr = (await listen(server)).toString().replace(/\/$/, ''); - const options: webdriverio.RemoteOptions = { + const options: Parameters[0] = { path: '/', automationProtocol: protocol, capabilities: { @@ -147,14 +147,14 @@ describe('@axe-core/webdriverio', () => { describe('AxeBuilder', () => { if (protocol === 'devtools') { it('check to make sure that client is running devtools protocol', () => { - assert.isTrue(client.isDevTools); + assert.isTrue((client as any).isDevTools); }); } if (protocol === 'webdriver') { it('check to make sure that client is running webdriver protocol', () => { // there is no `isWebdriver` option - assert.isUndefined(client.isDevTools); + assert.isUndefined((client as any).isDevTools); }); } From 0a38256cbb5f082ea6876f954c7a38c2b69f54f9 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Tue, 24 Mar 2026 08:50:24 -0600 Subject: [PATCH 10/28] fix lint issue --- packages/webdriverio/src/types.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/webdriverio/src/types.ts b/packages/webdriverio/src/types.ts index 428cf9da..92e79b38 100644 --- a/packages/webdriverio/src/types.ts +++ b/packages/webdriverio/src/types.ts @@ -17,10 +17,11 @@ interface WdioBrowserBase { $$(selector: string): any; // eslint-disable-next-line @typescript-eslint/no-explicit-any $(selector: string): any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any + execute( script: string | ((...args: unknown[]) => unknown), ...args: unknown[] + // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise; // eslint-disable-next-line @typescript-eslint/no-explicit-any executeAsync(script: string, ...args: unknown[]): Promise; From a675070d81f7679272d74792162f6e15f0fb0a5a Mon Sep 17 00:00:00 2001 From: Scott Ries Date: Thu, 2 Apr 2026 15:49:06 -0400 Subject: [PATCH 11/28] fix(wdio): fix parent frame switching and skip devtools tests on v9 In WDIO v9 WebDriver Classic (non-BiDi), switchFrame(null) calls the WebDriver "Switch To Frame" command with id=null which switches to the top-level frame rather than the immediate parent. This broke nested frame injection and runPartialRecursive traversal, causing timeouts and wrong selector paths in the results. Fix clientSwitchParentFrame to prefer switchToParentFrame() (available in both v8 and v9 via @wdio/protocols) which correctly targets the immediate parent frame. Also skip the devtools protocol test suite on WDIO v9+ since automationProtocol: 'devtools' was removed in v9. --- packages/webdriverio/src/utils.ts | 10 +++++++--- packages/webdriverio/test/axe-webdriverio.spec.ts | 14 +++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/webdriverio/src/utils.ts b/packages/webdriverio/src/utils.ts index bda55677..51a63b09 100644 --- a/packages/webdriverio/src/utils.ts +++ b/packages/webdriverio/src/utils.ts @@ -106,10 +106,14 @@ export async function clientSwitchParentFrame( ): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any const c = client as any; - if (typeof c.switchFrame === 'function') { - await c.switchFrame(null); - } else { + // Prefer switchToParentFrame (v8 and v9 via @wdio/protocols) because it + // correctly switches to the immediate parent frame. In WDIO v9 WebDriver + // Classic (non-BiDi), switchFrame(null) calls switchToFrame(null) which + // switches to the top-level frame instead of the parent frame. + if (typeof c.switchToParentFrame === 'function') { await c.switchToParentFrame(); + } else if (typeof c.switchFrame === 'function') { + await c.switchFrame(null); } } diff --git a/packages/webdriverio/test/axe-webdriverio.spec.ts b/packages/webdriverio/test/axe-webdriverio.spec.ts index 6c061419..c65bf6ce 100644 --- a/packages/webdriverio/test/axe-webdriverio.spec.ts +++ b/packages/webdriverio/test/axe-webdriverio.spec.ts @@ -22,6 +22,12 @@ const BDM_CACHE_DIR = path.resolve(HOME_DIR, '.browser-driver-manager'); config({ path: path.resolve(BDM_CACHE_DIR, '.env') }); +// devtools protocol was removed in WDIO v9 +const wdioMajorVersion = parseInt( + require('webdriverio/package.json').version, + 10 +); + const connectToChromeDriver = (port: number): Promise => { let socket: net.Socket; return new Promise((resolve, reject) => { @@ -83,7 +89,13 @@ describe('@axe-core/webdriverio', () => { }); } - describe(`WebdriverIO Async (${protocol} protocol)`, () => { + describe(`WebdriverIO Async (${protocol} protocol)`, function () { + before(function () { + if (protocol === 'devtools' && wdioMajorVersion >= 9) { + this.skip(); + } + }); + let server: Server; let addr: string; let client: WebdriverIO.Browser; From 9e654117efe34b36e172f45c5e8f29426eca3caa Mon Sep 17 00:00:00 2001 From: Scott Ries Date: Thu, 2 Apr 2026 15:59:07 -0400 Subject: [PATCH 12/28] fix(wdio): resolve webdriverio version without relying on package.json export --- .../webdriverio/test/axe-webdriverio.spec.ts | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/webdriverio/test/axe-webdriverio.spec.ts b/packages/webdriverio/test/axe-webdriverio.spec.ts index c65bf6ce..d13662da 100644 --- a/packages/webdriverio/test/axe-webdriverio.spec.ts +++ b/packages/webdriverio/test/axe-webdriverio.spec.ts @@ -22,11 +22,25 @@ const BDM_CACHE_DIR = path.resolve(HOME_DIR, '.browser-driver-manager'); config({ path: path.resolve(BDM_CACHE_DIR, '.env') }); -// devtools protocol was removed in WDIO v9 -const wdioMajorVersion = parseInt( - require('webdriverio/package.json').version, - 10 -); +// devtools protocol was removed in WDIO v9. +// require('webdriverio/package.json') fails when the package uses an exports +// field that doesn't include ./package.json, so walk up from the resolved +// entry point instead (fs.readFileSync bypasses the exports restriction). +const wdioMajorVersion = (() => { + let dir = path.dirname(require.resolve('webdriverio')); + while (dir !== path.dirname(dir)) { + try { + const pkg = JSON.parse( + fs.readFileSync(path.join(dir, 'package.json'), 'utf-8') + ); + if (pkg.name === 'webdriverio') return parseInt(pkg.version, 10); + } catch { + /* continue walking */ + } + dir = path.dirname(dir); + } + return 0; +})(); const connectToChromeDriver = (port: number): Promise => { let socket: net.Socket; From 1e3caa4c364956ca12320fbeee90178db32e1b66 Mon Sep 17 00:00:00 2001 From: Scott Ries Date: Fri, 3 Apr 2026 06:46:41 -0400 Subject: [PATCH 13/28] fix(wdio): fix BiDi mode frame navigation for WDIO v9 Replace clientSwitchParentFrame with getWindowHandles+switchToWindow+re-traverse to avoid the BiDi race condition where switchToParentFrame synchronously resets #currentContext before the async parent lookup resolves, causing subsequent BiDi calls to run in the wrong browsing context. Also fix clientSwitchWindow to prefer switchToWindow (handle-based) over switchWindow (pattern match by title/URL/name in v8). --- packages/webdriverio/src/index.ts | 30 ++++++++++++++++++++++-------- packages/webdriverio/src/utils.ts | 9 ++++++--- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/packages/webdriverio/src/index.ts b/packages/webdriverio/src/index.ts index a83f9d79..aaae8b8f 100644 --- a/packages/webdriverio/src/index.ts +++ b/packages/webdriverio/src/index.ts @@ -12,7 +12,6 @@ import { axeRunLegacy, configureAllowedOrigins, clientSwitchFrame, - clientSwitchParentFrame, clientSwitchWindow, FRAME_LOAD_TIMEOUT } from './utils'; @@ -233,7 +232,16 @@ export default class AxeBuilder { continue; } await this.inject(iframe); - await clientSwitchParentFrame(this.client); + // After inject(iframe) returns we are still inside iframe. Navigate + // back to top-level via switchToWindow (which correctly sets the BiDi + // browsing context from getWindowHandles(), avoiding the classic-handle + // mismatch that switchFrame(null) causes in BiDi mode), then re-enter + // browsingContext if needed. + const [topWindow] = await this.client.getWindowHandles(); + await clientSwitchWindow(this.client, topWindow); + if (browsingContext !== null) { + await clientSwitchFrame(this.client, browsingContext); + } } catch (error) { logOrRethrowError(error); } @@ -313,11 +321,7 @@ export default class AxeBuilder { private async setBrowsingContext( id: WdioElement | null = null ): Promise { - if (id) { - await clientSwitchFrame(this.client, id); - } else { - await clientSwitchParentFrame(this.client); - } + await clientSwitchFrame(this.client, id); } /** @@ -357,7 +361,17 @@ export default class AxeBuilder { partials.push(null); } } - await clientSwitchParentFrame(this.client); + // Navigate back to the parent context by switching to the top-level window + // (via getWindowHandles + switchToWindow, which correctly sets the BiDi + // context) then re-traversing the frame stack up to (but not including) + // the last frame. This avoids the WDIO v9 BiDi race condition where + // switchToParentFrame synchronously resets #currentContext before the async + // parent lookup resolves, causing subsequent BiDi calls to run in wrong context. + const [topWindow] = await this.client.getWindowHandles(); + await clientSwitchWindow(this.client, topWindow); + for (let i = 0; i < frameStack.length - 1; i++) { + await clientSwitchFrame(this.client, frameStack[i]); + } return partials; } diff --git a/packages/webdriverio/src/utils.ts b/packages/webdriverio/src/utils.ts index 51a63b09..45e147d5 100644 --- a/packages/webdriverio/src/utils.ts +++ b/packages/webdriverio/src/utils.ts @@ -123,10 +123,13 @@ export async function clientSwitchWindow( ): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any const c = client as any; - if (typeof c.switchWindow === 'function') { - await c.switchWindow(handle); - } else { + // Prefer switchToWindow (handle-based) over switchWindow (pattern match). + // switchWindow matches by title/URL/name in v8, so passing a window handle + // string fails with "No window found". switchToWindow takes a handle directly. + if (typeof c.switchToWindow === 'function') { await c.switchToWindow(handle); + } else if (typeof c.switchWindow === 'function') { + await c.switchWindow(handle); } } From 229f04d353105baffae0a4d39ec9a0f59a26e1f7 Mon Sep 17 00:00:00 2001 From: Scott Ries Date: Fri, 3 Apr 2026 13:23:53 -0400 Subject: [PATCH 14/28] fix(webdriverio): resolve BiDi frame context issues in WDIO v9 Two fixes for WDIO v9 WebDriver BiDi mode: 1. inject() re-entry: capture the BiDi context ID when entering each frame (returned by switchFrame), then use that string ID for re-entry after deep injection instead of the original element reference. In BiDi mode, Chrome may assign new document IDs to intermediate frame contexts after switchFrame(null) from deep nesting. An element's SharedId encodes the document ID at query time; if that ID has since changed, Chrome rejects it with "no such node - SharedId belongs to different document". Passing a context ID string instead causes WDIO to re-query fresh element references via browsingContextLocateNodes, bypassing the stale-ID issue. 2. assertFrameReady(): add document.URL !== 'about:blank' to the readiness check. In BiDi mode, script.callFunction can execute in cross-origin frames (BiDi bypasses same-origin restrictions). A lazy-loaded cross-origin iframe that hasn't fetched its content yet has readyState 'complete' on its blank document, so the old check passed and the frame was never reported as frame-tested incomplete. Classic WebDriver throws on cross-origin execution, which triggered the correct frame-tested path. Checking for about:blank replicates that behavior. --- packages/webdriverio/src/index.ts | 55 +++++++++++++++++++------------ packages/webdriverio/src/utils.ts | 23 ++++++++++--- 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/packages/webdriverio/src/index.ts b/packages/webdriverio/src/index.ts index aaae8b8f..47ceb2a1 100644 --- a/packages/webdriverio/src/index.ts +++ b/packages/webdriverio/src/index.ts @@ -207,9 +207,23 @@ export default class AxeBuilder { * Injects `axe-core` into all frames. */ private async inject( - browsingContext: WdioElement | null = null + browsingContext: WdioElement | null = null, + browsingContextId: string | null = null ): Promise { - await this.setBrowsingContext(browsingContext); + // Navigate to the target browsing context and capture its BiDi context ID. + // In WDIO v9 BiDi mode, switchFrame returns the browsing context ID string, + // which we use later to safely re-enter this frame after deep injection. + // In Classic WebDriver mode, switchFrame returns undefined and we fall back + // to re-entering via the original element reference. + if (browsingContext !== null) { + const result = await clientSwitchFrame(this.client, browsingContext); + if (typeof result === 'string') { + browsingContextId = result; + } + } else { + await clientSwitchFrame(this.client, null); + } + const runPartialSupported = await axeSourceInject( this.client, this.axeSource @@ -228,18 +242,27 @@ export default class AxeBuilder { for (const iframe of iframes) { try { - if (!(await iframe.isExisting())) { + const exists = await iframe.isExisting(); + if (!exists) { continue; } await this.inject(iframe); - // After inject(iframe) returns we are still inside iframe. Navigate - // back to top-level via switchToWindow (which correctly sets the BiDi - // browsing context from getWindowHandles(), avoiding the classic-handle - // mismatch that switchFrame(null) causes in BiDi mode), then re-enter - // browsingContext if needed. - const [topWindow] = await this.client.getWindowHandles(); - await clientSwitchWindow(this.client, topWindow); - if (browsingContext !== null) { + // After injecting into iframe (and its descendants), navigate back to + // this level. switchFrame(null) reliably resets to the top-level context. + // Then re-enter this frame using its BiDi context ID (WDIO v9 BiDi) or + // its element reference (Classic WebDriver). + // + // We use the context ID rather than the element reference because in WDIO + // v9 BiDi mode, Chrome may assign new document IDs to intermediate frame + // contexts after a deep switchFrame(null). An element's SharedId encodes + // the document ID at query time; if the document ID has since changed, the + // SharedId is stale and Chrome rejects it with "no such node". Passing a + // context ID string instead causes WDIO to re-query fresh element + // references via browsingContextLocateNodes, bypassing the stale-ID issue. + await clientSwitchFrame(this.client, null); + if (browsingContextId !== null) { + await clientSwitchFrame(this.client, browsingContextId); + } else if (browsingContext !== null) { await clientSwitchFrame(this.client, browsingContext); } } catch (error) { @@ -314,16 +337,6 @@ export default class AxeBuilder { return selector; } - /** - * Set browsing context - when `null` sets top level page as context - * - https://webdriver.io/docs/api/webdriver.html#switchtoframe - */ - private async setBrowsingContext( - id: WdioElement | null = null - ): Promise { - await clientSwitchFrame(this.client, id); - } - /** * Get partial results from the current context and its child frames * @param {ContextObject} context diff --git a/packages/webdriverio/src/utils.ts b/packages/webdriverio/src/utils.ts index 45e147d5..bdc52b3d 100644 --- a/packages/webdriverio/src/utils.ts +++ b/packages/webdriverio/src/utils.ts @@ -90,15 +90,19 @@ const promisify = (thenable: Promise): Promise => { export async function clientSwitchFrame( client: WdioBrowser, - id: WdioElement | null -): Promise { + id: WdioElement | null | string +): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any const c = client as any; if (typeof c.switchFrame === 'function') { - await c.switchFrame(id); - } else { + // WDIO v9 BiDi: switchFrame accepts elements, null, or context ID strings. + // It returns the BiDi browsing context ID, which we capture for safe re-entry. + return (await c.switchFrame(id)) as string; + } else if (typeof id !== 'string') { + // Classic WebDriver (WDIO v5–v8): no string context ID support. await c.switchToFrame(id); } + return undefined; } export async function clientSwitchParentFrame( @@ -172,7 +176,16 @@ async function assertFrameReady(client: WdioBrowser): Promise { }, FRAME_LOAD_TIMEOUT); }); const executePromise = client.execute(() => { - return document.readyState === 'complete'; + // In WDIO v9 BiDi mode, executing scripts in a lazy-loaded cross-origin + // iframe succeeds even when the frame hasn't loaded its content yet + // (BiDi bypasses same-origin restrictions). The frame's document will be + // about:blank until the browser actually fetches the remote URL. + // Classic WebDriver throws a cross-origin error in this case, which is + // caught and treated as an untestable frame. We replicate that behavior + // here by also checking that the document isn't still at about:blank. + return ( + document.readyState === 'complete' && document.URL !== 'about:blank' + ); }); const readyState = await Promise.race([timeoutPromise, executePromise]); assert(readyState); From 17e4cd556dececf262a125680763371be002b2d9 Mon Sep 17 00:00:00 2001 From: Scott Ries Date: Fri, 3 Apr 2026 16:45:58 -0400 Subject: [PATCH 15/28] fix(webdriverio): update esmTest to use webdriver protocol for WDIO v9 automationProtocol: 'devtools' was removed in WDIO v9. Switch the ESM export integration test to use webdriver protocol with ChromeDriver, matching the approach used in the main test suite. --- packages/webdriverio/test/esmTest.mjs | 102 +++++++++++++++++++++----- 1 file changed, 84 insertions(+), 18 deletions(-) diff --git a/packages/webdriverio/test/esmTest.mjs b/packages/webdriverio/test/esmTest.mjs index e4767b6c..d291c8e9 100644 --- a/packages/webdriverio/test/esmTest.mjs +++ b/packages/webdriverio/test/esmTest.mjs @@ -5,6 +5,10 @@ import * as webdriverio from 'webdriverio'; import { pathToFileURL } from 'url'; import { join } from 'path'; import { fixturesPath } from 'axe-test-fixtures'; +import { spawn } from 'child_process'; +import net from 'net'; +import os from 'os'; +import { readFileSync } from 'fs'; assert(typeof defaultExport === 'function', 'default export is not a function'); assert(typeof AxeBuilder === 'function', 'named export is not a function'); @@ -13,25 +17,87 @@ assert( 'default and named export are not the same' ); +// Load browser-driver-manager env vars (same approach as axe-webdriverio.spec.ts) +const BDM_ENV_PATH = join(os.homedir(), '.browser-driver-manager', '.env'); +try { + const envContent = readFileSync(BDM_ENV_PATH, 'utf-8'); + for (const line of envContent.split('\n')) { + const match = line.match(/^([^=\s]+)=(.*)$/); + if (match) { + // Strip surrounding quotes that some .env writers add + const value = match[2].replace(/^(['"])(.*)\1$/, '$2'); + process.env[match[1]] = value; + } + } +} catch { + // .env file not found; env vars may already be set in the environment +} + +const connectToChromeDriver = port => { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + socket.destroy(); + reject(new Error('Unable to connect to ChromeDriver')); + }, 1000); + const socket = net.createConnection({ host: 'localhost', port }, () => { + clearTimeout(timer); + socket.destroy(); + resolve(); + }); + socket.once('error', err => { + clearTimeout(timer); + socket.destroy(); + reject(err); + }); + }); +}; + async function integrationTest() { - const path = join(fixturesPath, 'index.html'); - - const options = { - automationProtocol: 'devtools', - path: '/', - capabilities: { - browserName: 'chrome', - 'goog:chromeOptions': { - args: ['--headless'] - } - }, - logLevel: 'error' - }; - const client = await webdriverio.remote(options); - await client.url(pathToFileURL(path).toString()); - - const results = await new defaultExport({ client }).analyze(); - assert(results.violations.length > 0, 'could not find violations'); + const port = 9516; + + assert( + process.env.CHROMEDRIVER_TEST_PATH, + 'CHROMEDRIVER_TEST_PATH is not set. Run `npx browser-driver-manager install chrome`' + ); + assert( + process.env.CHROME_TEST_PATH, + 'CHROME_TEST_PATH is not set. Run `npx browser-driver-manager install chrome`' + ); + + const chromedriverProcess = spawn(process.env.CHROMEDRIVER_TEST_PATH, [ + `--port=${port}` + ]); + + await new Promise(r => setTimeout(r, 500)); + await connectToChromeDriver(port); + + let client; + try { + const options = { + path: '/', + hostname: 'localhost', + port, + capabilities: { + browserName: 'chrome', + 'goog:chromeOptions': { + args: ['--headless', '--no-sandbox'], + binary: process.env.CHROME_TEST_PATH + } + }, + logLevel: 'error' + }; + + client = await webdriverio.remote(options); + await client.url(pathToFileURL(join(fixturesPath, 'index.html')).toString()); + + const results = await new defaultExport({ client }).analyze(); + assert(results.violations.length > 0, 'could not find violations'); + } finally { + await client?.deleteSession(); + chromedriverProcess.kill(); + } + process.exit(0); } + integrationTest(); From 94a20dbb89d5b58ed050be802e9d44b81c0f5d17 Mon Sep 17 00:00:00 2001 From: Scott Ries Date: Mon, 6 Apr 2026 13:29:09 -0400 Subject: [PATCH 16/28] store correct topWindow --- packages/webdriverio/src/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/webdriverio/src/index.ts b/packages/webdriverio/src/index.ts index 47ceb2a1..a5f6ce9d 100644 --- a/packages/webdriverio/src/index.ts +++ b/packages/webdriverio/src/index.ts @@ -346,6 +346,7 @@ export default class AxeBuilder { context: SerialContextObject, frameStack: WdioElement[] = [] ): Promise { + const topWindow = await this.client.getWindowHandle(); const frameContexts = await axeGetFrameContext(this.client, context); const partials: PartialResults = [ await axeRunPartial(this.client, context, this.option) @@ -364,7 +365,6 @@ export default class AxeBuilder { ])) ); } catch { - const [topWindow] = await this.client.getWindowHandles(); await clientSwitchWindow(this.client, topWindow); for (const frameElm of frameStack) { @@ -380,7 +380,6 @@ export default class AxeBuilder { // the last frame. This avoids the WDIO v9 BiDi race condition where // switchToParentFrame synchronously resets #currentContext before the async // parent lookup resolves, causing subsequent BiDi calls to run in wrong context. - const [topWindow] = await this.client.getWindowHandles(); await clientSwitchWindow(this.client, topWindow); for (let i = 0; i < frameStack.length - 1; i++) { await clientSwitchFrame(this.client, frameStack[i]); From 6b37db4ea2def870cf9f3c8d696eef9297703c66 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:32:16 +0000 Subject: [PATCH 17/28] fix(webdriverio): use dynamic ephemeral port in esmTest and inherit stdio Agent-Logs-Url: https://github.com/dequelabs/axe-core-npm/sessions/208815b7-6df7-44ba-9401-9ccbfc3e410d Co-authored-by: scottmries <1245800+scottmries@users.noreply.github.com> --- packages/webdriverio/test/esmTest.mjs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/webdriverio/test/esmTest.mjs b/packages/webdriverio/test/esmTest.mjs index d291c8e9..c2f41142 100644 --- a/packages/webdriverio/test/esmTest.mjs +++ b/packages/webdriverio/test/esmTest.mjs @@ -52,8 +52,25 @@ const connectToChromeDriver = port => { }); }; +const getFreePort = () => { + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.listen(0, '127.0.0.1', () => { + const { port } = server.address(); + server.close(err => { + if (err) { + reject(err); + } else { + resolve(port); + } + }); + }); + server.once('error', reject); + }); +}; + async function integrationTest() { - const port = 9516; + const port = await getFreePort(); assert( process.env.CHROMEDRIVER_TEST_PATH, @@ -66,7 +83,7 @@ async function integrationTest() { const chromedriverProcess = spawn(process.env.CHROMEDRIVER_TEST_PATH, [ `--port=${port}` - ]); + ], { stdio: 'inherit' }); await new Promise(r => setTimeout(r, 500)); await connectToChromeDriver(port); From 27d637316421c73ec9300bb71a7504e3eb49218a Mon Sep 17 00:00:00 2001 From: Scott Ries Date: Mon, 6 Apr 2026 13:40:20 -0400 Subject: [PATCH 18/28] fix(webdriverio): use dynamic port in esmTest instead of fixed port --- packages/webdriverio/test/esmTest.mjs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/webdriverio/test/esmTest.mjs b/packages/webdriverio/test/esmTest.mjs index c2f41142..7347559f 100644 --- a/packages/webdriverio/test/esmTest.mjs +++ b/packages/webdriverio/test/esmTest.mjs @@ -33,6 +33,18 @@ try { // .env file not found; env vars may already be set in the environment } +const getFreePort = () => { + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.unref(); + server.on('error', reject); + server.listen(0, () => { + const { port } = server.address(); + server.close(() => resolve(port)); + }); + }); +}; + const connectToChromeDriver = port => { return new Promise((resolve, reject) => { const timer = setTimeout(() => { From a5fd3408e803890d44c18377461bbcdc0b65edd9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:47:28 +0000 Subject: [PATCH 19/28] test(webdriverio): add unit tests for clientSwitchFrame v9 BiDi and v8 classic paths Agent-Logs-Url: https://github.com/dequelabs/axe-core-npm/sessions/12d366ad-7cb4-4a11-8fdb-2638ff4a8fa1 Co-authored-by: scottmries <1245800+scottmries@users.noreply.github.com> --- .../webdriverio/test/axe-webdriverio.spec.ts | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/webdriverio/test/axe-webdriverio.spec.ts b/packages/webdriverio/test/axe-webdriverio.spec.ts index d13662da..5474e7d4 100644 --- a/packages/webdriverio/test/axe-webdriverio.spec.ts +++ b/packages/webdriverio/test/axe-webdriverio.spec.ts @@ -8,7 +8,7 @@ import net from 'net'; import fs from 'fs'; import delay from 'delay'; import { AxeBuilder } from '../src'; -import { logOrRethrowError } from '../src/utils'; +import { logOrRethrowError, clientSwitchFrame } from '../src/utils'; import type { AxeResults, Result } from 'axe-core'; import child_process from 'child_process'; import { ChildProcessWithoutNullStreams } from 'child_process'; @@ -1578,4 +1578,46 @@ describe('@axe-core/webdriverio', () => { }); }); } + + describe('clientSwitchFrame', () => { + it('calls switchFrame with an element and returns the context ID on a v9-style client', async () => { + const contextId = 'some-bidi-context-id'; + const stubClient = { switchFrame: sinon.stub().resolves(contextId) }; + const element = {} as any; + const result = await clientSwitchFrame(stubClient as any, element); + assert.isTrue(stubClient.switchFrame.calledOnceWith(element)); + assert.equal(result, contextId); + }); + + it('calls switchFrame with a context ID string for re-entry on a v9-style client', async () => { + const contextId = 'some-bidi-context-id'; + const stubClient = { switchFrame: sinon.stub().resolves(contextId) }; + const result = await clientSwitchFrame(stubClient as any, contextId); + assert.isTrue(stubClient.switchFrame.calledOnceWith(contextId)); + assert.equal(result, contextId); + }); + + it('calls switchFrame with null on a v9-style client', async () => { + const contextId = 'top-level-context-id'; + const stubClient = { switchFrame: sinon.stub().resolves(contextId) }; + const result = await clientSwitchFrame(stubClient as any, null); + assert.isTrue(stubClient.switchFrame.calledOnceWith(null)); + assert.equal(result, contextId); + }); + + it('calls switchToFrame with an element and returns undefined on a v8-style client', async () => { + const stubClient = { switchToFrame: sinon.stub().resolves(undefined) }; + const element = {} as any; + const result = await clientSwitchFrame(stubClient as any, element); + assert.isTrue(stubClient.switchToFrame.calledOnceWith(element)); + assert.isUndefined(result); + }); + + it('does not call switchToFrame with a string context ID on a v8-style client and returns undefined', async () => { + const stubClient = { switchToFrame: sinon.stub().resolves(undefined) }; + const result = await clientSwitchFrame(stubClient as any, 'some-context-id'); + assert.isFalse(stubClient.switchToFrame.called); + assert.isUndefined(result); + }); + }); }); From a7ce0c426c2cbfe30de18225bd4d62376730d155 Mon Sep 17 00:00:00 2001 From: Scott Ries Date: Mon, 6 Apr 2026 14:06:56 -0400 Subject: [PATCH 20/28] fix(webdriverio): use dynamic port in tests instead of hard-coded port Replace hard-coded port 9515 in axe-webdriverio.spec.ts with a dynamic ephemeral port via getFreePort(), and remove duplicate getFreePort() definition from esmTest.mjs. --- .../webdriverio/test/axe-webdriverio.spec.ts | 17 +++++++++++++++-- packages/webdriverio/test/esmTest.mjs | 12 ------------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/webdriverio/test/axe-webdriverio.spec.ts b/packages/webdriverio/test/axe-webdriverio.spec.ts index 5474e7d4..b3353a6e 100644 --- a/packages/webdriverio/test/axe-webdriverio.spec.ts +++ b/packages/webdriverio/test/axe-webdriverio.spec.ts @@ -42,6 +42,20 @@ const wdioMajorVersion = (() => { return 0; })(); +const getFreePort = (): Promise => { + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.listen(0, '127.0.0.1', () => { + const { port } = server.address() as net.AddressInfo; + server.close(err => { + if (err) reject(err); + else resolve(port); + }); + }); + server.once('error', reject); + }); +}; + const connectToChromeDriver = (port: number): Promise => { let socket: net.Socket; return new Promise((resolve, reject) => { @@ -75,11 +89,10 @@ describe('@axe-core/webdriverio', () => { let port: number; for (const protocol of ['devtools', 'webdriver'] as const) { if (protocol === 'webdriver') { - port = 9515; - let chromedriverProcess: ChildProcessWithoutNullStreams; before(async () => { + port = await getFreePort(); assert( process.env.CHROME_TEST_PATH, 'CHROME_TEST_PATH is not set. Run `npx browser-driver-manager install chrome`' diff --git a/packages/webdriverio/test/esmTest.mjs b/packages/webdriverio/test/esmTest.mjs index 7347559f..c2f41142 100644 --- a/packages/webdriverio/test/esmTest.mjs +++ b/packages/webdriverio/test/esmTest.mjs @@ -33,18 +33,6 @@ try { // .env file not found; env vars may already be set in the environment } -const getFreePort = () => { - return new Promise((resolve, reject) => { - const server = net.createServer(); - server.unref(); - server.on('error', reject); - server.listen(0, () => { - const { port } = server.address(); - server.close(() => resolve(port)); - }); - }); -}; - const connectToChromeDriver = port => { return new Promise((resolve, reject) => { const timer = setTimeout(() => { From 1634b10f22615afb35580b18300e7aa57b7d5092 Mon Sep 17 00:00:00 2001 From: Scott Ries Date: Mon, 6 Apr 2026 14:24:15 -0400 Subject: [PATCH 21/28] refactor(webdriverio): extract shared test utilities into testUtils.js Move getFreePort, connectToChromeDriver, and BDM env loading into a shared testUtils.js so both esmTest.mjs and axe-webdriverio.spec.ts can import them rather than duplicating the logic. --- .../webdriverio/test/axe-webdriverio.spec.ts | 60 ++++--------------- packages/webdriverio/test/esmTest.mjs | 56 +---------------- packages/webdriverio/test/testUtils.js | 47 +++++++++++++++ 3 files changed, 59 insertions(+), 104 deletions(-) create mode 100644 packages/webdriverio/test/testUtils.js diff --git a/packages/webdriverio/test/axe-webdriverio.spec.ts b/packages/webdriverio/test/axe-webdriverio.spec.ts index b3353a6e..b5c16401 100644 --- a/packages/webdriverio/test/axe-webdriverio.spec.ts +++ b/packages/webdriverio/test/axe-webdriverio.spec.ts @@ -4,7 +4,6 @@ import listen from 'async-listen'; import { assert } from 'chai'; import path from 'path'; import { Server, createServer } from 'http'; -import net from 'net'; import fs from 'fs'; import delay from 'delay'; import { AxeBuilder } from '../src'; @@ -13,14 +12,15 @@ import type { AxeResults, Result } from 'axe-core'; import child_process from 'child_process'; import { ChildProcessWithoutNullStreams } from 'child_process'; import { fixturesPath } from 'axe-test-fixtures'; -import { config } from 'dotenv'; -import os from 'os'; import sinon from 'sinon'; -const HOME_DIR = os.homedir(); -const BDM_CACHE_DIR = path.resolve(HOME_DIR, '.browser-driver-manager'); +const { + getFreePort, + connectToChromeDriver, + loadBdmEnv +} = require('./testUtils'); -config({ path: path.resolve(BDM_CACHE_DIR, '.env') }); +loadBdmEnv(); // devtools protocol was removed in WDIO v9. // require('webdriverio/package.json') fails when the package uses an exports @@ -42,49 +42,6 @@ const wdioMajorVersion = (() => { return 0; })(); -const getFreePort = (): Promise => { - return new Promise((resolve, reject) => { - const server = net.createServer(); - server.listen(0, '127.0.0.1', () => { - const { port } = server.address() as net.AddressInfo; - server.close(err => { - if (err) reject(err); - else resolve(port); - }); - }); - server.once('error', reject); - }); -}; - -const connectToChromeDriver = (port: number): Promise => { - let socket: net.Socket; - return new Promise((resolve, reject) => { - // Give up after 1s - const timer = setTimeout(() => { - socket.destroy(); - reject(new Error('Unable to connect to ChromeDriver')); - }, 1000); - - const connectionListener = (): void => { - clearTimeout(timer); - socket.destroy(); - return resolve(); - }; - - socket = net.createConnection( - { host: 'localhost', port }, - connectionListener - ); - - // Fail on error - socket.once('error', (err: Error) => { - clearTimeout(timer); - socket.destroy(); - return reject(err); - }); - }); -}; - describe('@axe-core/webdriverio', () => { let port: number; for (const protocol of ['devtools', 'webdriver'] as const) { @@ -1628,7 +1585,10 @@ describe('@axe-core/webdriverio', () => { it('does not call switchToFrame with a string context ID on a v8-style client and returns undefined', async () => { const stubClient = { switchToFrame: sinon.stub().resolves(undefined) }; - const result = await clientSwitchFrame(stubClient as any, 'some-context-id'); + const result = await clientSwitchFrame( + stubClient as any, + 'some-context-id' + ); assert.isFalse(stubClient.switchToFrame.called); assert.isUndefined(result); }); diff --git a/packages/webdriverio/test/esmTest.mjs b/packages/webdriverio/test/esmTest.mjs index c2f41142..37c02e32 100644 --- a/packages/webdriverio/test/esmTest.mjs +++ b/packages/webdriverio/test/esmTest.mjs @@ -6,9 +6,7 @@ import { pathToFileURL } from 'url'; import { join } from 'path'; import { fixturesPath } from 'axe-test-fixtures'; import { spawn } from 'child_process'; -import net from 'net'; -import os from 'os'; -import { readFileSync } from 'fs'; +import { getFreePort, connectToChromeDriver, loadBdmEnv } from './testUtils.js'; assert(typeof defaultExport === 'function', 'default export is not a function'); assert(typeof AxeBuilder === 'function', 'named export is not a function'); @@ -17,57 +15,7 @@ assert( 'default and named export are not the same' ); -// Load browser-driver-manager env vars (same approach as axe-webdriverio.spec.ts) -const BDM_ENV_PATH = join(os.homedir(), '.browser-driver-manager', '.env'); -try { - const envContent = readFileSync(BDM_ENV_PATH, 'utf-8'); - for (const line of envContent.split('\n')) { - const match = line.match(/^([^=\s]+)=(.*)$/); - if (match) { - // Strip surrounding quotes that some .env writers add - const value = match[2].replace(/^(['"])(.*)\1$/, '$2'); - process.env[match[1]] = value; - } - } -} catch { - // .env file not found; env vars may already be set in the environment -} - -const connectToChromeDriver = port => { - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - socket.destroy(); - reject(new Error('Unable to connect to ChromeDriver')); - }, 1000); - const socket = net.createConnection({ host: 'localhost', port }, () => { - clearTimeout(timer); - socket.destroy(); - resolve(); - }); - socket.once('error', err => { - clearTimeout(timer); - socket.destroy(); - reject(err); - }); - }); -}; - -const getFreePort = () => { - return new Promise((resolve, reject) => { - const server = net.createServer(); - server.listen(0, '127.0.0.1', () => { - const { port } = server.address(); - server.close(err => { - if (err) { - reject(err); - } else { - resolve(port); - } - }); - }); - server.once('error', reject); - }); -}; +loadBdmEnv(); async function integrationTest() { const port = await getFreePort(); diff --git a/packages/webdriverio/test/testUtils.js b/packages/webdriverio/test/testUtils.js new file mode 100644 index 00000000..82bf9b4d --- /dev/null +++ b/packages/webdriverio/test/testUtils.js @@ -0,0 +1,47 @@ +'use strict'; + +const net = require('net'); +const path = require('path'); +const os = require('os'); +const { config } = require('dotenv'); + +const getFreePort = () => { + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.listen(0, '127.0.0.1', () => { + const { port } = server.address(); + server.close(err => { + if (err) reject(err); + else resolve(port); + }); + }); + server.once('error', reject); + }); +}; + +const connectToChromeDriver = port => { + let socket; + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + socket.destroy(); + reject(new Error('Unable to connect to ChromeDriver')); + }, 1000); + socket = net.createConnection({ host: 'localhost', port }, () => { + clearTimeout(timer); + socket.destroy(); + resolve(); + }); + socket.once('error', err => { + clearTimeout(timer); + socket.destroy(); + reject(err); + }); + }); +}; + +const loadBdmEnv = () => { + const bdmCacheDir = path.resolve(os.homedir(), '.browser-driver-manager'); + config({ path: path.resolve(bdmCacheDir, '.env') }); +}; + +module.exports = { getFreePort, connectToChromeDriver, loadBdmEnv }; From 78c9878b64d3f5482b038410881b800e10ac530f Mon Sep 17 00:00:00 2001 From: Scott Ries Date: Mon, 6 Apr 2026 15:13:09 -0400 Subject: [PATCH 22/28] chore(webdriverio): clean up --- packages/webdriverio/src/index.ts | 20 ++++--- packages/webdriverio/src/utils.ts | 52 +++++++------------ .../webdriverio/test/axe-webdriverio.spec.ts | 20 ------- packages/webdriverio/test/esmTest.mjs | 1 - packages/webdriverio/test/testUtils.js | 40 ++++++++------ 5 files changed, 57 insertions(+), 76 deletions(-) diff --git a/packages/webdriverio/src/index.ts b/packages/webdriverio/src/index.ts index a5f6ce9d..4de61b83 100644 --- a/packages/webdriverio/src/index.ts +++ b/packages/webdriverio/src/index.ts @@ -261,7 +261,9 @@ export default class AxeBuilder { // references via browsingContextLocateNodes, bypassing the stale-ID issue. await clientSwitchFrame(this.client, null); if (browsingContextId !== null) { - await clientSwitchFrame(this.client, browsingContextId); + // browsingContextId is only set on v9 BiDi clients, so switchFrame is available. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await (this.client as any).switchFrame(browsingContextId); } else if (browsingContext !== null) { await clientSwitchFrame(this.client, browsingContext); } @@ -344,9 +346,12 @@ export default class AxeBuilder { private async runPartialRecursive( context: SerialContextObject, - frameStack: WdioElement[] = [] + frameStack: WdioElement[] = [], + topWindow?: string ): Promise { - const topWindow = await this.client.getWindowHandle(); + if (topWindow === undefined) { + topWindow = await this.client.getWindowHandle(); + } const frameContexts = await axeGetFrameContext(this.client, context); const partials: PartialResults = [ await axeRunPartial(this.client, context, this.option) @@ -359,10 +364,11 @@ export default class AxeBuilder { await clientSwitchFrame(this.client, frame); await axeSourceInject(this.client, this.script); partials.push( - ...(await this.runPartialRecursive(frameContext, [ - ...frameStack, - frame - ])) + ...(await this.runPartialRecursive( + frameContext, + [...frameStack, frame], + topWindow + )) ); } catch { await clientSwitchWindow(this.client, topWindow); diff --git a/packages/webdriverio/src/utils.ts b/packages/webdriverio/src/utils.ts index bdc52b3d..5c16f65b 100644 --- a/packages/webdriverio/src/utils.ts +++ b/packages/webdriverio/src/utils.ts @@ -90,37 +90,21 @@ const promisify = (thenable: Promise): Promise => { export async function clientSwitchFrame( client: WdioBrowser, - id: WdioElement | null | string + id: WdioElement | null ): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any const c = client as any; if (typeof c.switchFrame === 'function') { - // WDIO v9 BiDi: switchFrame accepts elements, null, or context ID strings. + // WDIO v9 BiDi: switchFrame accepts elements or null. // It returns the BiDi browsing context ID, which we capture for safe re-entry. return (await c.switchFrame(id)) as string; - } else if (typeof id !== 'string') { - // Classic WebDriver (WDIO v5–v8): no string context ID support. + } else { + // Classic WebDriver (WDIO v5–v8). await c.switchToFrame(id); } return undefined; } -export async function clientSwitchParentFrame( - client: WdioBrowser -): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const c = client as any; - // Prefer switchToParentFrame (v8 and v9 via @wdio/protocols) because it - // correctly switches to the immediate parent frame. In WDIO v9 WebDriver - // Classic (non-BiDi), switchFrame(null) calls switchToFrame(null) which - // switches to the top-level frame instead of the parent frame. - if (typeof c.switchToParentFrame === 'function') { - await c.switchToParentFrame(); - } else if (typeof c.switchFrame === 'function') { - await c.switchFrame(null); - } -} - export async function clientSwitchWindow( client: WdioBrowser, handle: string @@ -175,18 +159,22 @@ async function assertFrameReady(client: WdioBrowser): Promise { reject(); }, FRAME_LOAD_TIMEOUT); }); - const executePromise = client.execute(() => { - // In WDIO v9 BiDi mode, executing scripts in a lazy-loaded cross-origin - // iframe succeeds even when the frame hasn't loaded its content yet - // (BiDi bypasses same-origin restrictions). The frame's document will be - // about:blank until the browser actually fetches the remote URL. - // Classic WebDriver throws a cross-origin error in this case, which is - // caught and treated as an untestable frame. We replicate that behavior - // here by also checking that the document isn't still at about:blank. - return ( - document.readyState === 'complete' && document.URL !== 'about:blank' - ); - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const isBiDi = typeof (client as any).switchFrame === 'function'; + const executePromise = isBiDi + ? client.execute(() => { + // In WDIO v9 BiDi mode, executing scripts in a lazy-loaded cross-origin + // iframe succeeds even when the frame hasn't loaded its content yet + // (BiDi bypasses same-origin restrictions). The frame's document will be + // about:blank until the browser actually fetches the remote URL. + // Classic WebDriver throws a cross-origin error in this case, which is + // caught and treated as an untestable frame. We replicate that behavior + // here by also checking that the document isn't still at about:blank. + return ( + document.readyState === 'complete' && document.URL !== 'about:blank' + ); + }) + : client.execute(() => document.readyState === 'complete'); const readyState = await Promise.race([timeoutPromise, executePromise]); assert(readyState); } catch { diff --git a/packages/webdriverio/test/axe-webdriverio.spec.ts b/packages/webdriverio/test/axe-webdriverio.spec.ts index b5c16401..9c4e5522 100644 --- a/packages/webdriverio/test/axe-webdriverio.spec.ts +++ b/packages/webdriverio/test/axe-webdriverio.spec.ts @@ -5,7 +5,6 @@ import { assert } from 'chai'; import path from 'path'; import { Server, createServer } from 'http'; import fs from 'fs'; -import delay from 'delay'; import { AxeBuilder } from '../src'; import { logOrRethrowError, clientSwitchFrame } from '../src/utils'; import type { AxeResults, Result } from 'axe-core'; @@ -64,7 +63,6 @@ describe('@axe-core/webdriverio', () => { ]); chromedriverProcess.stdout.pipe(process.stdout); chromedriverProcess.stderr.pipe(process.stderr); - await delay(500); await connectToChromeDriver(port); }); @@ -1559,14 +1557,6 @@ describe('@axe-core/webdriverio', () => { assert.equal(result, contextId); }); - it('calls switchFrame with a context ID string for re-entry on a v9-style client', async () => { - const contextId = 'some-bidi-context-id'; - const stubClient = { switchFrame: sinon.stub().resolves(contextId) }; - const result = await clientSwitchFrame(stubClient as any, contextId); - assert.isTrue(stubClient.switchFrame.calledOnceWith(contextId)); - assert.equal(result, contextId); - }); - it('calls switchFrame with null on a v9-style client', async () => { const contextId = 'top-level-context-id'; const stubClient = { switchFrame: sinon.stub().resolves(contextId) }; @@ -1582,15 +1572,5 @@ describe('@axe-core/webdriverio', () => { assert.isTrue(stubClient.switchToFrame.calledOnceWith(element)); assert.isUndefined(result); }); - - it('does not call switchToFrame with a string context ID on a v8-style client and returns undefined', async () => { - const stubClient = { switchToFrame: sinon.stub().resolves(undefined) }; - const result = await clientSwitchFrame( - stubClient as any, - 'some-context-id' - ); - assert.isFalse(stubClient.switchToFrame.called); - assert.isUndefined(result); - }); }); }); diff --git a/packages/webdriverio/test/esmTest.mjs b/packages/webdriverio/test/esmTest.mjs index 37c02e32..a56ad2c0 100644 --- a/packages/webdriverio/test/esmTest.mjs +++ b/packages/webdriverio/test/esmTest.mjs @@ -33,7 +33,6 @@ async function integrationTest() { `--port=${port}` ], { stdio: 'inherit' }); - await new Promise(r => setTimeout(r, 500)); await connectToChromeDriver(port); let client; diff --git a/packages/webdriverio/test/testUtils.js b/packages/webdriverio/test/testUtils.js index 82bf9b4d..47e5913f 100644 --- a/packages/webdriverio/test/testUtils.js +++ b/packages/webdriverio/test/testUtils.js @@ -19,24 +19,32 @@ const getFreePort = () => { }); }; -const connectToChromeDriver = port => { - let socket; - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - socket.destroy(); - reject(new Error('Unable to connect to ChromeDriver')); - }, 1000); - socket = net.createConnection({ host: 'localhost', port }, () => { - clearTimeout(timer); - socket.destroy(); - resolve(); +const connectToChromeDriver = (port, retries = 10, interval = 200) => { + const attempt = () => { + return new Promise((resolve, reject) => { + const socket = net.createConnection({ host: 'localhost', port }, () => { + socket.destroy(); + resolve(); + }); + socket.once('error', err => { + socket.destroy(); + reject(err); + }); }); - socket.once('error', err => { - clearTimeout(timer); - socket.destroy(); - reject(err); + }; + + const retry = remaining => { + return attempt().catch(err => { + if (remaining <= 0) { + throw new Error(`Unable to connect to ChromeDriver: ${err.message}`); + } + return new Promise(resolve => setTimeout(resolve, interval)).then(() => + retry(remaining - 1) + ); }); - }); + }; + + return retry(retries); }; const loadBdmEnv = () => { From 8d7a126d6c3c184d76830dbc80bdd6d6308e5e7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:49:02 +0000 Subject: [PATCH 23/28] fix(webdriverio): replace `any` casts with `in` checks in clientSwitchFrame/clientSwitchWindow Agent-Logs-Url: https://github.com/dequelabs/axe-core-npm/sessions/0bd1d6bd-324c-46c0-8974-b554d2b8353f Co-authored-by: Garbee <868301+Garbee@users.noreply.github.com> --- packages/webdriverio/src/utils.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/webdriverio/src/utils.ts b/packages/webdriverio/src/utils.ts index 5c16f65b..455abaf5 100644 --- a/packages/webdriverio/src/utils.ts +++ b/packages/webdriverio/src/utils.ts @@ -92,15 +92,13 @@ export async function clientSwitchFrame( client: WdioBrowser, id: WdioElement | null ): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const c = client as any; - if (typeof c.switchFrame === 'function') { + if ('switchFrame' in client) { // WDIO v9 BiDi: switchFrame accepts elements or null. // It returns the BiDi browsing context ID, which we capture for safe re-entry. - return (await c.switchFrame(id)) as string; + return (await client.switchFrame(id)) as string; } else { // Classic WebDriver (WDIO v5–v8). - await c.switchToFrame(id); + await client.switchToFrame(id); } return undefined; } @@ -109,15 +107,13 @@ export async function clientSwitchWindow( client: WdioBrowser, handle: string ): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const c = client as any; // Prefer switchToWindow (handle-based) over switchWindow (pattern match). // switchWindow matches by title/URL/name in v8, so passing a window handle // string fails with "No window found". switchToWindow takes a handle directly. - if (typeof c.switchToWindow === 'function') { - await c.switchToWindow(handle); - } else if (typeof c.switchWindow === 'function') { - await c.switchWindow(handle); + if ('switchToWindow' in client) { + await client.switchToWindow(handle); + } else if ('switchWindow' in client) { + await client.switchWindow(handle); } } From 50db5d88884e35d80c8c40af5dd91d17248ec125 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:16:40 +0000 Subject: [PATCH 24/28] fix(webdriverio): remove remaining `as any` casts from src files Agent-Logs-Url: https://github.com/dequelabs/axe-core-npm/sessions/04c624f6-4014-43a5-a943-9b2fe3954d1d Co-authored-by: Garbee <868301+Garbee@users.noreply.github.com> --- packages/webdriverio/src/index.ts | 8 +++----- packages/webdriverio/src/utils.ts | 8 ++++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/webdriverio/src/index.ts b/packages/webdriverio/src/index.ts index 4de61b83..b98a2261 100644 --- a/packages/webdriverio/src/index.ts +++ b/packages/webdriverio/src/index.ts @@ -38,8 +38,7 @@ async function loadAxePath() { if (typeof require === 'function' && typeof require.resolve === 'function') { axeCorePath = require.resolve('axe-core'); } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { createRequire } = (await import('node:module')) as any; + const { createRequire } = await import('node:module'); // `getFilename` is needed because esm's `import.meta.url` is illegal syntax in cjs const filename = pathToFileURL(getFilename()).toString(); @@ -260,10 +259,9 @@ export default class AxeBuilder { // context ID string instead causes WDIO to re-query fresh element // references via browsingContextLocateNodes, bypassing the stale-ID issue. await clientSwitchFrame(this.client, null); - if (browsingContextId !== null) { + if (browsingContextId !== null && 'switchFrame' in this.client) { // browsingContextId is only set on v9 BiDi clients, so switchFrame is available. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (this.client as any).switchFrame(browsingContextId); + await this.client.switchFrame(browsingContextId); } else if (browsingContext !== null) { await clientSwitchFrame(this.client, browsingContext); } diff --git a/packages/webdriverio/src/utils.ts b/packages/webdriverio/src/utils.ts index 455abaf5..c83f40e1 100644 --- a/packages/webdriverio/src/utils.ts +++ b/packages/webdriverio/src/utils.ts @@ -26,8 +26,7 @@ export const isWebdriverClient = (client: WdioBrowser): boolean => { // @wdio/globals browser uses proxies for the functions so using `'switchToFrame' in client` doesn't work // @see https://github.com/webdriverio/webdriverio/blob/main/packages/wdio-globals/src/index.ts - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const c = client as any; + const c = client as unknown as Record; if ( typeof c.switchToFrame !== 'function' && typeof c.switchFrame !== 'function' @@ -155,8 +154,9 @@ async function assertFrameReady(client: WdioBrowser): Promise { reject(); }, FRAME_LOAD_TIMEOUT); }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const isBiDi = typeof (client as any).switchFrame === 'function'; + const isBiDi = + typeof (client as unknown as Record).switchFrame === + 'function'; const executePromise = isBiDi ? client.execute(() => { // In WDIO v9 BiDi mode, executing scripts in a lazy-loaded cross-origin From 568a210c3703f2d8f7e37e4ba602de61f47b1dd2 Mon Sep 17 00:00:00 2001 From: Scott Ries Date: Tue, 7 Apr 2026 11:14:22 -0400 Subject: [PATCH 25/28] chore(webdriverio): clean up - Add unit tests for clientSwitchWindow - Remove browser-driver-manager dependency from esmTest; use chromedriver package directly --- .../webdriverio/test/axe-webdriverio.spec.ts | 30 ++++++++++++++++++- packages/webdriverio/test/esmTest.mjs | 20 ++++--------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/packages/webdriverio/test/axe-webdriverio.spec.ts b/packages/webdriverio/test/axe-webdriverio.spec.ts index 9c4e5522..471fbc71 100644 --- a/packages/webdriverio/test/axe-webdriverio.spec.ts +++ b/packages/webdriverio/test/axe-webdriverio.spec.ts @@ -6,7 +6,11 @@ import path from 'path'; import { Server, createServer } from 'http'; import fs from 'fs'; import { AxeBuilder } from '../src'; -import { logOrRethrowError, clientSwitchFrame } from '../src/utils'; +import { + logOrRethrowError, + clientSwitchFrame, + clientSwitchWindow +} from '../src/utils'; import type { AxeResults, Result } from 'axe-core'; import child_process from 'child_process'; import { ChildProcessWithoutNullStreams } from 'child_process'; @@ -1573,4 +1577,28 @@ describe('@axe-core/webdriverio', () => { assert.isUndefined(result); }); }); + + describe('clientSwitchWindow', () => { + it('calls switchToWindow with a handle on a v8-style client', async () => { + const stubClient = { switchToWindow: sinon.stub().resolves() }; + await clientSwitchWindow(stubClient as any, 'window-handle'); + assert.isTrue(stubClient.switchToWindow.calledOnceWith('window-handle')); + }); + + it('calls switchWindow with a handle on a v9-style client', async () => { + const stubClient = { switchWindow: sinon.stub().resolves() }; + await clientSwitchWindow(stubClient as any, 'window-handle'); + assert.isTrue(stubClient.switchWindow.calledOnceWith('window-handle')); + }); + + it('prefers switchToWindow over switchWindow when both are present', async () => { + const stubClient = { + switchToWindow: sinon.stub().resolves(), + switchWindow: sinon.stub().resolves() + }; + await clientSwitchWindow(stubClient as any, 'window-handle'); + assert.isTrue(stubClient.switchToWindow.calledOnceWith('window-handle')); + assert.isFalse(stubClient.switchWindow.called); + }); + }); }); diff --git a/packages/webdriverio/test/esmTest.mjs b/packages/webdriverio/test/esmTest.mjs index a56ad2c0..0b8522e8 100644 --- a/packages/webdriverio/test/esmTest.mjs +++ b/packages/webdriverio/test/esmTest.mjs @@ -6,7 +6,9 @@ import { pathToFileURL } from 'url'; import { join } from 'path'; import { fixturesPath } from 'axe-test-fixtures'; import { spawn } from 'child_process'; -import { getFreePort, connectToChromeDriver, loadBdmEnv } from './testUtils.js'; +import { getFreePort, connectToChromeDriver } from './testUtils.js'; + +const { default: { path: chromedriverPath } } = await import('chromedriver'); assert(typeof defaultExport === 'function', 'default export is not a function'); assert(typeof AxeBuilder === 'function', 'named export is not a function'); @@ -15,21 +17,10 @@ assert( 'default and named export are not the same' ); -loadBdmEnv(); - async function integrationTest() { const port = await getFreePort(); - assert( - process.env.CHROMEDRIVER_TEST_PATH, - 'CHROMEDRIVER_TEST_PATH is not set. Run `npx browser-driver-manager install chrome`' - ); - assert( - process.env.CHROME_TEST_PATH, - 'CHROME_TEST_PATH is not set. Run `npx browser-driver-manager install chrome`' - ); - - const chromedriverProcess = spawn(process.env.CHROMEDRIVER_TEST_PATH, [ + const chromedriverProcess = spawn(chromedriverPath, [ `--port=${port}` ], { stdio: 'inherit' }); @@ -44,8 +35,7 @@ async function integrationTest() { capabilities: { browserName: 'chrome', 'goog:chromeOptions': { - args: ['--headless', '--no-sandbox'], - binary: process.env.CHROME_TEST_PATH + args: ['--headless', '--no-sandbox'] } }, logLevel: 'error' From a7c9d4341780bc9ba9ddbd8732a5970c67a70445 Mon Sep 17 00:00:00 2001 From: Scott Ries Date: Tue, 7 Apr 2026 11:30:36 -0400 Subject: [PATCH 26/28] chore(webdriverio): clean up Replace `as any` casts in clientSwitchFrame and clientSwitchWindow with `as unknown as Record` to avoid unsafe any usage. --- packages/webdriverio/src/utils.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/webdriverio/src/utils.ts b/packages/webdriverio/src/utils.ts index c83f40e1..4e7f7d88 100644 --- a/packages/webdriverio/src/utils.ts +++ b/packages/webdriverio/src/utils.ts @@ -91,13 +91,16 @@ export async function clientSwitchFrame( client: WdioBrowser, id: WdioElement | null ): Promise { - if ('switchFrame' in client) { + const c = client as unknown as Record; + if (typeof c.switchFrame === 'function') { // WDIO v9 BiDi: switchFrame accepts elements or null. // It returns the BiDi browsing context ID, which we capture for safe re-entry. - return (await client.switchFrame(id)) as string; + return (await (c.switchFrame as (id: unknown) => Promise)( + id + )) as string; } else { // Classic WebDriver (WDIO v5–v8). - await client.switchToFrame(id); + await (c.switchToFrame as (id: unknown) => Promise)(id); } return undefined; } @@ -106,13 +109,14 @@ export async function clientSwitchWindow( client: WdioBrowser, handle: string ): Promise { + const c = client as unknown as Record; // Prefer switchToWindow (handle-based) over switchWindow (pattern match). // switchWindow matches by title/URL/name in v8, so passing a window handle // string fails with "No window found". switchToWindow takes a handle directly. - if ('switchToWindow' in client) { - await client.switchToWindow(handle); - } else if ('switchWindow' in client) { - await client.switchWindow(handle); + if (typeof c.switchToWindow === 'function') { + await (c.switchToWindow as (handle: string) => Promise)(handle); + } else if (typeof c.switchWindow === 'function') { + await (c.switchWindow as (handle: string) => Promise)(handle); } } From 93dde353d63ff89f8c9bf9599945f25851836889 Mon Sep 17 00:00:00 2001 From: Scott Ries Date: Tue, 7 Apr 2026 11:39:51 -0400 Subject: [PATCH 27/28] fix(webdriverio): use browser-driver-manager chromedriver in esmTest when available Fall back to the chromedriver npm package when CHROMEDRIVER_TEST_PATH is not set, so the test works without browser-driver-manager installed. --- packages/webdriverio/test/esmTest.mjs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/webdriverio/test/esmTest.mjs b/packages/webdriverio/test/esmTest.mjs index 0b8522e8..6e521fde 100644 --- a/packages/webdriverio/test/esmTest.mjs +++ b/packages/webdriverio/test/esmTest.mjs @@ -6,9 +6,14 @@ import { pathToFileURL } from 'url'; import { join } from 'path'; import { fixturesPath } from 'axe-test-fixtures'; import { spawn } from 'child_process'; -import { getFreePort, connectToChromeDriver } from './testUtils.js'; +import { getFreePort, connectToChromeDriver, loadBdmEnv } from './testUtils.js'; -const { default: { path: chromedriverPath } } = await import('chromedriver'); +const { default: { path: chromedriverPackagePath } } = await import('chromedriver'); +// Prefer a browser-driver-manager installed chromedriver (matched to the local +// Chrome version) if available, otherwise fall back to the chromedriver package. +loadBdmEnv(); +const chromedriverPath = process.env.CHROMEDRIVER_TEST_PATH ?? chromedriverPackagePath; +const chromeBinary = process.env.CHROME_TEST_PATH; assert(typeof defaultExport === 'function', 'default export is not a function'); assert(typeof AxeBuilder === 'function', 'named export is not a function'); @@ -28,15 +33,17 @@ async function integrationTest() { let client; try { + const chromeOptions = { + args: ['--headless', '--no-sandbox'], + ...(chromeBinary ? { binary: chromeBinary } : {}) + }; const options = { path: '/', hostname: 'localhost', port, capabilities: { browserName: 'chrome', - 'goog:chromeOptions': { - args: ['--headless', '--no-sandbox'] - } + 'goog:chromeOptions': chromeOptions }, logLevel: 'error' }; From dbe20fceaba2ead0b7885dac2c94bbcbe790f45c Mon Sep 17 00:00:00 2001 From: Scott Ries Date: Tue, 7 Apr 2026 11:44:00 -0400 Subject: [PATCH 28/28] fix(webdriverio): pin chromedriver to ^146 to match CI Chrome version --- package-lock.json | 514 ++++++++++++++++++++++++-- packages/webdriverio/package.json | 3 +- packages/webdriverio/test/esmTest.mjs | 17 +- 3 files changed, 486 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index f2f29363..ef84a33f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9121,7 +9121,8 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "node_modules/available-typed-arrays": { "version": "1.0.7", @@ -9159,14 +9160,23 @@ "license": "MPL-2.0" }, "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", + "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" } }, "node_modules/b4a": { @@ -10343,52 +10353,70 @@ } }, "node_modules/chromedriver": { - "version": "127.0.2", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-127.0.2.tgz", - "integrity": "sha512-mYfJ/8FqzsdFOs2rPiAI4y0suFnv78cRnzZK0MHdSfSIDeRPbqZz0rNX4lrXt14hXc9vqXa+a8cMxlrhWtXKSQ==", + "version": "146.0.6", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-146.0.6.tgz", + "integrity": "sha512-FIRi3hy0nRiyirK03etVXEpYTIodevFcvTBAM5ZCq+pX3w31jLm6JE8BVW1ypAVLvSp6HJDvboCcdgUroS3miw==", "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { "@testim/chrome-version": "^1.1.4", - "axios": "^1.6.7", + "axios": "^1.13.5", "compare-versions": "^6.1.0", "extract-zip": "^2.0.1", - "proxy-agent": "^6.4.0", - "proxy-from-env": "^1.1.0", + "proxy-agent": "^6.5.0", + "proxy-from-env": "^2.0.0", "tcp-port-used": "^1.0.2" }, "bin": { "chromedriver": "bin/chromedriver" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/chromedriver/node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", "engines": { "node": ">=12" } }, "node_modules/chromedriver/node_modules/proxy-agent": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", - "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.3", + "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.0.1", + "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.2" + "socks-proxy-agent": "^8.0.5" }, "engines": { "node": ">= 14" } }, + "node_modules/chromedriver/node_modules/proxy-agent/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/chromedriver/node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/chromium-bidi": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", @@ -10724,6 +10752,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -12208,6 +12237,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -14394,15 +14424,16 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -14438,9 +14469,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -19244,6 +19275,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -19252,6 +19284,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -27312,7 +27345,7 @@ "dependencies": { "@axe-core/webdriverjs": "^4.11.1", "axe-core": "~4.11.1", - "chromedriver": "*", + "chromedriver": "latest", "colors": "^1.4.0", "commander": "^9.4.1", "dotenv": "^17.2.2", @@ -27460,6 +27493,7 @@ "async-listen": "^3.0.1", "axe-test-fixtures": "github:dequelabs/axe-test-fixtures#v1", "chai": "^4.3.6", + "chromedriver": "^146", "cross-dirname": "^0.1.0", "delay": "^5.0.0", "devtools": "^8.27.2", @@ -27469,12 +27503,422 @@ "rimraf": "^6.0.1", "source-map-support": "^0.5.21", "tsup": "^8.0.1", - "webdriverio": "^9.22.0" + "webdriverio": "^9.27.0" }, "peerDependencies": { "webdriverio": "^5 || ^6 || ^7 || ^8 || ^9" } }, + "packages/webdriverio/node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "packages/webdriverio/node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "packages/webdriverio/node_modules/@wdio/config": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.27.0.tgz", + "integrity": "sha512-9y8z7ugIbU6ycKrA2SqCpKh1/hobut2rDq9CLt/BNVzSlebBBVOTMiAt1XroZzcPnA7/ZqpbkpOsbpPUaAQuNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "9.18.0", + "@wdio/types": "9.27.0", + "@wdio/utils": "9.27.0", + "deepmerge-ts": "^7.0.3", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0", + "jiti": "^2.6.1" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "packages/webdriverio/node_modules/@wdio/protocols": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.27.0.tgz", + "integrity": "sha512-rIk69BsY1+6uU2PEN5FiRpI6K7HJ86YHzZRFBe4iRzKXQgGNk1zWzbdVJIuNFoOWsnmYUkK42KSSOT4Le6EmiQ==", + "dev": true, + "license": "MIT" + }, + "packages/webdriverio/node_modules/@wdio/types": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.27.0.tgz", + "integrity": "sha512-DQJ+OdRBqUBcQ30DN2Z651hEVh3OoxnlDUSRqlWy9An2AY6v9rYWTj825B6zsj5pLLEToYO1tfwWq0ab183pXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "packages/webdriverio/node_modules/@wdio/utils": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.27.0.tgz", + "integrity": "sha512-fUasd5OKJTy2seJfWnYZ9xlxTtY0p/Kyeuh7Tbb8kcofBqmBi2fTvM3sfZlo1tGQX9yCh+IS2N7hlfyFMmuZ+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.18.0", + "@wdio/types": "9.27.0", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^6.1.2", + "geckodriver": "^6.1.0", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "mitt": "^3.0.1", + "safaridriver": "^1.0.0", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "packages/webdriverio/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "packages/webdriverio/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "packages/webdriverio/node_modules/decamelize": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", + "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/webdriverio/node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "packages/webdriverio/node_modules/edgedriver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-6.3.0.tgz", + "integrity": "sha512-ggEQL+oEyIcM4nP2QC3AtCQ04o4kDNefRM3hja0odvlPSnsaxiruMxEZ93v3gDCKWYW6BXUr51PPradb+3nffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "^9.18.0", + "@zip.js/zip.js": "^2.8.11", + "decamelize": "^6.0.1", + "edge-paths": "^3.0.5", + "fast-xml-parser": "^5.3.3", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "which": "^6.0.0" + }, + "bin": { + "edgedriver": "bin/edgedriver.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "packages/webdriverio/node_modules/get-port": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.2.0.tgz", + "integrity": "sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/webdriverio/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/webdriverio/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/webdriverio/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "packages/webdriverio/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "packages/webdriverio/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "packages/webdriverio/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/webdriverio/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "packages/webdriverio/node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "packages/webdriverio/node_modules/safaridriver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-1.0.1.tgz", + "integrity": "sha512-jkg4434cYgtrIF2AeY/X0Wmd2W73cK5qIEFE3hDrrQenJH/2SDJIXGvPAigfvQTcE9+H31zkiNHbUqcihEiMRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "packages/webdriverio/node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "packages/webdriverio/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "packages/webdriverio/node_modules/webdriver": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.27.0.tgz", + "integrity": "sha512-w07ThZND48SIr0b4S7eFougYUyclmoUwdmju8yXvEJiXYjDjeYUpl8wZrYPEYRBylxpSx+sBHfEUBrPQkcTTRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0", + "@types/ws": "^8.5.3", + "@wdio/config": "9.27.0", + "@wdio/logger": "9.18.0", + "@wdio/protocols": "9.27.0", + "@wdio/types": "9.27.0", + "@wdio/utils": "9.27.0", + "deepmerge-ts": "^7.0.3", + "https-proxy-agent": "^7.0.6", + "undici": "^6.21.3", + "ws": "^8.8.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "packages/webdriverio/node_modules/webdriverio": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.27.0.tgz", + "integrity": "sha512-Y4FbMf4bKBXpPB0lYpglzQ2GfDDe6uojmMZl85uPyrDx18NW7mqN84ZawGoIg/FRvcLaVhcOzc98WOPo725Rag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.27.0", + "@wdio/logger": "9.18.0", + "@wdio/protocols": "9.27.0", + "@wdio/repl": "9.16.2", + "@wdio/types": "9.27.0", + "@wdio/utils": "9.27.0", + "archiver": "^7.0.1", + "aria-query": "^5.3.0", + "cheerio": "^1.0.0-rc.12", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "grapheme-splitter": "^1.0.4", + "htmlfy": "^0.8.1", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^12.0.0", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.27.0" + }, + "engines": { + "node": ">=18.20.0" + }, + "peerDependencies": { + "puppeteer-core": ">=22.x || <=24.x" + }, + "peerDependenciesMeta": { + "puppeteer-core": { + "optional": true + } + } + }, + "packages/webdriverio/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "packages/webdriverjs": { "name": "@axe-core/webdriverjs", "version": "4.11.1", @@ -27515,11 +27959,11 @@ "version": "4.11.1", "license": "ISC", "dependencies": { - "@axe-core/cli": "*", - "@axe-core/playwright": "*", - "@axe-core/puppeteer": "*", - "@axe-core/webdriverio": "*", - "@axe-core/webdriverjs": "*", + "@axe-core/cli": "latest", + "@axe-core/playwright": "latest", + "@axe-core/puppeteer": "latest", + "@axe-core/webdriverio": "latest", + "@axe-core/webdriverjs": "latest", "chai": "^4.3.4", "execa": "^5.1.1", "mocha": "^11.7.5", @@ -28546,7 +28990,7 @@ "@wdio/globals": "^9.17.0", "@wdio/local-runner": "^9.19.2", "@wdio/mocha-framework": "^9.21.0", - "@wdio/spec-reporter": "^9.24.0", + "@wdio/spec-reporter": "^9.19.1", "async-listen": "^3.0.1", "axe-test-fixtures": "github:dequelabs/axe-test-fixtures#v1", "chai": "^4.3.10", diff --git a/packages/webdriverio/package.json b/packages/webdriverio/package.json index df3fab2e..68aaac64 100644 --- a/packages/webdriverio/package.json +++ b/packages/webdriverio/package.json @@ -54,6 +54,7 @@ "devDependencies": { "@types/chai": "^4.3.3", "@types/chromedriver": "^81.0.1", + "chromedriver": "^146", "@types/cssesc": "^3.0.0", "@types/express": "^5.0.3", "@types/mocha": "^10.0.6", @@ -70,7 +71,7 @@ "rimraf": "^6.0.1", "source-map-support": "^0.5.21", "tsup": "^8.0.1", - "webdriverio": "^9.22.0" + "webdriverio": "^9.27.0" }, "peerDependencies": { "webdriverio": "^5 || ^6 || ^7 || ^8 || ^9" diff --git a/packages/webdriverio/test/esmTest.mjs b/packages/webdriverio/test/esmTest.mjs index 6e521fde..0b8522e8 100644 --- a/packages/webdriverio/test/esmTest.mjs +++ b/packages/webdriverio/test/esmTest.mjs @@ -6,14 +6,9 @@ import { pathToFileURL } from 'url'; import { join } from 'path'; import { fixturesPath } from 'axe-test-fixtures'; import { spawn } from 'child_process'; -import { getFreePort, connectToChromeDriver, loadBdmEnv } from './testUtils.js'; +import { getFreePort, connectToChromeDriver } from './testUtils.js'; -const { default: { path: chromedriverPackagePath } } = await import('chromedriver'); -// Prefer a browser-driver-manager installed chromedriver (matched to the local -// Chrome version) if available, otherwise fall back to the chromedriver package. -loadBdmEnv(); -const chromedriverPath = process.env.CHROMEDRIVER_TEST_PATH ?? chromedriverPackagePath; -const chromeBinary = process.env.CHROME_TEST_PATH; +const { default: { path: chromedriverPath } } = await import('chromedriver'); assert(typeof defaultExport === 'function', 'default export is not a function'); assert(typeof AxeBuilder === 'function', 'named export is not a function'); @@ -33,17 +28,15 @@ async function integrationTest() { let client; try { - const chromeOptions = { - args: ['--headless', '--no-sandbox'], - ...(chromeBinary ? { binary: chromeBinary } : {}) - }; const options = { path: '/', hostname: 'localhost', port, capabilities: { browserName: 'chrome', - 'goog:chromeOptions': chromeOptions + 'goog:chromeOptions': { + args: ['--headless', '--no-sandbox'] + } }, logLevel: 'error' };