Skip to content

Commit ee4f276

Browse files
fix: disable mocks after restore
1 parent 821e272 commit ee4f276

9 files changed

Lines changed: 89 additions & 9 deletions

File tree

packages/webdriverio/src/commands/browser/mock.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import WebDriverNetworkInterception from '../../utils/interception/webdriver.js'
44
import { getBrowserObject } from '../../utils/index.js'
55
import type { Mock } from '../../types.js'
66
import type { MockFilterOptions } from '../../utils/interception/types.js'
7+
import type { CDPSession } from 'puppeteer-core/lib/esm/puppeteer/common/Connection.js'
78

89
export const SESSION_MOCKS: Record<string, Set<Interception>> = {}
10+
export const CDP_SESSIONS: Record<string, CDPSession> = {}
911

1012
/**
1113
* Mock the response of a request. You can define a mock based on a matching
@@ -158,7 +160,7 @@ export async function mock (
158160
page = pages[0]
159161
}
160162

161-
const client = await page.target().createCDPSession()
163+
const client = CDP_SESSIONS[handle] = await page.target().createCDPSession()
162164
await client.send('Fetch.enable', {
163165
patterns: [{ requestStage: 'Request' }, { requestStage: 'Response' }]
164166
})

packages/webdriverio/src/commands/browser/mockRestoreAll.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export async function mockRestoreAll () {
3232
for (const [handle, mocks] of Object.entries(SESSION_MOCKS)) {
3333
log.trace(`Clearing mocks for ${handle}`)
3434
for (const mock of mocks) {
35-
mock.restore()
35+
await mock.restore()
3636
}
3737
}
3838
}

packages/webdriverio/src/commands/mock/restore.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/**
22
* Does everything that `mock.clear()` does, and also removes any mocked return values or implementations.
3+
* Restored mock does not emit events and could not mock responses.
34
*
45
* <example>
56
:addValue.js

packages/webdriverio/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ type MultiRemoteElementCommands = {
116116
}
117117

118118
export type MultiRemoteBrowserCommandsType = {
119-
[K in keyof Omit<BrowserCommandsType, ElementCommandNames | 'SESSION_MOCKS'>]: (...args: Parameters<BrowserCommandsType[K]>) => Promise<ThenArg<ReturnType<BrowserCommandsType[K]>>[]>
119+
[K in keyof Omit<BrowserCommandsType, ElementCommandNames | 'SESSION_MOCKS' | 'CDP_SESSIONS'>]: (...args: Parameters<BrowserCommandsType[K]>) => Promise<ThenArg<ReturnType<BrowserCommandsType[K]>>[]>
120120
} & MultiRemoteElementCommands
121121
export type MultiRemoteElementCommandsType = {
122122
[K in keyof Omit<ElementCommandsType, ElementCommandNames>]: (...args: Parameters<ElementCommandsType[K]>) => Promise<ThenArg<ReturnType<ElementCommandsType[K]>>[]>

packages/webdriverio/src/utils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import type { CustomStrategyReference } from '../types.js'
2323

2424
const log = logger('webdriverio')
2525
const INVALID_SELECTOR_ERROR = 'selector needs to be typeof `string` or `function`'
26-
const IGNORED_COMMAND_FILE_EXPORTS = ['SESSION_MOCKS']
26+
const IGNORED_COMMAND_FILE_EXPORTS = ['SESSION_MOCKS', 'CDP_SESSIONS']
2727

2828
declare global {
2929
interface Window { __wdio_element: Record<string, HTMLElement> }

packages/webdriverio/src/utils/interception/devtools.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Interception from './index.js'
88
import type { Matches, MockOverwrite, MockResponseParams } from './types.js'
99
import { containsHeaderObject } from '../index.js'
1010
import { ERROR_REASON } from '../../constants.js'
11+
import { CDP_SESSIONS, SESSION_MOCKS } from '../../commands/browser/mock.js'
1112

1213
const log = logger('webdriverio')
1314

@@ -31,6 +32,8 @@ type Event = {
3132
type ExpectParameter<T> = ((param: T) => boolean) | T;
3233

3334
export default class DevtoolsInterception extends Interception {
35+
private restored = false
36+
3437
static handleRequestInterception (client: CDPSession, mocks: Set<Interception>): (event: Event) => Promise<void | ClientResponse> {
3538
return async (event) => {
3639
// responseHeaders and responseStatusCode are only present in Response stage
@@ -220,10 +223,27 @@ export default class DevtoolsInterception extends Interception {
220223
/**
221224
* Does everything that `mock.clear()` does, and also
222225
* removes any mocked return values or implementations.
226+
* Restored mock does not emit events and could not mock responses
223227
*/
224-
restore () {
228+
async restore (sessionMocks = SESSION_MOCKS, cdpSessions = CDP_SESSIONS) {
225229
this.clear()
226230
this.respondOverwrites = []
231+
this.restored = true
232+
const handle = await this.browser.getWindowHandle()
233+
234+
log.trace(`Restoring mock for ${handle}`)
235+
sessionMocks[handle].delete(this)
236+
237+
if (sessionMocks[handle].size) {
238+
return
239+
}
240+
241+
log.trace(`Disabling fetch domain for ${handle}`)
242+
return cdpSessions[handle].send('Fetch.disable')
243+
.then(() => {
244+
delete sessionMocks[handle]
245+
delete cdpSessions[handle]
246+
}).catch(/* istanbul ignore next */logFetchError)
227247
}
228248

229249
/**
@@ -232,6 +252,7 @@ export default class DevtoolsInterception extends Interception {
232252
* @param {*} params additional respond parameters to overwrite
233253
*/
234254
respond (overwrite: MockOverwrite, params: MockResponseParams = {}) {
255+
this.ensureNotRestored()
235256
this.respondOverwrites.push({ overwrite, params, sticky: true })
236257
}
237258

@@ -241,6 +262,7 @@ export default class DevtoolsInterception extends Interception {
241262
* @param {*} params additional respond parameters to overwrite
242263
*/
243264
respondOnce (overwrite: MockOverwrite, params: MockResponseParams = {}) {
265+
this.ensureNotRestored()
244266
this.respondOverwrites.push({ overwrite, params })
245267
}
246268

@@ -249,6 +271,7 @@ export default class DevtoolsInterception extends Interception {
249271
* @param {string} errorCode error code of the response
250272
*/
251273
abort (errorReason: Protocol.Network.ErrorReason, sticky: boolean = true) {
274+
this.ensureNotRestored()
252275
if (typeof errorReason !== 'string' || !ERROR_REASON.includes(errorReason)) {
253276
throw new Error(`Invalid value for errorReason, allowed are: ${ERROR_REASON.join(', ')}`)
254277
}
@@ -262,6 +285,12 @@ export default class DevtoolsInterception extends Interception {
262285
abortOnce (errorReason: Protocol.Network.ErrorReason) {
263286
this.abort(errorReason, false)
264287
}
288+
289+
private ensureNotRestored() {
290+
if (this.restored) {
291+
throw new Error('This can\'t be done on restored mock')
292+
}
293+
}
265294
}
266295

267296
const filterMethod = (method: string, expected?: ExpectParameter<string>) => {

packages/webdriverio/src/utils/interception/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { Protocol } from 'devtools-protocol'
1111
export default abstract class Interception extends EventEmitter {
1212
abstract calls: Matches[] | Promise<Matches[]>
1313
abstract clear (): void
14-
abstract restore (): void
14+
abstract restore (): Promise<void>
1515
abstract respond (overwrite: MockOverwrite, params: MockResponseParams): void
1616
abstract respondOnce (overwrite: MockOverwrite, params: MockResponseParams): void
1717
abstract abort (errorReason: Protocol.Network.ErrorReason, sticky: boolean): void

packages/webdriverio/src/utils/interception/webdriver.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ export default class WebDriverInterception extends Interception {
4747
* Does everything that `mock.clear()` does, and also
4848
* removes any mocked return values or implementations.
4949
*/
50-
restore () {
51-
return this.browser.call(
50+
async restore () {
51+
await this.browser.call(
5252
async () => this.browser.clearMockCalls(this.mockId as string, true))
5353
}
5454

packages/webdriverio/tests/utils/interception/devtools.test.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -645,7 +645,12 @@ test('allows to clear mocks', async () => {
645645
})
646646

647647
test('allows to restore mocks', async () => {
648+
const browserMock = {
649+
getWindowHandle: vi.fn().mockResolvedValue('window-handle')
650+
} as any as Browser
648651
const mock = new NetworkInterception('**/foobar/**', undefined, browserMock)
652+
const sessionMocks = { 'window-handle': new Set([mock]) }
653+
const cdpSessions = { 'window-handle': cdpClient }
649654
mock.respondOnce({ foo: 'bar' })
650655
mock.respond({ bar: 'foo' })
651656

@@ -661,6 +666,49 @@ test('allows to restore mocks', async () => {
661666
})
662667
expect(mock.respondOverwrites.length).toBe(1)
663668

664-
mock.restore()
669+
mock.restore(sessionMocks, cdpSessions)
665670
expect(mock.respondOverwrites.length).toBe(0)
666671
})
672+
673+
test('removes mock after restore', async () => {
674+
const browserMock = {
675+
getWindowHandle: vi.fn().mockResolvedValue('window-handle')
676+
} as any as Browser
677+
const mock = new NetworkInterception('**/foobar/**', undefined, browserMock)
678+
const sessionMocks = { 'window-handle': new Set([mock]) }
679+
const cdpSessions = { 'window-handle': cdpClient }
680+
681+
await mock.restore(sessionMocks, cdpSessions)
682+
683+
expect(sessionMocks).toEqual({})
684+
})
685+
686+
test('disables fetch domain after restore, if there are no other mocks', async () => {
687+
const browserMock = {
688+
getWindowHandle: vi.fn().mockResolvedValue('window-handle')
689+
} as any as Browser
690+
const mock = new NetworkInterception('**/foobar/**', undefined, browserMock)
691+
const sessionMocks = { 'window-handle': new Set([mock]) }
692+
const cdpSessions = { 'window-handle': cdpClient }
693+
694+
await mock.restore(sessionMocks, cdpSessions)
695+
696+
expect(cdpClient.send).toBeCalledWith('Fetch.disable')
697+
expect(cdpSessions).toEqual({})
698+
})
699+
700+
test('does not disable fetch domain after restore, if there are other mocks', async () => {
701+
const browserMock = {
702+
getWindowHandle: vi.fn().mockResolvedValue('window-handle')
703+
} as any as Browser
704+
const firstMock = new NetworkInterception('**/foobar/**', undefined, browserMock)
705+
const secondMock = new NetworkInterception('**/foobar/**', undefined, browserMock)
706+
const sessionMocks = { 'window-handle': new Set([firstMock, secondMock]) }
707+
const cdpSessions = { 'window-handle': cdpClient }
708+
709+
await firstMock.restore(sessionMocks, cdpSessions)
710+
711+
expect(cdpClient.send).not.toBeCalledWith('Fetch.disable')
712+
expect(sessionMocks).toEqual({ 'window-handle': new Set([secondMock]) })
713+
expect(cdpSessions).toEqual({ 'window-handle': cdpClient })
714+
})

0 commit comments

Comments
 (0)