From e8465b1db05bbab1fae8ea43e6fa60e23aeb0570 Mon Sep 17 00:00:00 2001 From: Tanmay Lokhande Date: Mon, 8 Dec 2025 10:26:55 +0530 Subject: [PATCH 1/4] fix: Logs leaking creds --- packages/wdio-browserstack-service/src/launcher.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/wdio-browserstack-service/src/launcher.ts b/packages/wdio-browserstack-service/src/launcher.ts index 7b74abe8644..460e9960cb2 100644 --- a/packages/wdio-browserstack-service/src/launcher.ts +++ b/packages/wdio-browserstack-service/src/launcher.ts @@ -117,7 +117,9 @@ export default class BrowserstackLauncherService implements Services.ServiceInst this.browserStackConfig = BrowserStackConfig.getInstance(_options, _config) BStackLogger.debug(`_options data: ${JSON.stringify(_options)}`) BStackLogger.debug(`webdriver capabilities data: ${JSON.stringify(capabilities)}`) - BStackLogger.debug(`_config data: ${JSON.stringify(_config)}`) + const configCopy = JSON.parse(JSON.stringify(_config)) + CrashReporter.recursivelyRedactKeysFromObject(configCopy, ['user', 'key', 'accesskey', 'password']) + BStackLogger.debug(`_config data: ${JSON.stringify(configCopy)}`) if (Array.isArray(capabilities)) { capabilities .flatMap((c) => { From 672aaf055461e10518817c3eaaacc13834af48c4 Mon Sep 17 00:00:00 2001 From: Vishnu Vardhan Date: Mon, 22 Dec 2025 15:06:01 +0530 Subject: [PATCH 2/4] Bug Fix: uploadCrashReport (#14947) --- packages/wdio-browserstack-service/src/crash-reporter.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/wdio-browserstack-service/src/crash-reporter.ts b/packages/wdio-browserstack-service/src/crash-reporter.ts index 7f7eb67af23..356d83361d2 100644 --- a/packages/wdio-browserstack-service/src/crash-reporter.ts +++ b/packages/wdio-browserstack-service/src/crash-reporter.ts @@ -89,7 +89,13 @@ export default class CrashReporter { }) if (response.ok) { - BStackLogger.debug(`[Crash_Report_Upload] Success response: ${JSON.stringify(await response.json())}`) + let body = await response.text() + try { + body = JSON.stringify(JSON.parse(body)) + } catch { + // Response is not JSON, use text as-is + } + BStackLogger.debug(`[Crash_Report_Upload] Success response: ${body}`) } else { BStackLogger.error(`[Crash_Report_Upload] Failed due to ${response.body}`) } From 5c764591cddf87605998735d3724ccf95d1433de Mon Sep 17 00:00:00 2001 From: MRUNAL CHAUDHARI Date: Tue, 23 Dec 2025 03:13:51 +0530 Subject: [PATCH 3/4] fix(webdriverio): fix wildcard support in browser.mock (#14944) --- e2e/wdio/headless/mocking.e2e.ts | 108 ++++++++++++++++++ .../src/utils/interception/index.ts | 30 ++++- .../src/utils/interception/utils.ts | 9 +- 3 files changed, 137 insertions(+), 10 deletions(-) diff --git a/e2e/wdio/headless/mocking.e2e.ts b/e2e/wdio/headless/mocking.e2e.ts index 17c566f0940..29a2948970c 100644 --- a/e2e/wdio/headless/mocking.e2e.ts +++ b/e2e/wdio/headless/mocking.e2e.ts @@ -1,6 +1,10 @@ import { browser } from '@wdio/globals' describe('network mocking', () => { + afterEach(async () => { + await browser.mockRestoreAll() + }) + it('marks a request as mocked even without overwrites', async () => { const baseUrl = 'https://guinea-pig.webdriver.io/' const mock = await browser.mock('https://cdn.jsdelivr.net/npm/hammerjs@1.1.3/hammer.min.js', { @@ -13,4 +17,108 @@ describe('network mocking', () => { timeout: 2000 }) }) + + it('should mock with wildcard (*) pattern', async () => { + const baseUrl = 'https://guinea-pig.webdriver.io/' + const mock = await browser.mock('https://cdn.jsdelivr.net/npm/jquery@3.6.0/*', { + method: 'get', + statusCode: 200, + }) + await browser.url(baseUrl) + await browser.execute(() => { + const script = document.createElement('script') + script.src = 'https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js' + document.body.appendChild(script) + }) + await browser.waitUntil(() => mock.calls.length >= 1, { + timeoutMsg: 'Expected wildcard mock to be called', + timeout: 5000 + }) + }) + + it('should mock with double wildcard (**) pattern', async () => { + const mock = await browser.mock('https://cdn.jsdelivr.net/npm/vue@2.6.14/**', { + method: 'get', + statusCode: 200, + }) + await browser.url('https://guinea-pig.webdriver.io/') + await browser.execute(() => { + const script = document.createElement('script') + script.src = 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js' + document.body.appendChild(script) + }) + await browser.waitUntil(() => mock.calls.length >= 1, { + timeoutMsg: 'Expected double wildcard mock to be called', + timeout: 5000 + }) + }) + + it('should mock with wildcard in middle of path', async () => { + const mock = await browser.mock('https://cdn.jsdelivr.net/npm/react@17.0.2/*/react.production.min.js', { + method: 'get', + statusCode: 200, + }) + await browser.url('https://guinea-pig.webdriver.io/') + await browser.execute(() => { + const script = document.createElement('script') + script.src = 'https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js' + document.body.appendChild(script) + }) + await browser.waitUntil(() => mock.calls.length >= 1, { + timeoutMsg: 'Expected middle wildcard mock to be called', + timeout: 5000 + }) + }) + + it('should mock with hostname wildcard', async () => { + const mock = await browser.mock('https://*.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js', { + method: 'get', + statusCode: 200, + }) + await browser.url('https://guinea-pig.webdriver.io/') + await browser.execute(() => { + const script = document.createElement('script') + script.src = 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js' + document.body.appendChild(script) + }) + await browser.waitUntil(() => mock.calls.length >= 1, { + timeoutMsg: 'Expected hostname wildcard mock to be called', + timeout: 5000 + }) + }) + + it('should mock with file extension wildcard', async () => { + const mock = await browser.mock('https://cdn.jsdelivr.net/npm/moment@2.29.1/moment.min.*', { + method: 'get', + statusCode: 200, + }) + await browser.url('https://guinea-pig.webdriver.io/') + await browser.execute(() => { + const script = document.createElement('script') + script.src = 'https://cdn.jsdelivr.net/npm/moment@2.29.1/moment.min.js' + document.body.appendChild(script) + }) + await browser.waitUntil(() => mock.calls.length >= 1, { + timeoutMsg: 'Expected extension wildcard mock to be called', + timeout: 5000 + }) + }) + + it('should mock with complex mixed wildcards', async () => { + // Matches https://cdn.jsdelivr.net/.../bootstrap.min.js + const mock = await browser.mock('https://*.jsdelivr.net/**/bootstrap.min.js', { + method: 'get', + statusCode: 200, + }) + await browser.url('https://guinea-pig.webdriver.io/') + await browser.execute(() => { + const script = document.createElement('script') + script.src = 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js' + document.body.appendChild(script) + }) + await browser.waitUntil(() => mock.calls.length >= 1, { + timeoutMsg: 'Expected complex wildcard mock to be called', + timeout: 5000 + }) + }) }) diff --git a/packages/webdriverio/src/utils/interception/index.ts b/packages/webdriverio/src/utils/interception/index.ts index 17fc2678aba..3f79673b5b0 100644 --- a/packages/webdriverio/src/utils/interception/index.ts +++ b/packages/webdriverio/src/utils/interception/index.ts @@ -39,7 +39,7 @@ export default class WebDriverInterception { #calls: local.NetworkResponseCompletedParameters[] = [] #responseBodies = new Map() - constructor ( + constructor( pattern: URLPattern, mockId: string, filterOptions: MockFilterOptions, @@ -92,7 +92,7 @@ export default class WebDriverInterception { return new WebDriverInterception(pattern, interception.intercept, filterOptions, browser) } - #emit (event: string, args: unknown) { + #emit(event: string, args: unknown) { if (!this.#eventHandler.has(event)) { return } @@ -103,7 +103,7 @@ export default class WebDriverInterception { } } - #addEventHandler (event: string, handler: Function) { + #addEventHandler(event: string, handler: Function) { if (!this.#eventHandler.has(event)) { this.#eventHandler.set(event, []) } @@ -119,6 +119,15 @@ export default class WebDriverInterception { * - request is not matching the pattern, e.g. a different mock is responsible for this request */ if (!this.#isRequestMatching(request)) { + /** + * if request is not matching pattern but blocked by this mock (due to catch-all), + * we need to continue the request + */ + if (request.intercepts?.includes(this.#mockId)) { + return this.#browser.networkContinueRequest({ + request: request.request.request + }) + } return } @@ -163,6 +172,15 @@ export default class WebDriverInterception { * - request is not matching the pattern, e.g. a different mock is responsible for this request */ if (!this.#isRequestMatching(request)) { + /** + * if request is not matching pattern but blocked by this mock (due to catch-all), + * we need to continue the request + */ + if (request.intercepts?.includes(this.#mockId)) { + return this.#browser.networkProvideResponse({ + request: request.request.request + }).catch(this.#handleNetworkProvideResponseError) + } return } @@ -269,12 +287,12 @@ export default class WebDriverInterception { return this.#responseBodies } - #isRequestMatching (request: T) { + #isRequestMatching(request: T) { const matches = this.#pattern && this.#pattern.test(request.request.url) return request.isBlocked && matches } - #matchesFilterOptions (request: T) { + #matchesFilterOptions(request: T) { let isRequestMatching = true if (isRequestMatching && this.#filterOptions.method) { @@ -482,7 +500,7 @@ export default class WebDriverInterception { } } - waitForResponse ({ + waitForResponse({ timeout = this.#browser.options.waitforTimeout, interval = this.#browser.options.waitforInterval, timeoutMsg, diff --git a/packages/webdriverio/src/utils/interception/utils.ts b/packages/webdriverio/src/utils/interception/utils.ts index c7cd668355c..a1e247476ab 100644 --- a/packages/webdriverio/src/utils/interception/utils.ts +++ b/packages/webdriverio/src/utils/interception/utils.ts @@ -59,7 +59,7 @@ export function parseOverwrite< const statusCodeOverwrite = typeof overwrite.statusCode === 'function' ? overwrite.statusCode(request as local.NetworkResponseCompletedParameters) : overwrite.statusCode - ;(result as RespondWithOptions).statusCode = statusCodeOverwrite + ; (result as RespondWithOptions).statusCode = statusCodeOverwrite } if ('method' in overwrite) { @@ -77,8 +77,9 @@ export function parseOverwrite< return result } -export function getPatternParam (pattern: URLPattern, key: keyof Omit) { - if (pattern[key] === '*') { +export function getPatternParam(pattern: URLPattern, key: keyof Omit) { + const value = pattern[key] + if (value === '*' || value.includes('*')) { return } @@ -86,5 +87,5 @@ export function getPatternParam (pattern: URLPattern, key: keyof Omit Date: Mon, 22 Dec 2025 22:10:09 -0700 Subject: [PATCH 4/4] Adding ability to pass in custom timeout for appium start time (#14939) Co-authored-by: Christian Bromann --- packages/wdio-appium-service/src/launcher.ts | 4 +- packages/wdio-appium-service/src/types.ts | 6 +++ .../tests/launcher.test.ts | 51 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/wdio-appium-service/src/launcher.ts b/packages/wdio-appium-service/src/launcher.ts index caaa26c1780..e9b21b2fbe5 100644 --- a/packages/wdio-appium-service/src/launcher.ts +++ b/packages/wdio-appium-service/src/launcher.ts @@ -164,7 +164,9 @@ export default class AppiumLauncher implements Services.ServiceInstance { * start Appium */ const command = await this._getCommand(this._options.command) - this._process = await this._startAppium(command, this._appiumCliArgs) + + const timeout = this._options.appiumStartTimeout ?? APPIUM_START_TIMEOUT + this._process = await this._startAppium(command, this._appiumCliArgs, timeout) if (this._logPath) { this._redirectLogStream(this._logPath) diff --git a/packages/wdio-appium-service/src/types.ts b/packages/wdio-appium-service/src/types.ts index 559ad8af56f..f68769416af 100644 --- a/packages/wdio-appium-service/src/types.ts +++ b/packages/wdio-appium-service/src/types.ts @@ -139,6 +139,12 @@ export interface AppiumServiceConfig { * @default {} */ args?: AppiumServerArguments + + /** + * Timeout in milliseconds for Appium to start successfully. + * @default 30000 + */ + appiumStartTimeout?: number } export type ArgValue = string | number | boolean | null | object diff --git a/packages/wdio-appium-service/tests/launcher.test.ts b/packages/wdio-appium-service/tests/launcher.test.ts index 82a9c0da613..29cea01f0ab 100644 --- a/packages/wdio-appium-service/tests/launcher.test.ts +++ b/packages/wdio-appium-service/tests/launcher.test.ts @@ -710,6 +710,45 @@ describe('Appium launcher', () => { await launcher.onPrepare() expect(launcher['_startAppium']).toHaveBeenCalledTimes(1) }) + + test('should use custom appiumStartTimeout when provided', async () => { + const options = { + logPath: './', + command: 'test/path', + args: { address: 'bar' }, + appiumStartTimeout: 60000 + } + const capabilities = [{ 'appium:deviceName': 'baz' }] as WebdriverIO.Capabilities[] + const launcher = new AppiumLauncher(options, capabilities, {} as any) + const startAppiumSpy = vi.spyOn(launcher as any, '_startAppium') + + await launcher.onPrepare() + + expect(startAppiumSpy).toHaveBeenCalledWith( + 'test/path', + expect.any(Array), + 60000 + ) + }) + + test('should use default timeout when appiumStartTimeout is not provided', async () => { + const options = { + logPath: './', + command: 'test/path', + args: { address: 'bar' } + } + const capabilities = [{ 'appium:deviceName': 'baz' }] as WebdriverIO.Capabilities[] + const launcher = new AppiumLauncher(options, capabilities, {} as any) + const startAppiumSpy = vi.spyOn(launcher as any, '_startAppium') + + await launcher.onPrepare() + + expect(startAppiumSpy).toHaveBeenCalledWith( + 'test/path', + expect.any(Array), + 30000 // Default APPIUM_START_TIMEOUT + ) + }) }) describe('onComplete', () => { @@ -965,6 +1004,18 @@ describe('Appium launcher', () => { expect(mockLogWarn).toHaveBeenCalled() expect(mockLogError).not.toHaveBeenCalled() }) + + test('should respect custom timeout from config', async () => { + const origSpawn = await vi.importActual('node:child_process').then((m) => m.spawn) + vi.mocked(spawn).mockImplementationOnce(origSpawn) + const launcher = new AppiumLauncher({ appiumStartTimeout: 5000 }, [{ 'appium:deviceName': 'baz' }], {} as any) + + await expect(launcher['_startAppium']( + 'node', + ['-e', '(() => { setTimeout(() => { console.log(JSON.stringify({message: \'Appium REST http interface listener started\'})); }, 3000); })()'], + 5000 + )).resolves.toEqual(expect.objectContaining({ spawnargs: expect.arrayContaining(['-e', expect.any(String)]) })) + }) }) afterEach(() => {