import path from 'node:path' import { describe, it, vi, expect, beforeEach } from 'vitest' import { remote } from '../../../src/index.js' import { getContextManager } from '../../../src/session/context.js' import { ELEMENT_KEY } from 'webdriver' let browser: WebdriverIO.Browser vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))) vi.mock('../../../src/session/context.ts', () => { const manager = { getCurrentContext: vi.fn().mockResolvedValue('5D4662C2B4465334DFD34239BA1E9E66'), setCurrentContext: vi.fn(), getFlatContextTree: vi.fn().mockResolvedValue([]), initialize: vi.fn(), findContext: vi.fn().mockImplementation((search, contexts, strategy) => { if (strategy === 'byUrl' && search === 'https://mno.com') { return { context: '5', url: 'https://mno.com' } } return undefined }) } return { getContextManager: () => manager } }) const contextManager = getContextManager({ on: vi.fn() } as any) describe('switchFrame command', () => { describe('non bidi', () => { beforeEach(async () => { browser = await remote({ baseUrl: 'http://foobar.com', capabilities: { browserName: 'foobar' } }) }) it('fails when applying a function parameter', async () => { await expect( browser.switchFrame((ctx: { url: string }) => Promise.resolve(ctx.url === 'https://mno.com')) ).rejects.toThrow('Cannot use a function to fetch a context in WebDriver Classic') }) it('fails when applying a string parameter', async () => { await expect( browser.switchFrame('https://mno.com') ).rejects.toThrow('Cannot use a string to fetch a context in WebDriver Classic') }) it('calls classic switchToFrame', async () => { const switchToFrame = vi.spyOn(browser, 'switchToFrame') await browser.switchFrame(await browser.$('iframe')) expect(switchToFrame).toHaveBeenCalledWith( expect.objectContaining({ elementId: 'some-elem-123' }) ) }) it('should switch context via unresolved WDIO element', async () => { const switchToFrame = vi.spyOn(browser, 'switchToFrame') await browser.switchFrame(browser.$('iframe')) expect(switchToFrame).toHaveBeenCalledWith( expect.objectContaining({ [ELEMENT_KEY]: 'some-elem-123' }) ) }) it('switch to parent frame', async () => { const switchToFrame = vi.spyOn(browser, 'switchToFrame') await browser.switchFrame(null) expect(switchToFrame).toHaveBeenCalledWith(null) }) }) describe('bidi', () => { beforeEach(async () => { vi.mocked(contextManager.initialize).mockClear() vi.mocked(contextManager.setCurrentContext).mockClear() vi.mocked(contextManager.getCurrentContext).mockClear() browser = await remote({ baseUrl: 'http://foobar.com', capabilities: { browserName: 'bidi' } }) const execute = vi.spyOn(browser, 'execute') execute.mockResolvedValue({ context: '5D4662C2B4465334DFD34239BA1E9E66' }) const browsingContextActivate = vi.spyOn(browser, 'browsingContextActivate') browsingContextActivate.mockResolvedValue({}) const getWindowHandle = vi.spyOn(browser, 'getWindowHandle') getWindowHandle.mockResolvedValue('5D4662C2B4465334DFD34239BA1E9E66') const browsingContextLocateNodes = vi.spyOn(browser, 'browsingContextLocateNodes') browsingContextLocateNodes.mockResolvedValue({ nodes: [] }) const browsingContextGetTree = vi.spyOn(browser, 'browsingContextGetTree') browsingContextGetTree.mockResolvedValue({ contexts: [{ children: [{ children: [], context: '2', url: 'https://def.com', userContext: 'default' }, { children: [{ children: [{ children: [], context: '5', url: 'https://mno.com', userContext: 'default' }], context: '4', url: 'https://jkl.com', userContext: 'default' }], context: '3', url: 'https://3.com', userContext: 'default' }], context: '1', url: 'https://abc.com', userContext: 'default' }] }) }) it('should switch context via element', async () => { const elem = await browser.$('iframe') const elemExecute = vi.spyOn(elem, 'execute') elemExecute.mockResolvedValue({ context: '5D4662C2B4465334DFD34239BA1E9E66' }) vi.spyOn(elem, 'waitForExist').mockResolvedValue(true) await browser.switchFrame(elem) expect(contextManager.setCurrentContext).toBeCalledWith('5D4662C2B4465334DFD34239BA1E9E66') }) it('should switch context via null', async () => { await browser.switchFrame(null) expect(contextManager.setCurrentContext).toBeCalledWith('5D4662C2B4465334DFD34239BA1E9E66') }) it('should NOT re-resolve if element already has an elementId', async () => { // Mock the resolved element const resolvedElem = { elementId: 'elem-456', [ELEMENT_KEY]: 'elem-456', selector: 'iframe', waitForExist: vi.fn().mockResolvedValue(true), isExisting: vi.fn().mockResolvedValue(true), isElement: true, getElement: vi.fn() } as any resolvedElem.getElement.mockResolvedValue(resolvedElem) const switchToFrame = vi.spyOn(browser, 'switchToFrame').mockResolvedValue(undefined) await browser.switchFrame(resolvedElem) expect(switchToFrame).toHaveBeenCalledWith(expect.objectContaining({ [ELEMENT_KEY]: 'elem-456' })) }) it('should re-resolve element if elementId is missing', async () => { // Mock the resolved element const resolvedElement = { selector: 'iframe', elementId: 'elem-789', [ELEMENT_KEY]: 'elem-789', waitForExist: vi.fn().mockResolvedValue(true), isExisting: vi.fn().mockResolvedValue(true), isElement: true } // Spy on browser.$ to simulate re-resolving the selector const $spy = vi.spyOn(browser, '$').mockResolvedValue(resolvedElement as any) const switchToFrame = vi.spyOn(browser, 'switchToFrame').mockResolvedValue(undefined) // Fake unresolved element (missing elementId/ELEMENT_KEY) const unresolvedElement = { selector: 'iframe', parent: browser, isElement: true, elementId: undefined, [ELEMENT_KEY]: undefined, async getElement() { if (!this.elementId && typeof this.selector === 'string') { const resolved = await this.parent.$(this.selector) this.elementId = resolved.elementId this[ELEMENT_KEY] = resolved[ELEMENT_KEY] return resolved } return this as any } } await browser.switchFrame(unresolvedElement as any) // Assert: re-resolution happened and switchToFrame was called with resolved element expect($spy).toHaveBeenCalledWith('iframe') expect(switchToFrame).toHaveBeenCalledWith(expect.objectContaining({ [ELEMENT_KEY]: 'elem-789' })) }) it('should switch context for delayed iframe URL', async () => { const resolvedTree = { '1': { context: '1', parent: null, url: 'https://abc.com', clientWindow: 'window-1', originalOpener: null, userContext: 'default', children: ['5'] }, '5': { context: '5', parent: '1', url: 'https://mno.com', clientWindow: 'window-5', originalOpener: null, userContext: 'default', children: [] } } const sessionContext = getContextManager(browser) // Mock `browsingContextGetTree` to simulate retry behavior let getTreeCall = 0 const browsingContextGetTreeMock = vi .spyOn(browser, 'browsingContextGetTree') .mockImplementation(() => { getTreeCall++ if (getTreeCall < 3) { return Promise.resolve({ contexts: [] }) // 1st & 2nd retries } return Promise.resolve({ contexts: [{ context: '5', url: 'https://mno.com', children: [] }] }) // 3rd call resolves }) // `findContext` returns match only on 3rd call let findCall = 0 vi.spyOn(sessionContext, 'findContext').mockImplementation((ctx, contexts, strategy) => { findCall++ if ( strategy === 'byUrl' && findCall >= 3 && Array.isArray(contexts) && contexts.some(c => c.url === 'https://mno.com') ) { return { context: '5', url: 'https://mno.com' } } return undefined }) // After resolving, context tree is fetched const getFlatContextTreeMock = vi .spyOn(sessionContext, 'getFlatContextTree') .mockResolvedValue(resolvedTree) // Locate iframe node in DOM vi.spyOn(browser, 'browsingContextLocateNodes').mockResolvedValue({ nodes: [{ sharedId: 'node-5', value: { nodeType: 1, childNodeCount: 0, attributes: { src: 'https://mno.com' } } }] }) // Resolve context from iframe node vi.spyOn(browser, 'scriptCallFunction').mockResolvedValue({ type: 'success', result: { type: 'window', value: { context: '5' } } }) // Ensure switchToFrame is called const switchToFrameMock = vi .spyOn(browser, 'switchToFrame') .mockResolvedValue(undefined) // Execute const result = await browser.switchFrame('https://mno.com') expect(result).toBe('5') // Assertions expect(browsingContextGetTreeMock).toHaveBeenCalledTimes(3) // Ensures retries expect(getFlatContextTreeMock).toHaveBeenCalledTimes(1) expect(switchToFrameMock).toHaveBeenCalled() }) }) })