From 5a3f8140bbb22f8a6415e5b9117aca583d800b15 Mon Sep 17 00:00:00 2001 From: Ignacio Tomas Crespo Date: Mon, 26 Jan 2026 09:03:24 -0300 Subject: [PATCH 1/4] fix: use addEventListener for reliable response capture The previous implementation overrode onreadystatechange directly, which failed when handlers were set after send() was called. This fix uses addEventListener('readystatechange', ...) instead, which reliably captures responses regardless of when other handlers are attached. fix: implement custom XHRInterceptor to avoid RN private API warnings Replace dependency on React Native's internal XHRInterceptor with a custom implementation that directly patches XMLHttpRequest prototype. This fixes the deprecation warning in React Native 0.80+: "Deep imports from the 'react-native' package are deprecated" The new implementation: - Patches XMLHttpRequest.prototype.open, send, setRequestHeader - Captures response headers and body via onreadystatechange - Properly restores original methods when interception is disabled - Maintains the same callback API for compatibility with Logger.ts - Works across all React Native versions without version-specific paths --- src/XHRInterceptor.ts | 247 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 221 insertions(+), 26 deletions(-) diff --git a/src/XHRInterceptor.ts b/src/XHRInterceptor.ts index 4e9fcd3..52b236a 100644 --- a/src/XHRInterceptor.ts +++ b/src/XHRInterceptor.ts @@ -1,32 +1,227 @@ -type XHRInterceptorModule = { - isInterceptorEnabled: () => boolean; - setOpenCallback: (...props: any[]) => void; - setRequestHeaderCallback: (...props: any[]) => void; - setSendCallback: (...props: any[]) => void; - setHeaderReceivedCallback: (...props: any[]) => void; - setResponseCallback: (...props: any[]) => void; - enableInterception: () => void; - disableInterception: () => void; -}; +// Type declarations for globals provided by React Native runtime +declare class URL { + constructor(url: string, base?: string); + toString(): string; +} + +declare class XMLHttpRequest { + static readonly UNSENT: number; + static readonly OPENED: number; + static readonly HEADERS_RECEIVED: number; + static readonly LOADING: number; + static readonly DONE: number; + + readonly readyState: number; + readonly response: any; + readonly responseText: string; + responseType: string; + readonly responseURL: string; + readonly status: number; + timeout: number; + + onreadystatechange: ((this: XMLHttpRequest, ev: any) => any) | null; + + open(method: string, url: string | URL, async?: boolean, username?: string | null, password?: string | null): void; + send(body?: any): void; + setRequestHeader(header: string, value: string): void; + getResponseHeader(name: string): string | null; + getAllResponseHeaders(): string; + addEventListener(type: string, listener: (this: XMLHttpRequest, ev: any) => any): void; +} + +// Callback types use 'any' to match React Native's XHRInterceptor API +// This allows Logger.ts to use its own types (RequestMethod, XHR, etc.) +type OpenCallback = (...args: any[]) => void; +type RequestHeaderCallback = (...args: any[]) => void; +type SendCallback = (...args: any[]) => void; +type HeaderReceivedCallback = (...args: any[]) => void; +type ResponseCallback = (...args: any[]) => void; + +// Store original XMLHttpRequest methods +let originalXHROpen: typeof XMLHttpRequest.prototype.open | null = null; +let originalXHRSend: typeof XMLHttpRequest.prototype.send | null = null; +let originalXHRSetRequestHeader: typeof XMLHttpRequest.prototype.setRequestHeader | null = + null; + +// Callbacks +let openCallback: OpenCallback = () => {}; +let requestHeaderCallback: RequestHeaderCallback = () => {}; +let sendCallback: SendCallback = () => {}; +let headerReceivedCallback: HeaderReceivedCallback = () => {}; +let responseCallback: ResponseCallback = () => {}; -let XHRInterceptor: XHRInterceptorModule; -try { - // new location for React Native 0.80+ - const module = require('react-native/src/private/devsupport/devmenu/elementinspector/XHRInterceptor'); - XHRInterceptor = module.default ?? module; -} catch { - try { - // new location for React Native 0.79+ - const module = require('react-native/src/private/inspector/XHRInterceptor'); - XHRInterceptor = module.default ?? module; - } catch { - try { - const module = require('react-native/Libraries/Network/XHRInterceptor'); - XHRInterceptor = module.default ?? module; - } catch { - throw new Error('XHRInterceptor could not be found in either location'); +let isInterceptorEnabled = false; + +function parseResponseHeaders(headersString: string): Record { + const headers: Record = {}; + if (!headersString) return headers; + + const headerLines = headersString.trim().split('\r\n'); + for (const line of headerLines) { + const index = line.indexOf(':'); + if (index > 0) { + const key = line.substring(0, index).trim(); + const value = line.substring(index + 1).trim(); + headers[key] = value; } } + return headers; +} + +function enableInterception(): void { + if (isInterceptorEnabled) return; + + // Store original methods + originalXHROpen = XMLHttpRequest.prototype.open; + originalXHRSend = XMLHttpRequest.prototype.send; + originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; + + // Override open + XMLHttpRequest.prototype.open = function ( + method: string, + url: string | URL, + async: boolean = true, + username?: string | null, + password?: string | null + ): void { + const xhr = this as any; + xhr._interception = { + method, + url: url.toString(), + }; + + openCallback(method, url.toString(), this); + + return originalXHROpen!.call( + this, + method, + url, + async, + username ?? null, + password ?? null + ); + }; + + // Override setRequestHeader + XMLHttpRequest.prototype.setRequestHeader = function ( + header: string, + value: string + ): void { + requestHeaderCallback(header, value, this); + return originalXHRSetRequestHeader!.call(this, header, value); + }; + + // Override send + XMLHttpRequest.prototype.send = function (body?: any): void { + const xhr = this as any; + + const dataString = body === null || body === undefined ? '' : String(body); + sendCallback(dataString, xhr); + + // Use addEventListener which is more reliable than overriding onreadystatechange + // This works even when handlers are set after send() is called + this.addEventListener('readystatechange', function () { + if (this.readyState === XMLHttpRequest.HEADERS_RECEIVED) { + if (!xhr._interception?.hasCalledHeaderReceived) { + if (xhr._interception) { + xhr._interception.hasCalledHeaderReceived = true; + } + const contentType = this.getResponseHeader('content-type') || ''; + const contentLength = this.getResponseHeader('content-length'); + const responseSize = contentLength ? parseInt(contentLength, 10) : 0; + const responseHeaders = parseResponseHeaders( + this.getAllResponseHeaders() + ); + + // Set responseHeaders on xhr for compatibility with Logger.ts + xhr.responseHeaders = responseHeaders; + + headerReceivedCallback(contentType, responseSize, responseHeaders, xhr); + } + } + + if (this.readyState === XMLHttpRequest.DONE) { + if (!xhr._interception?.hasCalledResponse) { + if (xhr._interception) { + xhr._interception.hasCalledResponse = true; + } + let responseData = ''; + if (this.responseType === '' || this.responseType === 'text') { + responseData = this.responseText || ''; + } else if (this.responseType === 'json' && this.response) { + try { + responseData = JSON.stringify(this.response); + } catch { + responseData = '[Unable to stringify response]'; + } + } else if (this.response) { + responseData = '[Non-text response]'; + } + + responseCallback( + this.status, + this.timeout, + responseData, + this.responseURL, + this.responseType, + xhr + ); + } + } + }); + + return originalXHRSend!.call(this, body); + }; + + isInterceptorEnabled = true; +} + +function disableInterception(): void { + if (!isInterceptorEnabled) return; + + // Restore original methods + if (originalXHROpen) { + XMLHttpRequest.prototype.open = originalXHROpen; + originalXHROpen = null; + } + if (originalXHRSend) { + XMLHttpRequest.prototype.send = originalXHRSend; + originalXHRSend = null; + } + if (originalXHRSetRequestHeader) { + XMLHttpRequest.prototype.setRequestHeader = originalXHRSetRequestHeader; + originalXHRSetRequestHeader = null; + } + + // Reset callbacks + openCallback = () => {}; + requestHeaderCallback = () => {}; + sendCallback = () => {}; + headerReceivedCallback = () => {}; + responseCallback = () => {}; + + isInterceptorEnabled = false; } +const XHRInterceptor = { + isInterceptorEnabled: () => isInterceptorEnabled, + setOpenCallback: (callback: OpenCallback) => { + openCallback = callback; + }, + setRequestHeaderCallback: (callback: RequestHeaderCallback) => { + requestHeaderCallback = callback; + }, + setSendCallback: (callback: SendCallback) => { + sendCallback = callback; + }, + setHeaderReceivedCallback: (callback: HeaderReceivedCallback) => { + headerReceivedCallback = callback; + }, + setResponseCallback: (callback: ResponseCallback) => { + responseCallback = callback; + }, + enableInterception, + disableInterception, +}; + export default XHRInterceptor; From 442e48d80a752a4089decf6b3cd2432eda60944f Mon Sep 17 00:00:00 2001 From: Alex Brazier Date: Fri, 6 Mar 2026 15:59:28 +0000 Subject: [PATCH 2/4] Lint --- src/XHRInterceptor.ts | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/XHRInterceptor.ts b/src/XHRInterceptor.ts index 52b236a..ca1f74b 100644 --- a/src/XHRInterceptor.ts +++ b/src/XHRInterceptor.ts @@ -21,12 +21,21 @@ declare class XMLHttpRequest { onreadystatechange: ((this: XMLHttpRequest, ev: any) => any) | null; - open(method: string, url: string | URL, async?: boolean, username?: string | null, password?: string | null): void; + open( + method: string, + url: string | URL, + async?: boolean, + username?: string | null, + password?: string | null + ): void; send(body?: any): void; setRequestHeader(header: string, value: string): void; getResponseHeader(name: string): string | null; getAllResponseHeaders(): string; - addEventListener(type: string, listener: (this: XMLHttpRequest, ev: any) => any): void; + addEventListener( + type: string, + listener: (this: XMLHttpRequest, ev: any) => any + ): void; } // Callback types use 'any' to match React Native's XHRInterceptor API @@ -40,8 +49,9 @@ type ResponseCallback = (...args: any[]) => void; // Store original XMLHttpRequest methods let originalXHROpen: typeof XMLHttpRequest.prototype.open | null = null; let originalXHRSend: typeof XMLHttpRequest.prototype.send | null = null; -let originalXHRSetRequestHeader: typeof XMLHttpRequest.prototype.setRequestHeader | null = - null; +let originalXHRSetRequestHeader: + | typeof XMLHttpRequest.prototype.setRequestHeader + | null = null; // Callbacks let openCallback: OpenCallback = () => {}; @@ -136,7 +146,12 @@ function enableInterception(): void { // Set responseHeaders on xhr for compatibility with Logger.ts xhr.responseHeaders = responseHeaders; - headerReceivedCallback(contentType, responseSize, responseHeaders, xhr); + headerReceivedCallback( + contentType, + responseSize, + responseHeaders, + xhr + ); } } From 63840044acf0e2d488f63ce566cbceeb6e5952e5 Mon Sep 17 00:00:00 2001 From: Alex Brazier Date: Fri, 6 Mar 2026 16:10:49 +0000 Subject: [PATCH 3/4] fix: handle null/undefined responses and improve error handling in XHR interceptor --- src/NetworkRequestInfo.ts | 32 ++++- src/XHRInterceptor.spec.ts | 176 +++++++++++++++++++++++ src/XHRInterceptor.ts | 120 +++++++++------- src/__tests__/NetworkRequestInfo.spec.ts | 64 +++++---- 4 files changed, 311 insertions(+), 81 deletions(-) create mode 100644 src/XHRInterceptor.spec.ts diff --git a/src/NetworkRequestInfo.ts b/src/NetworkRequestInfo.ts index 5f2ff1d..47af05c 100644 --- a/src/NetworkRequestInfo.ts +++ b/src/NetworkRequestInfo.ts @@ -108,9 +108,11 @@ export default class NetworkRequestInfo { } private async parseResponseBlob() { - const blobReader = new BlobFileReader(); - blobReader.readAsText(this.response); + if (this.response === null || this.response === undefined) { + return ''; + } + const blobReader = new BlobFileReader(); return await new Promise((resolve, reject) => { const handleError = () => reject(blobReader.error); @@ -119,14 +121,32 @@ export default class NetworkRequestInfo { }); blobReader.addEventListener('error', handleError); blobReader.addEventListener('abort', handleError); + + try { + blobReader.readAsText(this.response); + } catch (error) { + reject(error); + } }); } async getResponseBody() { - const body = await (this.responseType !== 'blob' - ? this.response - : this.parseResponseBlob()); + if (this.endTime === 0 && this.status < 0) { + return 'Pending response...'; + } - return this.stringifyFormat(body); + try { + const body = await (this.responseType !== 'blob' + ? this.response + : this.parseResponseBlob()); + + if (body === '' || body === null || body === undefined) { + return ''; + } + + return this.stringifyFormat(body); + } catch { + return '[Unable to load response body]'; + } } } diff --git a/src/XHRInterceptor.spec.ts b/src/XHRInterceptor.spec.ts new file mode 100644 index 0000000..1aab923 --- /dev/null +++ b/src/XHRInterceptor.spec.ts @@ -0,0 +1,176 @@ +type ReadyStateListener = () => void; + +class FakeXMLHttpRequest { + static readonly UNSENT = 0; + static readonly OPENED = 1; + static readonly HEADERS_RECEIVED = 2; + static readonly LOADING = 3; + static readonly DONE = 4; + + readyState = FakeXMLHttpRequest.UNSENT; + response: any = ''; + responseText = ''; + responseType = ''; + responseURL = ''; + status = 0; + timeout = 0; + onreadystatechange = null; + + openCalled = false; + sendCalled = false; + setRequestHeaderCalled = false; + + private listeners: Record = {}; + private responseHeaders: Record = {}; + + open( + _method?: string, + _url?: string, + _async?: boolean, + _username?: string | null, + _password?: string | null + ) { + this.openCalled = true; + } + + send(_data?: any) { + this.sendCalled = true; + } + + setRequestHeader(_header?: string, _value?: string) { + this.setRequestHeaderCalled = true; + } + + getResponseHeader(name: string): string | null { + return this.responseHeaders[name.toLowerCase()] || null; + } + + getAllResponseHeaders(): string { + return Object.entries(this.responseHeaders) + .map(([name, value]) => `${name}: ${value}`) + .join('\r\n'); + } + + addEventListener(type: string, listener: ReadyStateListener) { + if (!this.listeners[type]) { + this.listeners[type] = []; + } + this.listeners[type].push(listener.bind(this)); + } + + setResponseHeaders(headers: Record) { + this.responseHeaders = headers; + } + + emitReadyState(state: number) { + this.readyState = state; + (this.listeners.readystatechange || []).forEach((listener) => listener()); + } +} + +const loadInterceptor = () => { + jest.resetModules(); + return require('./XHRInterceptor').default; +}; + +describe('XHRInterceptor', () => { + beforeEach(() => { + (global as any).XMLHttpRequest = FakeXMLHttpRequest; + }); + + it('does not block XHR methods when interceptor callbacks throw', () => { + const interceptor = loadInterceptor(); + + interceptor.setOpenCallback(() => { + throw new Error('open failed'); + }); + interceptor.setRequestHeaderCallback(() => { + throw new Error('header failed'); + }); + interceptor.setSendCallback(() => { + throw new Error('send failed'); + }); + + interceptor.enableInterception(); + + const xhr = new FakeXMLHttpRequest(); + expect(() => xhr.open('GET', 'https://example.com')).not.toThrow(); + expect(() => xhr.setRequestHeader('x-test', '1')).not.toThrow(); + expect(() => xhr.send('data')).not.toThrow(); + + expect(xhr.openCalled).toBe(true); + expect(xhr.setRequestHeaderCalled).toBe(true); + expect(xhr.sendCalled).toBe(true); + + interceptor.disableInterception(); + }); + + it('does not require addEventListener to exist', () => { + const interceptor = loadInterceptor(); + interceptor.enableInterception(); + + const xhr = new FakeXMLHttpRequest() as any; + xhr.addEventListener = undefined; + + expect(() => xhr.open('GET', 'https://example.com')).not.toThrow(); + expect(() => xhr.send()).not.toThrow(); + expect(xhr.sendCalled).toBe(true); + + interceptor.disableInterception(); + }); + + it('passes non-text response objects through callback for blob responses', () => { + const interceptor = loadInterceptor(); + const responseCallback = jest.fn(); + interceptor.setResponseCallback(responseCallback); + interceptor.enableInterception(); + + const xhr = new FakeXMLHttpRequest(); + const responseBlob = { _data: 'blob' }; + + xhr.responseType = 'blob'; + xhr.response = responseBlob; + xhr.status = 200; + xhr.timeout = 123; + xhr.responseURL = 'https://example.com'; + + xhr.open('GET', 'https://example.com'); + xhr.send(); + xhr.emitReadyState(FakeXMLHttpRequest.DONE); + + expect(responseCallback).toHaveBeenCalledTimes(1); + expect(responseCallback.mock.calls[0][2]).toBe(responseBlob); + + interceptor.disableInterception(); + }); + + it('invokes header and response callbacks once each', () => { + const interceptor = loadInterceptor(); + const headerReceivedCallback = jest.fn(); + const responseCallback = jest.fn(); + interceptor.setHeaderReceivedCallback(headerReceivedCallback); + interceptor.setResponseCallback(responseCallback); + interceptor.enableInterception(); + + const xhr = new FakeXMLHttpRequest(); + xhr.setResponseHeaders({ + 'content-type': 'application/json; charset=utf-8', + 'content-length': '42', + }); + xhr.responseType = 'text'; + xhr.responseText = '{"ok":true}'; + + xhr.open('GET', 'https://example.com'); + xhr.send(); + + xhr.emitReadyState(FakeXMLHttpRequest.HEADERS_RECEIVED); + xhr.emitReadyState(FakeXMLHttpRequest.HEADERS_RECEIVED); + xhr.emitReadyState(FakeXMLHttpRequest.DONE); + xhr.emitReadyState(FakeXMLHttpRequest.DONE); + + expect(headerReceivedCallback).toHaveBeenCalledTimes(1); + expect(responseCallback).toHaveBeenCalledTimes(1); + + interceptor.disableInterception(); + }); +}); diff --git a/src/XHRInterceptor.ts b/src/XHRInterceptor.ts index ca1f74b..2464212 100644 --- a/src/XHRInterceptor.ts +++ b/src/XHRInterceptor.ts @@ -100,7 +100,11 @@ function enableInterception(): void { url: url.toString(), }; - openCallback(method, url.toString(), this); + try { + openCallback(method, url.toString(), this); + } catch { + // Interceptor callbacks must not break network requests. + } return originalXHROpen!.call( this, @@ -117,7 +121,11 @@ function enableInterception(): void { header: string, value: string ): void { - requestHeaderCallback(header, value, this); + try { + requestHeaderCallback(header, value, this); + } catch { + // Interceptor callbacks must not break network requests. + } return originalXHRSetRequestHeader!.call(this, header, value); }; @@ -126,64 +134,78 @@ function enableInterception(): void { const xhr = this as any; const dataString = body === null || body === undefined ? '' : String(body); - sendCallback(dataString, xhr); + try { + sendCallback(dataString, xhr); + } catch { + // Interceptor callbacks must not break network requests. + } // Use addEventListener which is more reliable than overriding onreadystatechange // This works even when handlers are set after send() is called - this.addEventListener('readystatechange', function () { - if (this.readyState === XMLHttpRequest.HEADERS_RECEIVED) { - if (!xhr._interception?.hasCalledHeaderReceived) { - if (xhr._interception) { - xhr._interception.hasCalledHeaderReceived = true; + if (typeof this.addEventListener === 'function') { + this.addEventListener('readystatechange', function () { + if (this.readyState === XMLHttpRequest.HEADERS_RECEIVED) { + if (!xhr._interception?.hasCalledHeaderReceived) { + if (xhr._interception) { + xhr._interception.hasCalledHeaderReceived = true; + } + const contentType = this.getResponseHeader('content-type') || ''; + const contentLength = this.getResponseHeader('content-length'); + const responseSize = contentLength + ? parseInt(contentLength, 10) + : 0; + const responseHeaders = parseResponseHeaders( + this.getAllResponseHeaders() + ); + + // Set responseHeaders on xhr for compatibility with Logger.ts + xhr.responseHeaders = responseHeaders; + + try { + headerReceivedCallback( + contentType, + responseSize, + responseHeaders, + xhr + ); + } catch { + // Interceptor callbacks must not break network requests. + } } - const contentType = this.getResponseHeader('content-type') || ''; - const contentLength = this.getResponseHeader('content-length'); - const responseSize = contentLength ? parseInt(contentLength, 10) : 0; - const responseHeaders = parseResponseHeaders( - this.getAllResponseHeaders() - ); - - // Set responseHeaders on xhr for compatibility with Logger.ts - xhr.responseHeaders = responseHeaders; - - headerReceivedCallback( - contentType, - responseSize, - responseHeaders, - xhr - ); } - } - if (this.readyState === XMLHttpRequest.DONE) { - if (!xhr._interception?.hasCalledResponse) { - if (xhr._interception) { - xhr._interception.hasCalledResponse = true; - } - let responseData = ''; - if (this.responseType === '' || this.responseType === 'text') { - responseData = this.responseText || ''; - } else if (this.responseType === 'json' && this.response) { + if (this.readyState === XMLHttpRequest.DONE) { + if (!xhr._interception?.hasCalledResponse) { + if (xhr._interception) { + xhr._interception.hasCalledResponse = true; + } + let responseData: any = this.response; + if (this.responseType === '' || this.responseType === 'text') { + responseData = this.responseText || ''; + } else if (this.responseType === 'json' && this.response) { + try { + responseData = JSON.stringify(this.response); + } catch { + responseData = '[Unable to stringify response]'; + } + } + try { - responseData = JSON.stringify(this.response); + responseCallback( + this.status, + this.timeout, + responseData, + this.responseURL, + this.responseType, + xhr + ); } catch { - responseData = '[Unable to stringify response]'; + // Interceptor callbacks must not break network requests. } - } else if (this.response) { - responseData = '[Non-text response]'; } - - responseCallback( - this.status, - this.timeout, - responseData, - this.responseURL, - this.responseType, - xhr - ); } - } - }); + }); + } return originalXHRSend!.call(this, body); }; diff --git a/src/__tests__/NetworkRequestInfo.spec.ts b/src/__tests__/NetworkRequestInfo.spec.ts index cd9f179..f305b2b 100644 --- a/src/__tests__/NetworkRequestInfo.spec.ts +++ b/src/__tests__/NetworkRequestInfo.spec.ts @@ -175,36 +175,48 @@ describe('getRequestBody', () => { }); describe('getResponseBody', () => { - const info = new NetworkRequestInfo( - '1', - 'application/json', - 'GET', - 'https://test.com' - ); + it('should show pending response for requests not finished yet', async () => { + const info = new NetworkRequestInfo( + '1', + 'application/json', + 'GET', + 'https://test.com' + ); + info.status = -1; + info.endTime = 0; + info.response = ''; - it('should return stringified data in consistent format', () => { - info.dataSent = '{"data": {"a": 1 }}'; - const result = info.getRequestBody(); - expect(typeof result).toBe('string'); - expect(result).toMatchInlineSnapshot(` - "{ - "data": { - "a": 1 - } - }" - `); + await expect(info.getResponseBody()).resolves.toBe('Pending response...'); }); - it('should return object wrapped in data if parsing fails', () => { - // @ts-ignore - info.dataSent = { test: 1 }; - const result = info.getRequestBody(); - expect(typeof result).toBe('string'); - expect(result).toMatchInlineSnapshot(` + it('should return empty string for successful empty responses', async () => { + const info = new NetworkRequestInfo( + '1', + 'application/json', + 'GET', + 'https://test.com' + ); + info.status = 204; + info.endTime = Date.now(); + info.response = ''; + + await expect(info.getResponseBody()).resolves.toBe(''); + }); + + it('should return stringified JSON response', async () => { + const info = new NetworkRequestInfo( + '1', + 'application/json', + 'GET', + 'https://test.com' + ); + info.status = 200; + info.endTime = Date.now(); + info.response = '{"ok":true}'; + + await expect(info.getResponseBody()).resolves.toMatchInlineSnapshot(` "{ - "data": { - "test": 1 - } + "ok": true }" `); }); From 0e2deee90d7785b09d621c64d221dd117e3d69ab Mon Sep 17 00:00:00 2001 From: Alex Brazier Date: Fri, 6 Mar 2026 16:20:11 +0000 Subject: [PATCH 4/4] Fix tsc --- src/XHRInterceptor.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/XHRInterceptor.spec.ts b/src/XHRInterceptor.spec.ts index 1aab923..ae158af 100644 --- a/src/XHRInterceptor.spec.ts +++ b/src/XHRInterceptor.spec.ts @@ -1,3 +1,5 @@ +export {}; + type ReadyStateListener = () => void; class FakeXMLHttpRequest {