diff --git a/README.md b/README.md index 2e24d26..f650585 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/package.json b/package.json index da985ea..4c3db17 100644 --- a/package.json +++ b/package.json @@ -63,4 +63,4 @@ "testcafe": "^1.9.4", "typescript": "^4.1.3" } -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 33a431b..4aea08e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/fetch-interceptor/fetch-interceptor.ts b/src/request-interceptor/fetch-interceptor/fetch-interceptor.ts similarity index 95% rename from src/fetch-interceptor/fetch-interceptor.ts rename to src/request-interceptor/fetch-interceptor/fetch-interceptor.ts index 199b7fb..e166cb7 100644 --- a/src/fetch-interceptor/fetch-interceptor.ts +++ b/src/request-interceptor/fetch-interceptor/fetch-interceptor.ts @@ -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); } diff --git a/src/request-interceptor/fetch-interceptor/index.ts b/src/request-interceptor/fetch-interceptor/index.ts new file mode 100644 index 0000000..5480a6a --- /dev/null +++ b/src/request-interceptor/fetch-interceptor/index.ts @@ -0,0 +1,7 @@ +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import { InterceptUrls, RequestInterceptor } from '../request-interceptor'; + +export class FetchInterceptor extends RequestInterceptor { + protected readonly clientCode: string = readFileSync(resolve(__dirname, './fetch-interceptor.js')).toString(); +} diff --git a/src/request-interceptor/index.ts b/src/request-interceptor/index.ts new file mode 100644 index 0000000..a18b818 --- /dev/null +++ b/src/request-interceptor/index.ts @@ -0,0 +1,2 @@ +export * from './fetch-interceptor'; +export * from './xhr-interceptor'; diff --git a/src/fetch-interceptor/index.ts b/src/request-interceptor/request-interceptor.ts similarity index 76% rename from src/fetch-interceptor/index.ts rename to src/request-interceptor/request-interceptor.ts index ee772d4..f068339 100644 --- a/src/fetch-interceptor/index.ts +++ b/src/request-interceptor/request-interceptor.ts @@ -1,7 +1,4 @@ -import { readFileSync } from 'fs'; -import { resolve } from 'path'; - -type InterceptUrls = Record; +export type InterceptUrls = Record; declare global { const urls: InterceptUrls; @@ -14,26 +11,24 @@ declare global { } } -export class FetchInterceptor { - private readonly REG_EXP_PREFIX: string = '__RegExp'; +export abstract class RequestInterceptor { + 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) => { + >(urlKey: K): (t: { t: TestController }) => Promise { 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) => @@ -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('') ); `, diff --git a/src/request-interceptor/xhr-interceptor/index.ts b/src/request-interceptor/xhr-interceptor/index.ts new file mode 100644 index 0000000..def0dfc --- /dev/null +++ b/src/request-interceptor/xhr-interceptor/index.ts @@ -0,0 +1,7 @@ +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import { InterceptUrls, RequestInterceptor } from '../request-interceptor'; + +export class XHRInterceptor extends RequestInterceptor { + protected readonly clientCode: string = readFileSync(resolve(__dirname, './xhr-interceptor.js')).toString(); +} diff --git a/src/request-interceptor/xhr-interceptor/xhr-interceptor.ts b/src/request-interceptor/xhr-interceptor/xhr-interceptor.ts new file mode 100644 index 0000000..f25a3aa --- /dev/null +++ b/src/request-interceptor/xhr-interceptor/xhr-interceptor.ts @@ -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; + } + }, +});