diff --git a/.changeset/pr-119.md b/.changeset/pr-119.md new file mode 100644 index 0000000..12b2e1e --- /dev/null +++ b/.changeset/pr-119.md @@ -0,0 +1,5 @@ +--- +"@wdio/browserstack-service": patch +--- + +- Fixed BrowserStack executor commands (session name, status, annotations) being ignored in WebDriver BiDi sessions. diff --git a/packages/browserstack-service/src/service.ts b/packages/browserstack-service/src/service.ts index b9e2bbe..9daea81 100644 --- a/packages/browserstack-service/src/service.ts +++ b/packages/browserstack-service/src/service.ts @@ -242,6 +242,23 @@ export default class BrowserstackService implements Services.ServiceInstance { PerformanceTester.scenarioThatRan = this._scenariosThatRan if (this._browser) { + const patchBidiExecutorRouting = (resolveBrowser: () => WebdriverIO.Browser, label?: string) => { + try { + this._routeBidiExecutorToHttp(resolveBrowser()) + } catch (err) { + BStackLogger.warn(`Failed to patch execute for BiDi browserstack_executor routing${label ? ` on ${label}` : ''}; executor commands may not work in BiDi sessions: ${err}`) + } + } + + if (this._browser.isMultiremote) { + const multiRemoteBrowser = this._browser as unknown as WebdriverIO.MultiRemoteBrowser + Object.keys(this._caps).forEach((browserName) => { + patchBidiExecutorRouting(() => multiRemoteBrowser.getInstance(browserName), browserName) + }) + } else { + patchBidiExecutorRouting(() => this._browser as WebdriverIO.Browser) + } + try { const sessionId = this._browser.sessionId @@ -888,6 +905,19 @@ export default class BrowserstackService implements Services.ServiceInstance { }) } + _routeBidiExecutorToHttp (browser: WebdriverIO.Browser) { + if (!browser.isBidi) { + return + } + + browser.overwriteCommand('execute', async (originalExecute, script, ...args) => { + if (typeof script === 'string' && script.startsWith('browserstack_executor:')) { + return browser.executeScript(script, args) + } + return originalExecute(script, ...args) + }) + } + _multiRemoteAction (action: MultiRemoteAction) { if (!this._browser) { return Promise.resolve() diff --git a/packages/browserstack-service/tests/service.test.ts b/packages/browserstack-service/tests/service.test.ts index 95a2e32..7a28f01 100644 --- a/packages/browserstack-service/tests/service.test.ts +++ b/packages/browserstack-service/tests/service.test.ts @@ -113,6 +113,7 @@ beforeEach(() => { browser = { execute: vi.fn(), executeScript: vi.fn(), + overwriteCommand: vi.fn(), on: vi.fn(), sessionId: sessionId, config: {}, @@ -626,6 +627,86 @@ describe('before', () => { expect(service['_failReasons']).toEqual([]) expect(service['_sessionBaseUrl']).toEqual('https://api.browserstack.com/automate-turboscale/v1/sessions') }) + + it('should overwrite execute command to route browserstack_executor via executeScript', async () => { + (browser as any).isBidi = true + const service = new BrowserstackService({} as any, [{}] as any, { user: 'foo', key: 'bar', capabilities: {} }) + await service.before(service['_config'] as any, [], browser) + + expect(browser.overwriteCommand).toHaveBeenCalledWith('execute', expect.any(Function)) + + const overwrite = vi.mocked(browser.overwriteCommand).mock.calls[0][1] as Function + const originalExecute = vi.fn() + + await overwrite(originalExecute, 'browserstack_executor: {"action":"annotate"}') + expect(browser.executeScript).toHaveBeenCalledWith('browserstack_executor: {"action":"annotate"}', []) + expect(originalExecute).not.toHaveBeenCalled() + + await overwrite(originalExecute, 'return document.title') + expect(originalExecute).toHaveBeenCalledWith('return document.title') + + const extraArg = { key: 'value' } + await overwrite(originalExecute, 'return arguments[0]', extraArg) + expect(originalExecute).toHaveBeenCalledWith('return arguments[0]', extraArg) + }) + + it('should not overwrite execute command for non-BiDi sessions', async () => { + (browser as any).isBidi = false + const service = new BrowserstackService({} as any, [{}] as any, { user: 'foo', key: 'bar', capabilities: {} }) + await service.before(service['_config'] as any, [], browser) + + expect(browser.overwriteCommand).not.toHaveBeenCalled() + }) + + it('should overwrite execute on each instance for multiremote', async () => { + const browserA = { executeScript: vi.fn(), overwriteCommand: vi.fn(), sessionId: 'sessionA', isBidi: true } + const browserB = { executeScript: vi.fn(), overwriteCommand: vi.fn(), sessionId: 'sessionB', isBidi: true } + const multiRemoteBrowser = { + ...browser, + isMultiremote: true, + getInstance: vi.fn().mockImplementation((name: string) => name === 'browserA' ? browserA : browserB) + } as unknown as WebdriverIO.MultiRemoteBrowser + + const service = new BrowserstackService({} as any, { browserA: {}, browserB: {} } as any, { + user: 'foo', key: 'bar' + }) + await service.before(service['_config'] as any, [], multiRemoteBrowser as any) + + expect(browserA.overwriteCommand).toHaveBeenCalledWith('execute', expect.any(Function)) + expect(browserB.overwriteCommand).toHaveBeenCalledWith('execute', expect.any(Function)) + + const overwriteA = vi.mocked(browserA.overwriteCommand).mock.calls[0][1] as Function + await overwriteA(vi.fn(), 'browserstack_executor: {"action":"annotate"}') + expect(browserA.executeScript).toHaveBeenCalledWith('browserstack_executor: {"action":"annotate"}', []) + expect(browserB.executeScript).not.toHaveBeenCalled() + + const originalExecuteA = vi.fn() + const extraArg = { key: 'value' } + await overwriteA(originalExecuteA, 'return arguments[0]', extraArg) + expect(originalExecuteA).toHaveBeenCalledWith('return arguments[0]', extraArg) + }) + + it('should keep patching remaining multiremote instances when one instance fails to resolve', async () => { + const browserA = { executeScript: vi.fn(), overwriteCommand: vi.fn(), sessionId: 'sessionA', isBidi: true } + const browserB = { executeScript: vi.fn(), overwriteCommand: vi.fn(), sessionId: 'sessionB', isBidi: true } + const multiRemoteBrowser = { + ...browser, + isMultiremote: true, + getInstance: vi.fn() + .mockImplementationOnce(() => { + throw new Error('no such instance') + }) + .mockImplementation((name: string) => name === 'browserA' ? browserA : browserB) + } as unknown as WebdriverIO.MultiRemoteBrowser + + const service = new BrowserstackService({} as any, { browserA: {}, browserB: {} } as any, { + user: 'foo', key: 'bar' + }) + await service.before(service['_config'] as any, [], multiRemoteBrowser as any) + + expect(browserA.overwriteCommand).not.toHaveBeenCalled() + expect(browserB.overwriteCommand).toHaveBeenCalledWith('execute', expect.any(Function)) + }) }) describe('beforeHook', () => {