Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions src/NetworkRequestInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>((resolve, reject) => {
const handleError = () => reject(blobReader.error);

Expand All @@ -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]';
}
}
}
178 changes: 178 additions & 0 deletions src/XHRInterceptor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
export {};

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<string, ReadyStateListener[]> = {};
private responseHeaders: Record<string, string> = {};

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<string, string>) {
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();
});
});
Loading
Loading