Skip to content

Commit

Permalink
fix: skip interceptors for non-configurable globals (#665)
Browse files Browse the repository at this point in the history
  • Loading branch information
kettanaito authored Oct 30, 2024
1 parent 7e62e17 commit 2ec79d7
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 12 deletions.
23 changes: 16 additions & 7 deletions src/interceptors/WebSocket/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
WebSocketOverride,
} from './WebSocketOverride'
import { bindEvent } from './utils/bindEvent'
import { hasConfigurableGlobal } from '../../utils/hasConfigurableGlobal'

export { type WebSocketData, WebSocketTransport } from './WebSocketTransport'
export {
Expand Down Expand Up @@ -57,15 +58,16 @@ export class WebSocketInterceptor extends Interceptor<WebSocketEventMap> {
}

protected checkEnvironment(): boolean {
// Enable this interceptor in any environment
// that has a global WebSocket API.
return typeof globalThis.WebSocket !== 'undefined'
return hasConfigurableGlobal('WebSocket')
}

protected setup(): void {
const originalWebSocket = globalThis.WebSocket
const originalWebSocketDescriptor = Object.getOwnPropertyDescriptor(
globalThis,
'WebSocket'
)

const webSocketProxy = new Proxy(globalThis.WebSocket, {
const WebSocketProxy = new Proxy(globalThis.WebSocket, {
construct: (
target,
args: ConstructorParameters<typeof globalThis.WebSocket>,
Expand Down Expand Up @@ -152,10 +154,17 @@ export class WebSocketInterceptor extends Interceptor<WebSocketEventMap> {
},
})

globalThis.WebSocket = webSocketProxy
Object.defineProperty(globalThis, 'WebSocket', {
value: WebSocketProxy,
configurable: true,
})

this.subscriptions.push(() => {
globalThis.WebSocket = originalWebSocket
Object.defineProperty(
globalThis,
'WebSocket',
originalWebSocketDescriptor!
)
})
}
}
3 changes: 2 additions & 1 deletion src/interceptors/XMLHttpRequest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Emitter } from 'strict-event-emitter'
import { HttpRequestEventMap, IS_PATCHED_MODULE } from '../../glossary'
import { Interceptor } from '../../Interceptor'
import { createXMLHttpRequestProxy } from './XMLHttpRequestProxy'
import { hasConfigurableGlobal } from '../../utils/hasConfigurableGlobal'

export type XMLHttpRequestEmitter = Emitter<HttpRequestEventMap>

Expand All @@ -14,7 +15,7 @@ export class XMLHttpRequestInterceptor extends Interceptor<HttpRequestEventMap>
}

protected checkEnvironment() {
return typeof globalThis.XMLHttpRequest !== 'undefined'
return hasConfigurableGlobal('XMLHttpRequest')
}

protected setup() {
Expand Down
6 changes: 2 additions & 4 deletions src/interceptors/fetch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { RESPONSE_STATUS_CODES_WITH_REDIRECT } from '../../utils/responseUtils'
import { createNetworkError } from './utils/createNetworkError'
import { followFetchRedirect } from './utils/followRedirect'
import { decompressResponse } from './utils/decompression'
import { hasConfigurableGlobal } from '../../utils/hasConfigurableGlobal'

export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
static symbol = Symbol('fetch')
Expand All @@ -20,10 +21,7 @@ export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
}

protected checkEnvironment() {
return (
typeof globalThis !== 'undefined' &&
typeof globalThis.fetch !== 'undefined'
)
return hasConfigurableGlobal('fetch')
}

protected async setup() {
Expand Down
59 changes: 59 additions & 0 deletions src/utils/hasConfigurableGlobal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { vi, beforeAll, afterEach, afterAll, it, expect } from 'vitest'
import { hasConfigurableGlobal } from './hasConfigurableGlobal'

beforeAll(() => {
vi.spyOn(console, 'error').mockImplementation(() => {})
})

afterEach(() => {
vi.resetAllMocks()
})

afterAll(() => {
vi.restoreAllMocks()
})

it('returns true if the global property exists and is configurable', () => {
Object.defineProperty(global, '_existsAndConfigurable', {
value: 'something',
configurable: true,
})

expect(hasConfigurableGlobal('_existsAndConfigurable')).toBe(true)
})

it('returns false if the global property does not exist', () => {
expect(hasConfigurableGlobal('_non-existing')).toBe(false)
})

it('returns false and prints an error for implicitly non-configurable global property', () => {
Object.defineProperty(global, '_implicitlyNonConfigurable', {
value: 'something',
})

expect(hasConfigurableGlobal('_implicitlyNonConfigurable')).toBe(false)
expect(console.error).toHaveBeenCalledWith(
'[MSW] Failed to apply interceptor: the global `_implicitlyNonConfigurable` property is non-configurable. This is likely an issue with your environment. If you are using a framework, please open an issue about this in their repository.'
)
})

it('returns false and prints an error for explicitly non-configurable global property', () => {
Object.defineProperty(global, '_explicitlyNonConfigurable', {
value: 'something',
configurable: false,
})

expect(hasConfigurableGlobal('_explicitlyNonConfigurable')).toBe(false)
expect(console.error).toHaveBeenCalledWith(
'[MSW] Failed to apply interceptor: the global `_explicitlyNonConfigurable` property is non-configurable. This is likely an issue with your environment. If you are using a framework, please open an issue about this in their repository.'
)
})

it('returns false and prints an error for global property that only has a getter', () => {
Object.defineProperty(global, '_onlyGetter', { get: () => 'something' })

expect(hasConfigurableGlobal('_onlyGetter')).toBe(false)
expect(console.error).toHaveBeenCalledWith(
'[MSW] Failed to apply interceptor: the global `_onlyGetter` property is non-configurable. This is likely an issue with your environment. If you are using a framework, please open an issue about this in their repository.'
)
})
20 changes: 20 additions & 0 deletions src/utils/hasConfigurableGlobal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Returns a boolean indicating whether the given global property
* is defined and is configurable.
*/
export function hasConfigurableGlobal(propertyName: string): boolean {
const descriptor = Object.getOwnPropertyDescriptor(globalThis, propertyName)

if (typeof descriptor === 'undefined') {
return false
}

if (typeof descriptor.set === 'undefined' && !descriptor.configurable) {
console.error(
`[MSW] Failed to apply interceptor: the global \`${propertyName}\` property is non-configurable. This is likely an issue with your environment. If you are using a framework, please open an issue about this in their repository.`
)
return false
}

return true
}
3 changes: 3 additions & 0 deletions vitest.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ export default defineConfig({
test: {
include: ['./src/**/*.test.ts'],
},
esbuild: {
target: 'es2022',
},
})

0 comments on commit 2ec79d7

Please sign in to comment.