Skip to content

Commit

Permalink
feat: Add XHRInterceptor
Browse files Browse the repository at this point in the history
  • Loading branch information
Dominique Wirz committed Dec 18, 2020
1 parent b02f242 commit 87d7c4a
Show file tree
Hide file tree
Showing 9 changed files with 109 additions and 18 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

This package is a toolbox which contains various helpers we often use when working with TestCafe.

## `FetchInterceptor`
## `FetchInterceptor` or `XHRInterceptor`

The `FetchInterceptor` class can be used to intercept the `window.fetch`-calls within the browser. The class takes a key/value object, where the values are `string` or `RegExp` for an URL to intercept. Afterwards it is possible to resolve a running `fetch` call at any desired time with `await fetchInterceptor.resolve('interceptURLKey')({ t })`. (where `t` is the TestCafe `TestController`)
The `FetchInterceptor` class can be used to intercept the `window.fetch`-calls within the browser and the `XHRInterceptor` does the same for `XMLHttpRequest`. Both classes take a key/value object as constructor argument, where the values are `string` or `RegExp` for an URL to intercept. Afterwards it is possible to resolve a running `fetch` call at any desired time with `await fetchInterceptor.resolve('interceptURLKey')({ t })`. (where `t` is the TestCafe `TestController`)

### Example

The example only shows the `FetchInterceptor` because the `XHRInterceptor` is used the same way.

```Typescript
// The following snippet does not include all needed imports and code it is intended
// to give you a starting point and an idea how this class can be used.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,4 @@
"testcafe": "^1.9.4",
"typescript": "^4.1.3"
}
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './fetch-interceptor';
export * from './request-interceptor';
export * from './mock-headers';
export * from './mock-response';
export * from './request-call-count-mock';
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ attach().register({
const interceptedFetch = interceptedUrl ? window.__intercepted[interceptedUrl.toString()] : null;

if (interceptedFetch) {
console.log(`[fetch-interceptor] Intercepted '${response.url}' by '${interceptedUrl}'`);
console.log(`[${window.__interceptorName}] Intercepted '${response.url}' by '${interceptedUrl}'`);
return interceptedFetch.promise.then(() => response);
}

Expand Down
7 changes: 7 additions & 0 deletions src/request-interceptor/fetch-interceptor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { InterceptUrls, RequestInterceptor } from '../request-interceptor';

export class FetchInterceptor<T extends InterceptUrls> extends RequestInterceptor<T> {
protected readonly clientCode: string = readFileSync(resolve(__dirname, './fetch-interceptor.js')).toString();
}
2 changes: 2 additions & 0 deletions src/request-interceptor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './fetch-interceptor';
export * from './xhr-interceptor';
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import { readFileSync } from 'fs';
import { resolve } from 'path';

type InterceptUrls = Record<string, string | RegExp>;
export type InterceptUrls = Record<string, string | RegExp>;

declare global {
const urls: InterceptUrls;
Expand All @@ -14,26 +11,24 @@ declare global {
}
}

export class FetchInterceptor<T extends InterceptUrls> {
private readonly REG_EXP_PREFIX: string = '__RegExp';
export abstract class RequestInterceptor<T extends InterceptUrls> {
protected readonly REG_EXP_PREFIX: string = '__RegExp';

private readonly clientCode: string = readFileSync(resolve(__dirname, './fetch-interceptor.js')).toString();
protected abstract readonly clientCode: string;

constructor(public readonly interceptUrls: T) {}

public resolve = <
public resolve<
// This type definition only allows keys from the given InterceptUrls
K extends keyof any &
{
[K in keyof T]: T[K] extends string | RegExp ? K : never;
}[keyof T]
>(
urlKey: K,
): ((t: { t: TestController }) => Promise<void>) => {
>(urlKey: K): (t: { t: TestController }) => Promise<void> {
const options = { dependencies: { urlKey, urls: this.interceptUrls } };
return ({ t }: { t: TestController }) =>
t.eval(() => window.__intercepted[urls[urlKey.toString()].toString()].resolve(), options);
};
}

public clientScript(): ClientScriptContent {
const regExpReplacer = (_: any, value: RegExp | unknown) =>
Expand All @@ -53,11 +48,12 @@ const regExpReviver = (_, value) => {
return value;
};
window.__interceptorName = '${this.constructor.name}';
window.__interceptUrls = JSON.parse('${urls}', regExpReviver);
${this.clientCode}
console.log(
'[fetch-interceptor] Intercepted urls and patterns:',
'[${this.constructor.name}] Intercepted urls and patterns:',
window.__interceptUrls.map((url, i) => \`\\n\\t\${i + 1}) \${url}\`).join('')
);
`,
Expand Down
7 changes: 7 additions & 0 deletions src/request-interceptor/xhr-interceptor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { InterceptUrls, RequestInterceptor } from '../request-interceptor';

export class XHRInterceptor<T extends InterceptUrls> extends RequestInterceptor<T> {
protected readonly clientCode: string = readFileSync(resolve(__dirname, './xhr-interceptor.js')).toString();
}
77 changes: 77 additions & 0 deletions src/request-interceptor/xhr-interceptor/xhr-interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck

let interceptors = [];

function attach() {
window.XMLHttpRequest.prototype.open = (function (xhrOpen) {
return function (method, url, async, username, password) {
this.__requestMethod = method;
this.__requestUrl = url;
return xhrOpen.call(this, method, url, async, username, password);
};
})(window.XMLHttpRequest.prototype.open);

window.XMLHttpRequest.prototype.send = (function (xhrSend) {
return function (body) {
const reversedInterceptors = interceptors.reduce((array, interceptor) => [interceptor].concat(array), []);
const requestInterceptors = reversedInterceptors.map(({ request }) => request).filter(Boolean);
if (requestInterceptors.length) {
return Promise.all(requestInterceptors.map((interceptor) => interceptor(this.__requestUrl))).then(() =>
xhrSend.call(this, body),
);
}

xhrSend.call(this, body);
};
})(window.XMLHttpRequest.prototype.send);

return {
register: function (interceptor) {
interceptors.push(interceptor);
return () => {
const index = interceptors.indexOf(interceptor);
if (index >= 0) {
interceptors.splice(index, 1);
}
};
},
clear: function () {
interceptors = [];
},
};
}

window.__intercepted = window.__interceptUrls
? window.__interceptUrls.reduce(
(urls, url) =>
Object.assign(urls, {
[url]: (() => {
let resolvePromise;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});

return {
promise,
resolve: resolvePromise,
};
})(),
}),
{},
)
: {};

attach().register({
request: function (sendUrl) {
const interceptedUrl = window.__interceptUrls.find((url) =>
url instanceof RegExp ? url.test(sendUrl) : url === sendUrl,
);
const interceptedOpen = interceptedUrl ? window.__intercepted[interceptedUrl.toString()] : null;

if (interceptedOpen) {
console.log(`[${window.__interceptorName}] Intercepted '${sendUrl}' by '${interceptedUrl}'`);
return interceptedOpen.promise;
}
},
});

0 comments on commit 87d7c4a

Please sign in to comment.