├── .npmrc ├── test ├── integration │ └── fakes │ │ ├── remote.html │ │ ├── redirected.html │ │ ├── style.css │ │ ├── index.html │ │ ├── script.js │ │ ├── ajaxWithPost.html │ │ ├── ajaxWithUnavailableRequest.html │ │ └── ajaxWithCustomHeaders.html ├── common │ ├── TestDouble.ts │ ├── ServerSettings.ts │ ├── CustomRequestBlocker.ts │ ├── ServerDouble.ts │ ├── CustomFaker.ts │ ├── AssertionHelpers.ts │ ├── Browser.ts │ ├── filterForbiddenHeaders.ts │ ├── testServer.ts │ └── testDoubleFactories.ts ├── isolation │ ├── UrlAccessorResolver.dev.spec.ts │ ├── ResponseFaker.dev.spec.ts │ ├── RequestModifier.dev.spec.ts │ ├── RequestSpy.dev.spec.ts │ ├── RequestRedirector.dev.spec.ts │ ├── ResponseModifier.dev.spec.ts │ └── HttpRequestFactory.dev.spec.ts ├── unit │ ├── unitTestHelpers.ts │ └── puppeteer-request-spy.dev.spec.ts └── regression │ └── unit.spec.ts ├── custom-typings └── clear-module │ └── index.d.ts ├── src ├── common │ ├── Logger.ts │ ├── urlAccessor │ │ ├── UrlAccessor.ts │ │ ├── UrlFunctionAccessor.ts │ │ ├── UrlStringAccessor.ts │ │ └── UrlAccessorResolver.ts │ ├── VoidLogger.ts │ ├── resolveOptionalPromise.ts │ ├── interfaceValidators │ │ ├── instanceOfRequestSpy.ts │ │ ├── instanceOfResponseFaker.ts │ │ ├── instanceOfRequestModifier.ts │ │ └── instanceOfRequestBlocker.ts │ └── HttpRequestFactory.ts ├── types │ ├── RequestMatcher.ts │ └── ResponseModifierCallBack.ts ├── interface │ ├── IRequestFactory.ts │ ├── IRequestSpy.ts │ ├── IRequestBlocker.ts │ ├── IRequestModifier.ts │ └── IResponseFaker.ts ├── RequestBlocker.ts ├── index.ts ├── ResponseFaker.ts ├── RequestModifier.ts ├── RequestRedirector.ts ├── RequestSpy.ts ├── ResponseModifier.ts └── RequestInterceptor.ts ├── types ├── common │ ├── Logger.d.ts │ ├── VoidLogger.d.ts │ ├── resolveOptionalPromise.d.ts │ ├── interfaceValidators │ │ ├── instanceOfRequestSpy.d.ts │ │ ├── instanceOfResponseFaker.d.ts │ │ ├── instanceOfRequestBlocker.d.ts │ │ └── instanceOfRequestModifier.d.ts │ ├── urlAccessor │ │ ├── UrlAccessor.d.ts │ │ ├── UrlStringAccessor.d.ts │ │ ├── UrlAccessorResolver.d.ts │ │ └── UrlFunctionAccessor.d.ts │ └── HttpRequestFactory.d.ts ├── types │ ├── RequestMatcher.d.ts │ └── ResponseModifierCallBack.d.ts ├── interface │ ├── IRequestFactory.d.ts │ ├── IRequestSpy.d.ts │ ├── IRequestBlocker.d.ts │ ├── IRequestModifier.d.ts │ └── IResponseFaker.d.ts ├── RequestBlocker.d.ts ├── RequestModifier.d.ts ├── RequestRedirector.d.ts ├── ResponseFaker.d.ts ├── RequestSpy.d.ts ├── ResponseModifier.d.ts ├── index.d.ts └── RequestInterceptor.d.ts ├── documentation ├── activity.png ├── activity.iuml └── API.md ├── task ├── runTestServer.js ├── clean.js └── copyTypes.js ├── .idea ├── watcherTasks.xml ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── encodings.xml ├── vcs.xml ├── jsLibraryMappings.xml ├── misc.xml ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── typescript-compiler.xml ├── modules.xml ├── puppeteer-request-spy.iml └── codeStyleSettings.xml ├── .travis.yml ├── puppeteer-request-spy.iml ├── examples ├── CustomSpy.ts ├── CustomFaker.ts ├── block-test.spec.js ├── simple-test.spec.js ├── keyword-matcher.spec.js └── fake-test.spec.js ├── tsconfig-lint.json ├── CONTRIBUTING.md ├── tsconfig.json ├── LICENSE ├── .npmignore ├── .gitignore ├── package.json ├── README.md └── tslint.json /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /test/integration/fakes/remote.html: -------------------------------------------------------------------------------- 1 |

some stuff

2 | -------------------------------------------------------------------------------- /custom-typings/clear-module/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'clear-module'; 2 | -------------------------------------------------------------------------------- /test/integration/fakes/redirected.html: -------------------------------------------------------------------------------- 1 |

some redirected stuff

2 | -------------------------------------------------------------------------------- /src/common/Logger.ts: -------------------------------------------------------------------------------- 1 | export interface ILogger { 2 | log(message: string): void; 3 | } 4 | -------------------------------------------------------------------------------- /test/integration/fakes/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | -------------------------------------------------------------------------------- /types/common/Logger.d.ts: -------------------------------------------------------------------------------- 1 | export interface ILogger { 2 | log(message: string): void; 3 | } 4 | -------------------------------------------------------------------------------- /types/common/VoidLogger.d.ts: -------------------------------------------------------------------------------- 1 | export declare class VoidLogger { 2 | log(logText: string): void; 3 | } 4 | -------------------------------------------------------------------------------- /src/types/RequestMatcher.ts: -------------------------------------------------------------------------------- 1 | export type RequestMatcher = (testString: string, pattern: string) => boolean; 2 | -------------------------------------------------------------------------------- /documentation/activity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tabueeee/puppeteer-request-spy/HEAD/documentation/activity.png -------------------------------------------------------------------------------- /task/runTestServer.js: -------------------------------------------------------------------------------- 1 | const app = require('../build/test/common/testServer').testServer; 2 | 3 | app.listen(1337); 4 | -------------------------------------------------------------------------------- /types/types/RequestMatcher.d.ts: -------------------------------------------------------------------------------- 1 | export declare type RequestMatcher = (testString: string, pattern: string) => boolean; 2 | -------------------------------------------------------------------------------- /test/common/TestDouble.ts: -------------------------------------------------------------------------------- 1 | // noinspection TsLint 2 | export type TestDouble = { 3 | [P in keyof T]?: any; 4 | }; 5 | -------------------------------------------------------------------------------- /types/common/resolveOptionalPromise.d.ts: -------------------------------------------------------------------------------- 1 | export declare function resolveOptionalPromise(subject: T | Promise): Promise; 2 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - stable 5 | 6 | install: 7 | - npm install 8 | 9 | script: 10 | - npm run test-coverage-travis 11 | -------------------------------------------------------------------------------- /types/common/interfaceValidators/instanceOfRequestSpy.d.ts: -------------------------------------------------------------------------------- 1 | import { IRequestSpy } from '../../'; 2 | export declare function instanceOfRequestSpy(object: object): object is IRequestSpy; 3 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/common/urlAccessor/UrlAccessor.ts: -------------------------------------------------------------------------------- 1 | import {Request} from 'puppeteer'; 2 | 3 | export abstract class UrlAccessor { 4 | public abstract getUrlFromRequest(request: Request): string; 5 | } 6 | -------------------------------------------------------------------------------- /types/common/urlAccessor/UrlAccessor.d.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'puppeteer'; 2 | export declare abstract class UrlAccessor { 3 | abstract getUrlFromRequest(request: Request): string; 4 | } 5 | -------------------------------------------------------------------------------- /types/common/interfaceValidators/instanceOfResponseFaker.d.ts: -------------------------------------------------------------------------------- 1 | import { IResponseFaker } from '../../'; 2 | export declare function instanceOfResponseFaker(object: object): object is IResponseFaker; 3 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/types/ResponseModifierCallBack.ts: -------------------------------------------------------------------------------- 1 | import {Request} from 'puppeteer'; 2 | 3 | export type ResponseModifierCallBack = (err: Error | undefined, response: string, request: Request) => string | Promise; 4 | -------------------------------------------------------------------------------- /types/types/ResponseModifierCallBack.d.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'puppeteer'; 2 | export declare type ResponseModifierCallBack = (err: Error | undefined, response: string, request: Request) => string | Promise; 3 | -------------------------------------------------------------------------------- /types/common/interfaceValidators/instanceOfRequestBlocker.d.ts: -------------------------------------------------------------------------------- 1 | import { IRequestBlocker } from '../../interface/IRequestBlocker'; 2 | export declare function instanceOfRequestBlocker(object: object): object is IRequestBlocker; 3 | -------------------------------------------------------------------------------- /types/common/interfaceValidators/instanceOfRequestModifier.d.ts: -------------------------------------------------------------------------------- 1 | import { IRequestModifier } from '../../interface/IRequestModifier'; 2 | export declare function instanceOfRequestModifier(object: object): object is IRequestModifier; 3 | -------------------------------------------------------------------------------- /src/common/VoidLogger.ts: -------------------------------------------------------------------------------- 1 | export class VoidLogger { 2 | // @ts-ignore: logText 3 | // noinspection JSUnusedLocalSymbols, JSMethodCanBeStatic 4 | public log(logText: string): void { 5 | return undefined; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /types/interface/IRequestFactory.d.ts: -------------------------------------------------------------------------------- 1 | import { Request, RespondOptions } from 'puppeteer'; 2 | export interface IRequestFactory { 3 | createRequest(request: Request): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /types/common/urlAccessor/UrlStringAccessor.d.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'puppeteer'; 2 | import { UrlAccessor } from './UrlAccessor'; 3 | export declare class UrlStringAccessor extends UrlAccessor { 4 | getUrlFromRequest(request: Request): string; 5 | } 6 | -------------------------------------------------------------------------------- /types/common/urlAccessor/UrlAccessorResolver.d.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'puppeteer'; 2 | import { UrlAccessor } from './UrlAccessor'; 3 | export declare module UrlAccessorResolver { 4 | function getUrlAccessor(interceptedRequest: Request): UrlAccessor; 5 | } 6 | -------------------------------------------------------------------------------- /types/common/urlAccessor/UrlFunctionAccessor.d.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'puppeteer'; 2 | import { UrlAccessor } from './UrlAccessor'; 3 | export declare class UrlFunctionAccessor extends UrlAccessor { 4 | getUrlFromRequest(request: Request): string; 5 | } 6 | -------------------------------------------------------------------------------- /src/common/resolveOptionalPromise.ts: -------------------------------------------------------------------------------- 1 | export async function resolveOptionalPromise(subject: T | Promise): Promise { 2 | if (typeof subject === 'object' && subject instanceof Promise) { 3 | return await subject; 4 | } 5 | 6 | return subject; 7 | } 8 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/common/urlAccessor/UrlFunctionAccessor.ts: -------------------------------------------------------------------------------- 1 | import {Request} from 'puppeteer'; 2 | import {UrlAccessor} from './UrlAccessor'; 3 | 4 | export class UrlFunctionAccessor extends UrlAccessor { 5 | public getUrlFromRequest(request: Request): string { 6 | return request.url(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /test/common/ServerSettings.ts: -------------------------------------------------------------------------------- 1 | export interface ServerSettings { 2 | rootPath: string; 3 | port: number; 4 | host: string; 5 | } 6 | 7 | export const serverSettings: ServerSettings = { 8 | rootPath: 'test/integration/fakes', 9 | port: 1337, 10 | host: 'localhost' 11 | }; 12 | -------------------------------------------------------------------------------- /test/integration/fakes/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 |

hello world

14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/typescript-compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /src/common/interfaceValidators/instanceOfRequestSpy.ts: -------------------------------------------------------------------------------- 1 | import {IRequestSpy} from '../../'; 2 | 3 | export function instanceOfRequestSpy(object: object): object is IRequestSpy { 4 | return typeof (object).addMatch === 'function' 5 | && typeof (object).isMatchingRequest === 'function'; 6 | } 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /types/interface/IRequestSpy.d.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'puppeteer'; 2 | import { RequestMatcher } from '../types/RequestMatcher'; 3 | export interface IRequestSpy { 4 | isMatchingRequest(request: Request, matcher: RequestMatcher): Promise | boolean; 5 | addMatch(matchedRequest: Request): Promise | void; 6 | } 7 | -------------------------------------------------------------------------------- /src/common/interfaceValidators/instanceOfResponseFaker.ts: -------------------------------------------------------------------------------- 1 | import {IResponseFaker} from '../../'; 2 | 3 | export function instanceOfResponseFaker(object: object): object is IResponseFaker { 4 | return typeof (object).getResponseFake === 'function' 5 | && typeof (object).isMatchingRequest === 'function'; 6 | } 7 | -------------------------------------------------------------------------------- /src/common/urlAccessor/UrlStringAccessor.ts: -------------------------------------------------------------------------------- 1 | import {Request} from 'puppeteer'; 2 | import {UrlAccessor} from './UrlAccessor'; 3 | 4 | export class UrlStringAccessor extends UrlAccessor { 5 | public getUrlFromRequest(request: Request): string { 6 | // @ts-ignore: support old puppeteer version 7 | return request.url; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /types/interface/IRequestBlocker.d.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'puppeteer'; 2 | import { RequestMatcher } from '../types/RequestMatcher'; 3 | export interface IRequestBlocker { 4 | shouldBlockRequest(request: Request, matcher: RequestMatcher): Promise | boolean; 5 | clearUrlsToBlock(): void; 6 | addUrlsToBlock(urlsToBlock: Array | string): void; 7 | } 8 | -------------------------------------------------------------------------------- /types/interface/IRequestModifier.d.ts: -------------------------------------------------------------------------------- 1 | import { Overrides, Request } from 'puppeteer'; 2 | import { RequestMatcher } from '../types/RequestMatcher'; 3 | export interface IRequestModifier { 4 | isMatchingRequest(interceptedRequest: Request, matcher: RequestMatcher): Promise | boolean; 5 | getOverride(interceptedRequest: Request): Promise | Overrides; 6 | } 7 | -------------------------------------------------------------------------------- /types/interface/IResponseFaker.d.ts: -------------------------------------------------------------------------------- 1 | import { Request, RespondOptions } from 'puppeteer'; 2 | import { RequestMatcher } from '../types/RequestMatcher'; 3 | export interface IResponseFaker { 4 | isMatchingRequest(interceptedRequest: Request, matcher: RequestMatcher): Promise | boolean; 5 | getResponseFake(request: Request): RespondOptions | Promise; 6 | } 7 | -------------------------------------------------------------------------------- /src/common/interfaceValidators/instanceOfRequestModifier.ts: -------------------------------------------------------------------------------- 1 | import {IRequestModifier} from '../../interface/IRequestModifier'; 2 | 3 | export function instanceOfRequestModifier(object: object): object is IRequestModifier { 4 | return typeof (object).getOverride === 'function' 5 | && typeof (object).isMatchingRequest === 'function'; 6 | } 7 | -------------------------------------------------------------------------------- /types/common/HttpRequestFactory.d.ts: -------------------------------------------------------------------------------- 1 | import { Request, RespondOptions } from 'puppeteer'; 2 | import { IRequestFactory } from '../interface/IRequestFactory'; 3 | export declare class HttpRequestFactory implements IRequestFactory { 4 | private timeout; 5 | constructor(timeout?: number); 6 | createRequest(request: Request): Promise; 9 | private convertHeaders; 10 | } 11 | -------------------------------------------------------------------------------- /task/clean.js: -------------------------------------------------------------------------------- 1 | const promisify = require('util').promisify; 2 | const glob = promisify(require('glob')); 3 | const del = require('del'); 4 | 5 | (async () => { 6 | try { 7 | let files = await glob('build/**/*'); 8 | for (let file of files) { 9 | await del(file); 10 | } 11 | } catch (error) { 12 | console.error(error); 13 | process.exit(2); 14 | } 15 | })(); 16 | 17 | -------------------------------------------------------------------------------- /test/common/CustomRequestBlocker.ts: -------------------------------------------------------------------------------- 1 | import {IRequestBlocker} from '../../src/interface/IRequestBlocker'; 2 | 3 | export class CustomRequestBlocker implements IRequestBlocker { 4 | 5 | public shouldBlockRequest(): boolean { 6 | return true; 7 | } 8 | 9 | public clearUrlsToBlock(): void { 10 | return undefined; 11 | } 12 | 13 | public addUrlsToBlock(): void { 14 | return undefined; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/common/interfaceValidators/instanceOfRequestBlocker.ts: -------------------------------------------------------------------------------- 1 | import {IRequestBlocker} from '../../interface/IRequestBlocker'; 2 | 3 | export function instanceOfRequestBlocker(object: object): object is IRequestBlocker { 4 | return typeof (object).shouldBlockRequest === 'function' 5 | && typeof (object).clearUrlsToBlock === 'function' 6 | && typeof (object).addUrlsToBlock === 'function'; 7 | } 8 | -------------------------------------------------------------------------------- /types/RequestBlocker.d.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'puppeteer'; 2 | import { IRequestBlocker } from './interface/IRequestBlocker'; 3 | import { RequestMatcher } from './types/RequestMatcher'; 4 | export declare class RequestBlocker implements IRequestBlocker { 5 | private urlsToBlock; 6 | shouldBlockRequest(request: Request, matcher: RequestMatcher): boolean; 7 | addUrlsToBlock(urls: Array | string): void; 8 | clearUrlsToBlock(): void; 9 | } 10 | -------------------------------------------------------------------------------- /src/interface/IRequestFactory.ts: -------------------------------------------------------------------------------- 1 | import {Request, RespondOptions} from 'puppeteer'; 2 | 3 | export interface IRequestFactory { 4 | /** 5 | * @param request : puppeteers Request object 6 | * @return Overrides: RespondOptions as defined by puppeteer but with mandatory body string 7 | * 8 | * loads a http request in the node environment 9 | */ 10 | createRequest(request: Request): Promise; 11 | } 12 | -------------------------------------------------------------------------------- /test/integration/fakes/script.js: -------------------------------------------------------------------------------- 1 | console.log('hello world'); 2 | 3 | const xhrElement = document.getElementById('xhr'); 4 | 5 | var data = null; 6 | 7 | var xhr = new XMLHttpRequest(); 8 | xhr.withCredentials = true; 9 | 10 | xhr.addEventListener("readystatechange", function () { 11 | if (this.readyState === 4) { 12 | xhrElement.innerHTML = this.responseText; 13 | } 14 | }); 15 | 16 | xhr.open("GET", "/fakes/remote.html"); 17 | xhr.setRequestHeader("cache-control", "no-cache"); 18 | 19 | xhr.send(data); 20 | -------------------------------------------------------------------------------- /task/copyTypes.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const promisify = require('util').promisify; 3 | const glob = promisify(require('glob')); 4 | 5 | (async () => { 6 | try { 7 | let files = await glob('build/src/**/*.d.ts'); 8 | console.log(files); 9 | for (let file of files) { 10 | await fs.copy(file, file.replace('build/src', 'types'), {overwrite: true}); 11 | } 12 | } catch (error) { 13 | console.error(error); 14 | process.exit(2); 15 | } 16 | })(); 17 | 18 | -------------------------------------------------------------------------------- /test/common/ServerDouble.ts: -------------------------------------------------------------------------------- 1 | import {serverSettings} from './ServerSettings'; 2 | import {testServer} from './testServer'; 3 | 4 | export class ServerDouble { 5 | // tslint:disable-next-line:no-any 6 | private server: any; 7 | 8 | public async start(): Promise<{}> { 9 | return new Promise((resolve: () => void): void => { 10 | this.server = testServer.listen(serverSettings.port); 11 | resolve(); 12 | }); 13 | } 14 | 15 | public stop(): void { 16 | this.server.close(); 17 | } 18 | } 19 | 20 | export const serverDouble: ServerDouble = new ServerDouble(); 21 | -------------------------------------------------------------------------------- /types/RequestModifier.d.ts: -------------------------------------------------------------------------------- 1 | import { Overrides, Request } from 'puppeteer'; 2 | import { IRequestModifier } from './interface/IRequestModifier'; 3 | import { RequestMatcher } from './types/RequestMatcher'; 4 | export declare class RequestModifier implements IRequestModifier { 5 | private patterns; 6 | private requestOverrideFactory; 7 | constructor(patterns: Array | string, requestOverride: ((request: Request) => Promise | Overrides) | Overrides); 8 | getOverride(request: Request): Promise; 9 | isMatchingRequest(request: Request, matcher: RequestMatcher): boolean; 10 | getPatterns(): Array; 11 | } 12 | -------------------------------------------------------------------------------- /types/RequestRedirector.d.ts: -------------------------------------------------------------------------------- 1 | import { Overrides, Request } from 'puppeteer'; 2 | import { IRequestModifier } from './interface/IRequestModifier'; 3 | import { RequestMatcher } from './types/RequestMatcher'; 4 | export declare class RequestRedirector implements IRequestModifier { 5 | private patterns; 6 | private redirectionUrlFactory; 7 | constructor(patterns: Array | string, redirectionUrl: ((request: Request) => Promise | string) | string); 8 | isMatchingRequest(request: Request, matcher: RequestMatcher): boolean; 9 | getPatterns(): Array; 10 | getOverride(interceptedRequest: Request): Promise; 11 | } 12 | -------------------------------------------------------------------------------- /types/ResponseFaker.d.ts: -------------------------------------------------------------------------------- 1 | import { Request, RespondOptions } from 'puppeteer'; 2 | import { IResponseFaker } from './interface/IResponseFaker'; 3 | import { RequestMatcher } from './types/RequestMatcher'; 4 | export declare class ResponseFaker implements IResponseFaker { 5 | private patterns; 6 | private responseFakeFactory; 7 | constructor(patterns: Array | string, responseFake: ((request: Request) => RespondOptions | Promise) | RespondOptions); 8 | getResponseFake(request: Request): Promise; 9 | isMatchingRequest(request: Request, matcher: RequestMatcher): boolean; 10 | getPatterns(): Array; 11 | } 12 | -------------------------------------------------------------------------------- /test/integration/fakes/ajaxWithPost.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/integration/fakes/ajaxWithUnavailableRequest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /types/RequestSpy.d.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'puppeteer'; 2 | import { IRequestSpy } from './interface/IRequestSpy'; 3 | import { RequestMatcher } from './types/RequestMatcher'; 4 | export declare class RequestSpy implements IRequestSpy { 5 | private hasMatchingUrl; 6 | private matchCount; 7 | private patterns; 8 | private matchedRequests; 9 | constructor(patterns: Array | string); 10 | getPatterns(): Array; 11 | getMatchedRequests(): Array; 12 | hasMatch(): boolean; 13 | addMatch(matchedRequest: Request): void; 14 | isMatchingRequest(request: Request, matcher: RequestMatcher): boolean; 15 | getMatchedUrls(): Array; 16 | getMatchCount(): number; 17 | } 18 | -------------------------------------------------------------------------------- /src/interface/IRequestSpy.ts: -------------------------------------------------------------------------------- 1 | import {Request} from 'puppeteer'; 2 | import {RequestMatcher} from '../types/RequestMatcher'; 3 | 4 | export interface IRequestSpy { 5 | /** 6 | * @param request : puppeteers Request object to get the url from. 7 | * @param matcher <(url: string, pattern: string): Promise | boolean)>: matches the url with the pattern to block. 8 | * 9 | * checks if the RequestSpy matches the Request. 10 | */ 11 | isMatchingRequest(request: Request, matcher: RequestMatcher): Promise | boolean; 12 | /** 13 | * React to requests. 14 | * @param matchedRequest - The request to react to 15 | */ 16 | addMatch(matchedRequest: Request): Promise | void; 17 | } 18 | -------------------------------------------------------------------------------- /src/common/urlAccessor/UrlAccessorResolver.ts: -------------------------------------------------------------------------------- 1 | import {Request} from 'puppeteer'; 2 | import {UrlAccessor} from './UrlAccessor'; 3 | import {UrlFunctionAccessor} from './UrlFunctionAccessor'; 4 | import {UrlStringAccessor} from './UrlStringAccessor'; 5 | 6 | export module UrlAccessorResolver { 7 | let accessor: UrlAccessor; 8 | 9 | export function getUrlAccessor(interceptedRequest: Request): UrlAccessor { 10 | if (accessor instanceof UrlAccessor === false) { 11 | if (typeof interceptedRequest.url === 'string') { 12 | accessor = new UrlStringAccessor(); 13 | } else { 14 | accessor = new UrlFunctionAccessor(); 15 | } 16 | } 17 | 18 | return accessor; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/common/CustomFaker.ts: -------------------------------------------------------------------------------- 1 | import {Request, RespondOptions} from 'puppeteer'; 2 | import {IResponseFaker, RequestMatcher} from '../../src'; 3 | 4 | export class CustomFaker implements IResponseFaker { 5 | 6 | public getResponseFake(request: Request): RespondOptions { 7 | if (request.method() === 'GET') { 8 | return { 9 | status: 200, 10 | contentType: 'text/plain', 11 | body: 'Just a mock!' 12 | }; 13 | } 14 | 15 | return { 16 | status: 404, 17 | contentType: 'text/plain', 18 | body: 'Not Found!' 19 | }; 20 | } 21 | 22 | public isMatchingRequest(request: Request, matcher: RequestMatcher): boolean { 23 | return matcher(request.url(), '**/remote.html'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/interface/IRequestBlocker.ts: -------------------------------------------------------------------------------- 1 | import {Request} from 'puppeteer'; 2 | import {RequestMatcher} from '../types/RequestMatcher'; 3 | 4 | export interface IRequestBlocker { 5 | /** 6 | * @param request : request to get the url from. 7 | * @param matcher <(url: string, pattern: string): Promise | boolean)>: matches the url with the pattern to block. 8 | * 9 | * determines if a request should be blocked. 10 | */ 11 | shouldBlockRequest(request: Request, matcher: RequestMatcher): Promise | boolean; 12 | 13 | /** 14 | * removes all patterns added to the RequestBlocker. 15 | */ 16 | clearUrlsToBlock(): void; 17 | 18 | /** 19 | * adds new patterns to the RequestBlocker. 20 | */ 21 | addUrlsToBlock(urlsToBlock: Array | string): void; 22 | } 23 | -------------------------------------------------------------------------------- /types/ResponseModifier.d.ts: -------------------------------------------------------------------------------- 1 | import { Request, RespondOptions } from 'puppeteer'; 2 | import { IRequestFactory } from './interface/IRequestFactory'; 3 | import { IResponseFaker } from './interface/IResponseFaker'; 4 | import { RequestMatcher } from './types/RequestMatcher'; 5 | import { ResponseModifierCallBack } from './types/ResponseModifierCallBack'; 6 | export declare class ResponseModifier implements IResponseFaker { 7 | private patterns; 8 | private responseModifierCallBack; 9 | private httpRequestFactory; 10 | constructor(patterns: Array | string, responseModifierCallBack: ResponseModifierCallBack, httpRequestFactory?: IRequestFactory); 11 | isMatchingRequest(request: Request, matcher: RequestMatcher): boolean; 12 | getResponseFake(request: Request): Promise; 13 | getPatterns(): Array; 14 | } 15 | -------------------------------------------------------------------------------- /test/common/AssertionHelpers.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | export async function assertThrowsAsync(fn: () => void, regExp: RegExp): Promise { 4 | // noinspection TsLint 5 | let f: () => void = (): void => { 6 | }; 7 | try { 8 | await fn(); 9 | } catch (e) { 10 | f = (): void => { 11 | throw e; 12 | }; 13 | } finally { 14 | assert.throws(f, regExp); 15 | } 16 | } 17 | 18 | export async function assertDoesNotThrowAsync(fn: () => void, regExp: RegExp): Promise { 19 | // noinspection TsLint 20 | let f: () => void = (): void => { 21 | }; 22 | try { 23 | await fn(); 24 | } catch (e) { 25 | f = (): void => { 26 | throw e; 27 | }; 28 | } finally { 29 | assert.doesNotThrow(f, regExp); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /puppeteer-request-spy.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/CustomSpy.ts: -------------------------------------------------------------------------------- 1 | // import {IRequestSpy, RequestMatcher} from 'puppeteer-request-spy'; 2 | import {IRequestSpy, RequestMatcher} from '../..'; 3 | import {Request, Response} from 'puppeteer'; 4 | import * as assert from "assert"; 5 | 6 | 7 | export class CustomSpy implements IRequestSpy { 8 | private matches: Array = []; 9 | 10 | public isMatchingRequest(request: Request, matcher: RequestMatcher): boolean { 11 | 12 | return matcher(request.url(), '**/*'); 13 | } 14 | 15 | public addMatch(matchedRequest: Request): void { 16 | 17 | this.matches.push(matchedRequest); 18 | } 19 | 20 | public assertRequestsOk() { 21 | for (let match of this.matches) { 22 | let response: Response | null = match.response(); 23 | if (response !== null) { 24 | assert.ok(response.ok()); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.idea/puppeteer-request-spy.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/interface/IRequestModifier.ts: -------------------------------------------------------------------------------- 1 | import {Overrides, Request} from 'puppeteer'; 2 | import {RequestMatcher} from '../types/RequestMatcher'; 3 | 4 | export interface IRequestModifier { 5 | /** 6 | * @param interceptedRequest : puppeteers Request object to get the url from. 7 | * @param matcher <(url: string, pattern: string): Promise | boolean)>: matches the url with the pattern to block. 8 | * 9 | * checks if the ResponseFaker matches the Request and will provide a ResponseFake 10 | */ 11 | isMatchingRequest(interceptedRequest: Request, matcher: RequestMatcher): Promise | boolean; 12 | 13 | /** 14 | * @param interceptedRequest : puppeteers Request object 15 | * @return Overrides: Overrides as defined by puppeteer 16 | * 17 | * provides the RequestOverrides to be used by the RequestInterceptor to modify the request 18 | */ 19 | getOverride(interceptedRequest: Request): Promise | Overrides; 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig-lint.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "module": "commonjs", 5 | "noImplicitAny": true, 6 | "removeComments": true, 7 | "preserveConstEnums": false, 8 | "target": "es2015", 9 | "lib": ["es6", "DOM"], 10 | "moduleResolution": "node", 11 | "experimentalDecorators": true, 12 | "noEmitOnError": true, 13 | "allowUnreachableCode": false, 14 | "downlevelIteration": true, 15 | "locale": "en-us", 16 | "noFallthroughCasesInSwitch": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "strict": true, 22 | "strictNullChecks": true, 23 | "stripInternal": true, 24 | "suppressImplicitAnyIndexErrors": true, 25 | "outDir": "./build", 26 | "typeRoots": [ 27 | ] 28 | }, 29 | "exclude": [ 30 | "./src/**/VoidLogger.ts", 31 | "./test/**/*.ts", 32 | "./examples/**/*", 33 | "./types/**/*" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /src/interface/IResponseFaker.ts: -------------------------------------------------------------------------------- 1 | import {Request, RespondOptions} from 'puppeteer'; 2 | import {RequestMatcher} from '../types/RequestMatcher'; 3 | 4 | export interface IResponseFaker { 5 | /** 6 | * @param interceptedRequest : puppeteers Request object to get the url from. 7 | * @param matcher <(url: string, pattern: string): Promise | boolean)>: matches the url with the pattern to block. 8 | * 9 | * checks if the ResponseFaker matches the Request and will provide a ResponseFake 10 | */ 11 | isMatchingRequest(interceptedRequest: Request, matcher: RequestMatcher): Promise | boolean; 12 | 13 | /** 14 | * @param request : puppeteers Request object 15 | * @return RespondOptions: RespondOptions as defined by puppeteer 16 | * 17 | * provides the ResponseFake to be used by the RequestInterceptor to fake the response of the request 18 | */ 19 | getResponseFake(request: Request): RespondOptions | Promise; 20 | } 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | This Project uses Semantic Versioning. More information can be found [here](https://semver.org/). 2 | 3 | The branch and tag names also follow the following convention: 4 | - branches: ```v-x.y.z``` 5 | - tags: ```vx.y.z``` 6 | 7 | You only need to follow this convention when creating a Pull Request for a full npm release. 8 | 9 | Please make sure your branch passes the build process by running ```npm test```. 10 | You can check the code coverage by generating a html report using ```npm run test-coverage```. 11 | 12 | The tslint setting may seem harsh but they are usually useful to determine problems. 13 | Try to fix as much as possible but I am not contempt on keeping every rule. 14 | Some are a matter of choice after all. 15 | 16 | If you want to ensure a proper release, bump the version in the package.json and run ```npm run release-dry```. 17 | This will run all required steps for a successful release like ts-lint, build, test, generating types 18 | and creates a preview of the final package pushed to npm. 19 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { IRequestBlocker } from './interface/IRequestBlocker'; 2 | import { IRequestFactory } from './interface/IRequestFactory'; 3 | import { IRequestModifier } from './interface/IRequestModifier'; 4 | import { IRequestSpy } from './interface/IRequestSpy'; 5 | import { IResponseFaker } from './interface/IResponseFaker'; 6 | import { RequestInterceptor } from './RequestInterceptor'; 7 | import { RequestModifier } from './RequestModifier'; 8 | import { RequestRedirector } from './RequestRedirector'; 9 | import { RequestSpy } from './RequestSpy'; 10 | import { ResponseFaker } from './ResponseFaker'; 11 | import { ResponseModifier } from './ResponseModifier'; 12 | import { RequestMatcher } from './types/RequestMatcher'; 13 | import { ResponseModifierCallBack } from './types/ResponseModifierCallBack'; 14 | export { RequestInterceptor, RequestModifier, RequestRedirector, RequestSpy, ResponseFaker, ResponseModifier, IRequestBlocker, IRequestModifier, IRequestSpy, IResponseFaker, IRequestFactory, RequestMatcher, ResponseModifierCallBack }; 15 | -------------------------------------------------------------------------------- /test/integration/fakes/ajaxWithCustomHeaders.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "module": "commonjs", 5 | "noImplicitAny": true, 6 | "removeComments": true, 7 | "preserveConstEnums": false, 8 | "target": "ES2015", 9 | "lib": [ 10 | "es6", 11 | "dom" 12 | ], 13 | "moduleResolution": "node", 14 | "experimentalDecorators": true, 15 | "noEmitOnError": true, 16 | "allowUnreachableCode": false, 17 | "downlevelIteration": true, 18 | "locale": "en-us", 19 | "noFallthroughCasesInSwitch": true, 20 | "noImplicitReturns": true, 21 | "noImplicitThis": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "strict": true, 25 | "strictNullChecks": true, 26 | "stripInternal": true, 27 | "suppressImplicitAnyIndexErrors": true, 28 | "outDir": "./build", 29 | "typeRoots": [ 30 | "custom-typings", 31 | "./node_modules/@types" 32 | ] 33 | }, 34 | "include": [ 35 | "./src/**/*.ts", 36 | "./test/**/*.ts", 37 | "./custom-typings/index" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Tobias Nießen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /src/RequestBlocker.ts: -------------------------------------------------------------------------------- 1 | import {Request} from 'puppeteer'; 2 | import {UrlAccessor} from './common/urlAccessor/UrlAccessor'; 3 | import {UrlAccessorResolver} from './common/urlAccessor/UrlAccessorResolver'; 4 | import {IRequestBlocker} from './interface/IRequestBlocker'; 5 | import {RequestMatcher} from './types/RequestMatcher'; 6 | 7 | export class RequestBlocker implements IRequestBlocker { 8 | 9 | private urlsToBlock: Array = []; 10 | 11 | public shouldBlockRequest(request: Request, matcher: RequestMatcher): boolean { 12 | let urlAccessor: UrlAccessor = UrlAccessorResolver.getUrlAccessor(request); 13 | 14 | for (let urlToBlock of this.urlsToBlock) { 15 | if (matcher(urlAccessor.getUrlFromRequest(request), urlToBlock)) { 16 | return true; 17 | } 18 | } 19 | 20 | return false; 21 | } 22 | 23 | public addUrlsToBlock(urls: Array | string): void { 24 | this.urlsToBlock = this.urlsToBlock.concat(urls); 25 | } 26 | 27 | public clearUrlsToBlock(): void { 28 | this.urlsToBlock = []; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {IRequestBlocker} from './interface/IRequestBlocker'; 2 | import {IRequestFactory} from './interface/IRequestFactory'; 3 | import {IRequestModifier} from './interface/IRequestModifier'; 4 | import {IRequestSpy} from './interface/IRequestSpy'; 5 | import {IResponseFaker} from './interface/IResponseFaker'; 6 | import {RequestInterceptor} from './RequestInterceptor'; 7 | import {RequestModifier} from './RequestModifier'; 8 | import {RequestRedirector} from './RequestRedirector'; 9 | import {RequestSpy} from './RequestSpy'; 10 | import {ResponseFaker} from './ResponseFaker'; 11 | import {ResponseModifier} from './ResponseModifier'; 12 | import {RequestMatcher} from './types/RequestMatcher'; 13 | import {ResponseModifierCallBack} from './types/ResponseModifierCallBack'; 14 | 15 | export { 16 | RequestInterceptor, 17 | RequestModifier, 18 | RequestRedirector, 19 | RequestSpy, 20 | ResponseFaker, 21 | ResponseModifier, 22 | IRequestBlocker, 23 | IRequestModifier, 24 | IRequestSpy, 25 | IResponseFaker, 26 | IRequestFactory, 27 | RequestMatcher, 28 | ResponseModifierCallBack 29 | }; 30 | -------------------------------------------------------------------------------- /examples/CustomFaker.ts: -------------------------------------------------------------------------------- 1 | import {Request, RespondOptions} from 'puppeteer'; 2 | // import {IResponseFaker, RequestMatcher} from 'puppeteer-request-spy'; 3 | import {IResponseFaker, RequestMatcher} from '../..'; 4 | 5 | export class CustomFaker implements IResponseFaker { 6 | 7 | private patterns: Array = []; 8 | private fakesMap = { 9 | 'GET': 'some text', 10 | 'POST': 'Not Found!' 11 | }; 12 | 13 | public constructor(patterns: Array) { 14 | this.patterns = patterns; 15 | } 16 | 17 | public getResponseFake(request: Request): RespondOptions | Promise { 18 | return Promise.resolve( 19 | { 20 | status: 200, 21 | contentType: 'text/plain', 22 | body: this.fakesMap[request.method()] 23 | } 24 | ); 25 | } 26 | 27 | public isMatchingRequest(request: Request, matcher: RequestMatcher): boolean { 28 | for (let pattern of this.patterns) { 29 | if (matcher(request.url(), pattern)) { 30 | 31 | return true; 32 | } 33 | } 34 | 35 | return false; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/common/Browser.ts: -------------------------------------------------------------------------------- 1 | import * as puppeteer from 'puppeteer'; 2 | import {Browser, LaunchOptions} from 'puppeteer'; 3 | 4 | export class BrowserLauncher { 5 | 6 | private browser: Browser | undefined; 7 | private initialized: boolean = false; 8 | 9 | public async initialize(options?: LaunchOptions): Promise { 10 | this.initialized = true; 11 | if (typeof options === 'undefined') { 12 | options = { 13 | headless: true 14 | }; 15 | } 16 | 17 | this.browser = await puppeteer.launch(options); 18 | } 19 | 20 | public async closeBrowser(): Promise { 21 | if (typeof this.browser !== 'undefined') { 22 | await this.browser.close(); 23 | } 24 | } 25 | 26 | public async getBrowser(): Promise { 27 | if (typeof this.browser === 'undefined') { 28 | if (this.initialized) { 29 | throw new Error('unable to initialize browser.'); 30 | } else { 31 | await this.initialize(); 32 | } 33 | } 34 | 35 | return Promise.resolve( this.browser); 36 | } 37 | } 38 | 39 | export const browserLauncher: BrowserLauncher = new BrowserLauncher(); 40 | -------------------------------------------------------------------------------- /types/RequestInterceptor.d.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'puppeteer'; 2 | import { ILogger } from './common/Logger'; 3 | import { IRequestBlocker } from './interface/IRequestBlocker'; 4 | import { IRequestModifier } from './interface/IRequestModifier'; 5 | import { IRequestSpy } from './interface/IRequestSpy'; 6 | import { IResponseFaker } from './interface/IResponseFaker'; 7 | import { RequestMatcher } from './types/RequestMatcher'; 8 | export declare class RequestInterceptor { 9 | private requestSpies; 10 | private responseFakers; 11 | private requestModifiers; 12 | private matcher; 13 | private logger; 14 | private requestBlocker; 15 | constructor(matcher: RequestMatcher, logger?: ILogger); 16 | intercept(interceptedRequest: Request): Promise; 17 | addSpy(requestSpy: IRequestSpy): void; 18 | addFaker(responseFaker: IResponseFaker): void; 19 | addRequestModifier(requestModifier: IRequestModifier): void; 20 | block(urlsToBlock: Array | string): void; 21 | clearSpies(): void; 22 | clearFakers(): void; 23 | clearRequestModifiers(): void; 24 | clearUrlsToBlock(): void; 25 | setUrlsToBlock(urlsToBlock: Array): void; 26 | setRequestBlocker(requestBlocker: IRequestBlocker): void; 27 | private getMatchingFaker; 28 | private matchSpies; 29 | private getMatchingOverride; 30 | private blockUrl; 31 | private acceptUrl; 32 | } 33 | -------------------------------------------------------------------------------- /documentation/activity.iuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam shadowing false 3 | 4 | skinparam activity { 5 | DiamondFontSize 16 6 | FontSize 16 7 | BorderThickness 2 8 | 9 | FontColor black 10 | DiamondFontColor black 11 | BackgroundColor white 12 | DiamondBackgroundColor white 13 | BorderColor RoyalBlue 14 | DiamondBorderColor Seagreen 15 | 16 | StartColor 343131 17 | EndColor 343131 18 | } 19 | 20 | skinparam arrow { 21 | color 343131 22 | fontColor black 23 | FontSize 16 24 | } 25 | 26 | start 27 | 28 | if (\nRequest matches pattern\nof any spies?\n) then ( yes ) 29 | :Notify all matching spies; 30 | else ( no ) 31 | endif 32 | 33 | 34 | if (\nRequest matches any pattern\nof the RequestBlocker?\n) then ( no ) 35 | 36 | else ( yes ) 37 | :Block request\nwith request.abort; 38 | stop 39 | 40 | endif 41 | 42 | 43 | if (\nRequest matches pattern\nof any RequestModifiers?\n) then ( yes ) 44 | :Get __first__ matching\nRequestModifier; 45 | :Return modified response\nwith request.continue(overrides); 46 | stop 47 | else ( no ) 48 | if (\nRequest matches pattern\nof any ResponseFakers?\n) then ( yes ) 49 | :Get __first__ matching\nResponseFaker; 50 | :Respond Fake with\nrequest.respond; 51 | stop 52 | else ( no ) 53 | :Return original response\nwith request.continue(); 54 | stop 55 | endif 56 | endif 57 | 58 | 59 | 60 | @enduml 61 | -------------------------------------------------------------------------------- /test/common/filterForbiddenHeaders.ts: -------------------------------------------------------------------------------- 1 | const FORBIDDEN_STATIC_HEADERS: Array = [ 2 | 'Accept-Charset', 3 | 'Accept-Encoding', 4 | 'Access-Control-Request-Headers', 5 | 'Access-Control-Request-Method', 6 | 'Connection', 7 | 'Content-Length', 8 | 'Cookie', 9 | 'Cookie2', 10 | 'Date', 11 | 'DNT', 12 | 'Expect', 13 | 'Feature-Policy', 14 | 'Host', 15 | 'Keep-Alive', 16 | 'Origin', 17 | 'Referer', 18 | 'TE', 19 | 'Trailer', 20 | 'Transfer-Encoding', 21 | 'Upgrade', 22 | 'Via' 23 | ]; 24 | 25 | // Filters user-agent controlled headers for tests 26 | // more information here: https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name 27 | export function filterForbiddenHeaders(headers: { [index: string]: string }): { [index: string]: string } { 28 | 29 | Object.keys(headers) 30 | .map((header: string) => { 31 | if (FORBIDDEN_STATIC_HEADERS.indexOf(header) > -1) { 32 | delete headers[header]; 33 | } 34 | 35 | if (FORBIDDEN_STATIC_HEADERS.map((item: string) => item.toLowerCase()).indexOf(header.toLowerCase()) > -1) { 36 | delete headers[header.toLowerCase()]; 37 | } 38 | 39 | if (header.indexOf('Sec-') === 0 || header.indexOf('sec-') === 0) { 40 | delete headers[header]; 41 | } 42 | 43 | if (header.indexOf('Proxy-') === 0 || header.indexOf('proxy-') === 0) { 44 | delete headers[header]; 45 | } 46 | }); 47 | 48 | return headers; 49 | } 50 | -------------------------------------------------------------------------------- /examples/block-test.spec.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | const RequestInterceptor = require('puppeteer-request-spy').RequestInterceptor; 3 | const RequestSpy = require('puppeteer-request-spy').RequestSpy; 4 | const minimatch = require('minimatch'); 5 | const assert = require('assert'); 6 | 7 | let browser; 8 | 9 | before(async () => { 10 | browser = await puppeteer.launch({ 11 | headless: true 12 | }); 13 | }); 14 | 15 | after(async () => { 16 | await browser.close(); 17 | }); 18 | 19 | describe('example-block', function () { 20 | 21 | this.timeout(30000); 22 | this.slow(10000); 23 | 24 | let requestInterceptor; 25 | let secondaryRequestSpy; 26 | 27 | before(() => { 28 | requestInterceptor = new RequestInterceptor(minimatch, console); 29 | secondaryRequestSpy = new RequestSpy('!https://www.example.org/'); 30 | 31 | requestInterceptor.addSpy(secondaryRequestSpy); 32 | requestInterceptor.block('!https://www.example.org/'); 33 | }); 34 | 35 | describe('example-block', function () { 36 | it('example-test', async function () { 37 | let page = await browser.newPage(); 38 | 39 | page.setRequestInterception(true); 40 | page.on('request', requestInterceptor.intercept.bind(requestInterceptor)); 41 | 42 | await page.goto('https://www.example.org', { 43 | waitUntil: 'networkidle0', 44 | timeout: 3000000 45 | }); 46 | 47 | assert.ok(secondaryRequestSpy.hasMatch() && secondaryRequestSpy.getMatchCount() > 0); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /examples/simple-test.spec.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | const RequestInterceptor = require('puppeteer-request-spy').RequestInterceptor; 3 | const RequestSpy = require('puppeteer-request-spy').RequestSpy; 4 | const minimatch = require('minimatch'); 5 | const assert = require('assert'); 6 | 7 | let browser; 8 | 9 | before(async () => { 10 | browser = await puppeteer.launch({ 11 | headless: true 12 | }); 13 | }); 14 | 15 | after(async () => { 16 | await browser.close(); 17 | }); 18 | 19 | describe('example-block', function () { 20 | 21 | this.timeout(30000); 22 | this.slow(10000); 23 | 24 | let requestInterceptor; 25 | let cssSpy; 26 | 27 | before(() => { 28 | requestInterceptor = new RequestInterceptor(minimatch, console); 29 | cssSpy = new RequestSpy('**/*.css'); 30 | 31 | requestInterceptor.addSpy(cssSpy); 32 | requestInterceptor.block('**/specific.css'); 33 | }); 34 | 35 | describe('example-block', function () { 36 | it('example-test', async function () { 37 | let page = await browser.newPage(); 38 | 39 | page.setRequestInterception(true); 40 | page.on('request', requestInterceptor.intercept.bind(requestInterceptor)); 41 | 42 | await page.goto('https://www.example.org/', { 43 | waitUntil: 'networkidle0', 44 | timeout: 3000000 45 | }); 46 | 47 | assert.ok(cssSpy.hasMatch() && cssSpy.getMatchCount() > 0); 48 | 49 | for (let match of cssSpy.getMatchedRequests()) { 50 | // excludes specific.css since blocking requests will cause a failure 'net::ERR_FAILED' and response will be null 51 | if (!match.failure()) { 52 | assert.ok(match.response().ok()); 53 | } 54 | } 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /examples/keyword-matcher.spec.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | const RequestInterceptor = require('puppeteer-request-spy').RequestInterceptor; 3 | const RequestSpy = require('puppeteer-request-spy').RequestSpy; 4 | const assert = require('assert'); 5 | 6 | let browser; 7 | 8 | before(async () => { 9 | browser = await puppeteer.launch({ 10 | headless: true 11 | }); 12 | }); 13 | 14 | after(async () => { 15 | await browser.close(); 16 | }); 17 | 18 | describe('example-block', function () { 19 | 20 | this.timeout(30000); 21 | this.slow(20000); 22 | 23 | let requestInterceptor; 24 | let imagesSpy; 25 | 26 | before(() => { 27 | imagesSpy = new RequestSpy('pictures'); 28 | requestInterceptor = new RequestInterceptor( 29 | (testee, pattern) => testee.indexOf(pattern) > -1, 30 | console 31 | ); 32 | 33 | requestInterceptor.addSpy(imagesSpy); 34 | }); 35 | 36 | describe('example-block', function () { 37 | it('example-test', async function () { 38 | let page = await browser.newPage(); 39 | await page.setRequestInterception(true); 40 | 41 | page.on('request', requestInterceptor.intercept.bind(requestInterceptor)); 42 | 43 | // waiting for networkidle0 ensures that all request have been loaded before the page.goto promise resolves 44 | await page.goto('https://www.example.org/', { 45 | waitUntil: 'networkidle0', 46 | timeout: 3000000 47 | }); 48 | 49 | // verify spy found matches 50 | assert.ok(imagesSpy.hasMatch() && imagesSpy.getMatchCount() > 0); 51 | 52 | // verify status code for all matching requests 53 | for (let match of imagesSpy.getMatchedRequests()) { 54 | assert.ok(match.response().ok()); 55 | } 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/ResponseFaker.ts: -------------------------------------------------------------------------------- 1 | import {Request, RespondOptions} from 'puppeteer'; 2 | import {resolveOptionalPromise} from './common/resolveOptionalPromise'; 3 | import {UrlAccessor} from './common/urlAccessor/UrlAccessor'; 4 | import {UrlAccessorResolver} from './common/urlAccessor/UrlAccessorResolver'; 5 | import {IResponseFaker} from './interface/IResponseFaker'; 6 | import {RequestMatcher} from './types/RequestMatcher'; 7 | 8 | export class ResponseFaker implements IResponseFaker { 9 | private patterns: Array = []; 10 | private responseFakeFactory: (request: Request) => RespondOptions | Promise; 11 | 12 | public constructor( 13 | patterns: Array | string, 14 | responseFake: ((request: Request) => RespondOptions | Promise) | RespondOptions 15 | ) { 16 | if (typeof patterns !== 'string' && patterns.constructor !== Array) { 17 | throw new Error('invalid pattern, pattern must be of type string or string[].'); 18 | } 19 | 20 | if (typeof patterns === 'string') { 21 | patterns = [patterns]; 22 | } 23 | 24 | this.patterns = patterns; 25 | this.responseFakeFactory = typeof responseFake === 'function' ? responseFake : (): RespondOptions => responseFake; 26 | } 27 | 28 | public async getResponseFake(request: Request): Promise { 29 | return await resolveOptionalPromise(this.responseFakeFactory(request)); 30 | } 31 | 32 | public isMatchingRequest(request: Request, matcher: RequestMatcher): boolean { 33 | let urlAccessor: UrlAccessor = UrlAccessorResolver.getUrlAccessor(request); 34 | for (let pattern of this.patterns) { 35 | if (matcher(urlAccessor.getUrlFromRequest(request), pattern)) { 36 | return true; 37 | } 38 | } 39 | 40 | return false; 41 | } 42 | 43 | public getPatterns(): Array { 44 | return this.patterns; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/RequestModifier.ts: -------------------------------------------------------------------------------- 1 | import {Overrides, Request} from 'puppeteer'; 2 | import {resolveOptionalPromise} from './common/resolveOptionalPromise'; 3 | import {UrlAccessor} from './common/urlAccessor/UrlAccessor'; 4 | import {UrlAccessorResolver} from './common/urlAccessor/UrlAccessorResolver'; 5 | import {IRequestModifier} from './interface/IRequestModifier'; 6 | import {RequestMatcher} from './types/RequestMatcher'; 7 | 8 | export class RequestModifier implements IRequestModifier { 9 | private patterns: Array; 10 | private requestOverrideFactory: (request: Request) => Promise | Overrides; 11 | 12 | public constructor( 13 | patterns: Array | string, 14 | requestOverride: ((request: Request) => Promise | Overrides) | Overrides 15 | ) { 16 | if (typeof patterns !== 'string' && patterns.constructor !== Array) { 17 | throw new Error('invalid pattern, pattern must be of type string or string[].'); 18 | } 19 | 20 | if (typeof patterns === 'string') { 21 | patterns = [patterns]; 22 | } 23 | 24 | this.patterns = patterns; 25 | this.requestOverrideFactory = typeof requestOverride === 'function' 26 | ? requestOverride 27 | : (): Overrides => requestOverride; 28 | } 29 | 30 | public async getOverride(request: Request): Promise { 31 | return resolveOptionalPromise(this.requestOverrideFactory(request)); 32 | } 33 | 34 | public isMatchingRequest(request: Request, matcher: RequestMatcher): boolean { 35 | let urlAccessor: UrlAccessor = UrlAccessorResolver.getUrlAccessor(request); 36 | 37 | for (let pattern of this.patterns) { 38 | if (matcher(urlAccessor.getUrlFromRequest(request), pattern)) { 39 | return true; 40 | } 41 | } 42 | 43 | return false; 44 | } 45 | 46 | public getPatterns(): Array { 47 | return this.patterns; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/RequestRedirector.ts: -------------------------------------------------------------------------------- 1 | import {Overrides, Request} from 'puppeteer'; 2 | import {resolveOptionalPromise} from './common/resolveOptionalPromise'; 3 | import {UrlAccessor} from './common/urlAccessor/UrlAccessor'; 4 | import {UrlAccessorResolver} from './common/urlAccessor/UrlAccessorResolver'; 5 | import {IRequestModifier} from './interface/IRequestModifier'; 6 | import {RequestMatcher} from './types/RequestMatcher'; 7 | 8 | export class RequestRedirector implements IRequestModifier { 9 | private patterns: Array; 10 | private redirectionUrlFactory: ((request: Request) => string | Promise); 11 | 12 | public constructor( 13 | patterns: Array | string, 14 | redirectionUrl: ((request: Request) => Promise | string) | string 15 | ) { 16 | if (typeof patterns !== 'string' && patterns.constructor !== Array) { 17 | throw new Error('invalid pattern, pattern must be of type string or string[].'); 18 | } 19 | 20 | if (typeof patterns === 'string') { 21 | patterns = [patterns]; 22 | } 23 | 24 | this.patterns = patterns; 25 | this.redirectionUrlFactory = typeof redirectionUrl === 'function' 26 | ? redirectionUrl 27 | : (): string => redirectionUrl; 28 | } 29 | 30 | public isMatchingRequest(request: Request, matcher: RequestMatcher): boolean { 31 | let urlAccessor: UrlAccessor = UrlAccessorResolver.getUrlAccessor(request); 32 | for (let pattern of this.patterns) { 33 | if (matcher(urlAccessor.getUrlFromRequest(request), pattern)) { 34 | return true; 35 | } 36 | } 37 | 38 | return false; 39 | } 40 | 41 | public getPatterns(): Array { 42 | return this.patterns; 43 | } 44 | 45 | public async getOverride(interceptedRequest: Request): Promise { 46 | return { 47 | url: (await resolveOptionalPromise(this.redirectionUrlFactory(interceptedRequest))), 48 | method: interceptedRequest.method(), 49 | headers: interceptedRequest.headers(), 50 | postData: interceptedRequest.postData() 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/RequestSpy.ts: -------------------------------------------------------------------------------- 1 | import {Request} from 'puppeteer'; 2 | import {UrlAccessor} from './common/urlAccessor/UrlAccessor'; 3 | import {UrlAccessorResolver} from './common/urlAccessor/UrlAccessorResolver'; 4 | import {IRequestSpy} from './interface/IRequestSpy'; 5 | import {RequestMatcher} from './types/RequestMatcher'; 6 | 7 | export class RequestSpy implements IRequestSpy { 8 | 9 | private hasMatchingUrl: boolean = false; 10 | private matchCount: number = 0; 11 | private patterns: Array = []; 12 | private matchedRequests: Array = []; 13 | 14 | public constructor(patterns: Array | string) { 15 | if (typeof patterns !== 'string' && patterns.constructor !== Array) { 16 | throw new Error('invalid pattern, pattern must be of type string or string[].'); 17 | } 18 | 19 | if (typeof patterns === 'string') { 20 | patterns = [patterns]; 21 | } 22 | 23 | this.patterns = patterns; 24 | } 25 | 26 | public getPatterns(): Array { 27 | return this.patterns; 28 | } 29 | 30 | public getMatchedRequests(): Array { 31 | return this.matchedRequests; 32 | } 33 | 34 | public hasMatch(): boolean { 35 | return this.hasMatchingUrl; 36 | } 37 | 38 | public addMatch(matchedRequest: Request): void { 39 | this.hasMatchingUrl = true; 40 | this.matchedRequests.push(matchedRequest); 41 | this.matchCount++; 42 | } 43 | 44 | public isMatchingRequest(request: Request, matcher: RequestMatcher): boolean { 45 | let urlAccessor: UrlAccessor = UrlAccessorResolver.getUrlAccessor(request); 46 | 47 | for (let pattern of this.patterns) { 48 | if (matcher(urlAccessor.getUrlFromRequest(request), pattern)) { 49 | return true; 50 | } 51 | } 52 | 53 | return false; 54 | } 55 | 56 | public getMatchedUrls(): Array { 57 | let matchedUrls: Array = []; 58 | for (let match of this.matchedRequests) { 59 | let urlAccessor: UrlAccessor = UrlAccessorResolver.getUrlAccessor(match); 60 | 61 | let url: string = urlAccessor.getUrlFromRequest(match); 62 | matchedUrls.push(url); 63 | } 64 | 65 | return matchedUrls; 66 | } 67 | 68 | public getMatchCount(): number { 69 | return this.matchCount; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test/isolation/UrlAccessorResolver.dev.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as clearModule from 'clear-module'; 3 | import {Request} from 'puppeteer'; 4 | import {UrlAccessor} from '../../src/common/urlAccessor/UrlAccessor'; 5 | import {UrlFunctionAccessor} from '../../src/common/urlAccessor/UrlFunctionAccessor'; 6 | import {UrlStringAccessor} from '../../src/common/urlAccessor/UrlStringAccessor'; 7 | import {TestDouble} from '../common/TestDouble'; 8 | import {getLowVersionRequestDouble, getRequestDouble} from '../common/testDoubleFactories'; 9 | 10 | describe('module: UrlAccessorResolver', (): void => { 11 | beforeEach(() => { 12 | clearModule('../../src/common/urlAccessor/UrlAccessorResolver'); 13 | }); 14 | 15 | afterEach(() => { 16 | clearModule('../../src/common/urlAccessor/UrlAccessorResolver'); 17 | }); 18 | 19 | it('old puppeteer version resolves to UrlStringAccessor', async (): Promise => { 20 | let request: TestDouble = getLowVersionRequestDouble(); 21 | let UrlAccessorResolver: any = (await import('../../src/common/urlAccessor/UrlAccessorResolver')).UrlAccessorResolver; 22 | let urlAccessor: UrlAccessor = UrlAccessorResolver.getUrlAccessor( request); 23 | 24 | assert.ok(urlAccessor instanceof UrlStringAccessor); 25 | assert.strictEqual(urlAccessor.getUrlFromRequest( request), 'any-url'); 26 | }); 27 | 28 | it('new puppeteer version resolves to UrlFunctionAccessor', async (): Promise => { 29 | let request: TestDouble = getRequestDouble(); 30 | let UrlAccessorResolver: any = (await import('../../src/common/urlAccessor/UrlAccessorResolver')).UrlAccessorResolver; 31 | let urlAccessor: UrlAccessor = UrlAccessorResolver.getUrlAccessor( request); 32 | 33 | assert.ok(urlAccessor instanceof UrlFunctionAccessor); 34 | assert.strictEqual(urlAccessor.getUrlFromRequest( request), 'any-url'); 35 | }); 36 | 37 | it('running test twice provides same cached accessor', async (): Promise => { 38 | let request: TestDouble = getRequestDouble(); 39 | let UrlAccessorResolver: any = (await import('../../src/common/urlAccessor/UrlAccessorResolver')).UrlAccessorResolver; 40 | let urlAccessor1: UrlAccessor = UrlAccessorResolver.getUrlAccessor( request); 41 | let urlAccessor2: UrlAccessor = UrlAccessorResolver.getUrlAccessor( request); 42 | 43 | assert.deepStrictEqual(urlAccessor1, urlAccessor2); 44 | assert.ok(urlAccessor1 === urlAccessor2); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff: 7 | .idea/ 8 | 9 | # CMake 10 | cmake-build-debug/ 11 | 12 | ## File-based project format: 13 | *.iws 14 | 15 | ## Plugin-specific files: 16 | 17 | # IntelliJ 18 | out/ 19 | 20 | # mpeltonen/sbt-idea plugin 21 | .idea_modules/ 22 | 23 | # JIRA plugin 24 | atlassian-ide-plugin.xml 25 | 26 | # Cursive Clojure plugin 27 | .idea/replstate.xml 28 | 29 | # Crashlytics plugin (for Android Studio and IntelliJ) 30 | com_crashlytics_export_strings.xml 31 | crashlytics.properties 32 | crashlytics-build.properties 33 | fabric.properties 34 | ### Windows template 35 | # Windows thumbnail cache files 36 | Thumbs.db 37 | ehthumbs.db 38 | ehthumbs_vista.db 39 | 40 | # Dump file 41 | *.stackdump 42 | 43 | # Folder config file 44 | Desktop.ini 45 | 46 | # Recycle Bin used on file shares 47 | $RECYCLE.BIN/ 48 | 49 | # Windows Installer files 50 | *.cab 51 | *.msi 52 | *.msm 53 | *.msp 54 | 55 | # Windows shortcuts 56 | *.lnk 57 | ### Node template 58 | # Logs 59 | logs 60 | *.log 61 | npm-debug.log* 62 | yarn-debug.log* 63 | yarn-error.log* 64 | 65 | # Runtime data 66 | pids 67 | *.pid 68 | *.seed 69 | *.pid.lock 70 | 71 | # Directory for instrumented libs generated by jscoverage/JSCover 72 | lib-cov 73 | 74 | # Coverage directory used by tools like istanbul 75 | coverage 76 | 77 | # nyc test coverage 78 | .nyc_output 79 | 80 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 81 | .grunt 82 | 83 | # Bower dependency directory (https://bower.io/) 84 | bower_components 85 | 86 | # node-waf configuration 87 | .lock-wscript 88 | 89 | # Compiled binary addons (https://nodejs.org/api/addons.html) 90 | build/Release 91 | 92 | # Dependency directories 93 | node_modules/ 94 | jspm_packages/ 95 | 96 | # Typescript v1 declaration files 97 | typings/ 98 | examples/ 99 | 100 | # Optional npm cache directory 101 | .npm 102 | 103 | # Optional eslint cache 104 | .eslintcache 105 | 106 | # Optional REPL history 107 | .node_repl_history 108 | 109 | # Output of 'npm pack' 110 | *.tgz 111 | 112 | # Yarn Integrity file 113 | .yarn-integrity 114 | 115 | # dotenv environment variables file 116 | .env 117 | src/ 118 | build/test/ 119 | test/ 120 | task/ 121 | tsconfig.json 122 | tsconfig-lint.json 123 | tslint.json 124 | typings.json 125 | build/**/*.js.map 126 | custom-typings/ 127 | .travis.yml 128 | .coveralls.yml 129 | CONTRIBUTING.md 130 | notes.md 131 | documentation/ 132 | puppeteer-request-spy.iml 133 | -------------------------------------------------------------------------------- /examples/fake-test.spec.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | const RequestInterceptor = require('puppeteer-request-spy').RequestInterceptor; 3 | const ResponseFaker = require('puppeteer-request-spy').ResponseFaker; 4 | const minimatch = require('minimatch'); 5 | const assert = require('assert'); 6 | const fs = require('fs'); 7 | 8 | let browser; 9 | 10 | before(async () => { 11 | browser = await puppeteer.launch({ 12 | headless: true 13 | }); 14 | }); 15 | 16 | after(async () => { 17 | await browser.close(); 18 | }); 19 | 20 | describe('example-block', function () { 21 | 22 | this.timeout(30000); 23 | this.slow(10000); 24 | 25 | let requestInterceptor; 26 | let defaultPicture; 27 | let jsonResponseFaker; 28 | let imageResponseFaker; 29 | let textResponseFaker; 30 | let htmlResponseFaker; 31 | 32 | before(() => { 33 | requestInterceptor = new RequestInterceptor(minimatch, console); 34 | defaultPicture = fs.readFileSync('./some-picture.png'); 35 | imageResponseFaker = new ResponseFaker('**/*.jpg', { 36 | status: 200, 37 | contentType: 'image/png', 38 | body: defaultPicture 39 | }); 40 | 41 | textResponseFaker = new ResponseFaker('**/some-path', { 42 | status: 200, 43 | contentType: 'text/plain', 44 | body: 'some static text' 45 | }); 46 | 47 | htmlResponseFaker = new ResponseFaker('**/some-path', { 48 | status: 200, 49 | contentType: 'text/html', 50 | body: '
some static html
' 51 | }); 52 | 53 | jsonResponseFaker = new ResponseFaker('**/*.json', { 54 | status: 200, 55 | contentType: 'application/json', 56 | body: JSON.stringify({data: []}) 57 | }); 58 | 59 | requestInterceptor.addFaker(imageResponseFaker); 60 | requestInterceptor.addFaker(textResponseFaker); 61 | requestInterceptor.addFaker(htmlResponseFaker); 62 | requestInterceptor.addFaker(jsonResponseFaker); 63 | }); 64 | 65 | describe('example-block', function () { 66 | it('example-test', async function () { 67 | let page = await browser.newPage(); 68 | 69 | page.setRequestInterception(true); 70 | page.on('request', requestInterceptor.intercept.bind(requestInterceptor)); 71 | 72 | await page.goto('https://www.example.org', { 73 | waitUntil: 'networkidle0', 74 | timeout: 3000000 75 | }); 76 | 77 | let ajaxContent = await page.evaluate(() => { 78 | return document.getElementById('some-id').innerHTML; 79 | }); 80 | 81 | assert.strictEqual(ajaxContent, '
some static html
'); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/ResponseModifier.ts: -------------------------------------------------------------------------------- 1 | import {Request, RespondOptions} from 'puppeteer'; 2 | import {HttpRequestFactory} from './common/HttpRequestFactory'; 3 | import {resolveOptionalPromise} from './common/resolveOptionalPromise'; 4 | import {UrlAccessor} from './common/urlAccessor/UrlAccessor'; 5 | import {UrlAccessorResolver} from './common/urlAccessor/UrlAccessorResolver'; 6 | import {IRequestFactory} from './interface/IRequestFactory'; 7 | import {IResponseFaker} from './interface/IResponseFaker'; 8 | import {RequestMatcher} from './types/RequestMatcher'; 9 | import {ResponseModifierCallBack} from './types/ResponseModifierCallBack'; 10 | 11 | export class ResponseModifier implements IResponseFaker { 12 | private patterns: Array; 13 | private responseModifierCallBack: ResponseModifierCallBack; 14 | private httpRequestFactory: IRequestFactory; 15 | 16 | public constructor( 17 | patterns: Array | string, 18 | responseModifierCallBack: ResponseModifierCallBack, 19 | httpRequestFactory: IRequestFactory = (new HttpRequestFactory()) 20 | ) { 21 | if (typeof patterns !== 'string' && patterns.constructor !== Array) { 22 | throw new Error('invalid pattern, pattern must be of type string or string[].'); 23 | } 24 | 25 | if (typeof patterns === 'string') { 26 | patterns = [patterns]; 27 | } 28 | 29 | this.patterns = patterns; 30 | this.responseModifierCallBack = responseModifierCallBack; 31 | this.httpRequestFactory = httpRequestFactory; 32 | } 33 | 34 | public isMatchingRequest(request: Request, matcher: RequestMatcher): boolean { 35 | let urlAccessor: UrlAccessor = UrlAccessorResolver.getUrlAccessor(request); 36 | for (let pattern of this.patterns) { 37 | if (matcher(urlAccessor.getUrlFromRequest(request), pattern)) { 38 | return true; 39 | } 40 | } 41 | 42 | return false; 43 | } 44 | 45 | public async getResponseFake(request: Request): Promise { 46 | let originalResponse: RespondOptions = {}; 47 | let error: Error | undefined; 48 | let body: string; 49 | 50 | try { 51 | originalResponse = await this.httpRequestFactory.createRequest(request); 52 | body = originalResponse.body; 53 | } catch (err) { 54 | error = err; 55 | body = ''; 56 | } 57 | 58 | return Object.assign( 59 | {}, 60 | originalResponse, 61 | { 62 | body: await resolveOptionalPromise(this.responseModifierCallBack( 63 | error, 64 | body, 65 | request 66 | )) 67 | } 68 | ); 69 | } 70 | 71 | public getPatterns(): Array { 72 | return this.patterns; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/common/testServer.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as Koa from 'koa'; 3 | import {ExtendableContext} from 'koa'; 4 | import * as bodyParser from 'koa-bodyparser'; 5 | import * as mime from 'mime-types'; 6 | import * as path from 'path'; 7 | import {serverSettings} from './ServerSettings'; 8 | import Timer = NodeJS.Timer; 9 | 10 | 11 | function buildTestServer(): Koa { 12 | let app: Koa = new Koa(); 13 | 14 | app.use(bodyParser()); 15 | 16 | 17 | async function wait(time: number): Promise { 18 | return new Promise(((resolve: () => void): Timer => setTimeout(resolve, time))); 19 | } 20 | 21 | 22 | app.use(async (ctx: ExtendableContext, next: () => Promise) => { 23 | await next(); 24 | 25 | if (ctx.path === '/test-post-unavailable') { 26 | await wait(20); 27 | } 28 | }); 29 | 30 | 31 | app.use(async (ctx: ExtendableContext, next: () => Promise) => { 32 | await next(); 33 | if (!ctx.body) { 34 | ctx.body = 'ServerDouble: 404!'; 35 | } 36 | }); 37 | 38 | 39 | app.use(async (ctx: ExtendableContext, next: () => Promise) => { 40 | const ROOT_DIR: string = path.resolve(global.process.cwd(), serverSettings.rootPath); 41 | let requestPath: string = path.join(ROOT_DIR, ctx.path.replace('/fakes/', '')); 42 | 43 | if (fs.existsSync(requestPath)) { 44 | ctx.type = mime.lookup(requestPath) || 'text/plain'; 45 | ctx.body = fs.readFileSync(requestPath); 46 | } 47 | 48 | await next(); 49 | }); 50 | 51 | 52 | app.use(async (ctx: ExtendableContext, next: () => Promise) => { 53 | 54 | if (ctx.path === '/test-post-fake') { 55 | ctx.status = 200; 56 | ctx.type = 'application/json'; 57 | ctx.body = JSON.stringify({a: ctx.request.body.a * 1, n: ctx.request.body.n * 1}, null, 2); 58 | 59 | return; 60 | } 61 | 62 | if (ctx.path === '/test-post-real') { 63 | ctx.status = 200; 64 | ctx.type = 'application/json'; 65 | ctx.body = JSON.stringify({a: ctx.request.body.a * 100, n: ctx.request.body.n * 100}, null, 2); 66 | 67 | return; 68 | } 69 | 70 | await next(); 71 | }); 72 | 73 | app.use(async (ctx: ExtendableContext) => { 74 | if (ctx.path === '/show-headers-real') { 75 | ctx.status = 200; 76 | ctx.type = 'application/json'; 77 | ctx.body = JSON.stringify(ctx.request.headers, null, 2); 78 | } 79 | 80 | if (ctx.path === '/show-headers-fake') { 81 | ctx.status = 200; 82 | ctx.type = 'application/json'; 83 | ctx.body = JSON.stringify(ctx.request.headers, null, 2); 84 | } 85 | 86 | return; 87 | }); 88 | 89 | return app; 90 | } 91 | 92 | 93 | export const testServer: Koa = buildTestServer(); 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff: 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/dictionaries 10 | .idea/sonarlint/ 11 | .idea/StickySelectionHighlights.xml 12 | .idea/deployment.xml 13 | 14 | # Sensitive or high-churn files: 15 | .idea/**/dataSources/ 16 | .idea/**/dataSources.ids 17 | .idea/**/dataSources.xml 18 | .idea/**/dataSources.local.xml 19 | .idea/**/sqlDataSources.xml 20 | .idea/**/dynamic.xml 21 | .idea/**/uiDesigner.xml 22 | 23 | # Gradle: 24 | .idea/**/gradle.xml 25 | .idea/**/libraries 26 | 27 | # CMake 28 | cmake-build-debug/ 29 | 30 | # Mongo Explorer plugin: 31 | .idea/**/mongoSettings.xml 32 | 33 | ## File-based project format: 34 | *.iws 35 | 36 | ## Plugin-specific files: 37 | 38 | # IntelliJ 39 | out/ 40 | 41 | # mpeltonen/sbt-idea plugin 42 | .idea_modules/ 43 | 44 | # JIRA plugin 45 | atlassian-ide-plugin.xml 46 | 47 | # Cursive Clojure plugin 48 | .idea/replstate.xml 49 | 50 | # Crashlytics plugin (for Android Studio and IntelliJ) 51 | com_crashlytics_export_strings.xml 52 | crashlytics.properties 53 | crashlytics-build.properties 54 | fabric.properties 55 | ### Windows template 56 | # Windows thumbnail cache files 57 | Thumbs.db 58 | ehthumbs.db 59 | ehthumbs_vista.db 60 | 61 | # Dump file 62 | *.stackdump 63 | 64 | # Folder config file 65 | Desktop.ini 66 | 67 | # Recycle Bin used on file shares 68 | $RECYCLE.BIN/ 69 | 70 | # Windows Installer files 71 | *.cab 72 | *.msi 73 | *.msm 74 | *.msp 75 | 76 | # Windows shortcuts 77 | *.lnk 78 | ### Node template 79 | # Logs 80 | logs 81 | *.log 82 | npm-debug.log* 83 | yarn-debug.log* 84 | yarn-error.log* 85 | 86 | # Runtime data 87 | pids 88 | *.pid 89 | *.seed 90 | *.pid.lock 91 | 92 | # Directory for instrumented libs generated by jscoverage/JSCover 93 | lib-cov 94 | 95 | # Coverage directory used by tools like istanbul 96 | coverage 97 | 98 | # nyc test coverage 99 | .nyc_output 100 | 101 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 102 | .grunt 103 | 104 | # Bower dependency directory (https://bower.io/) 105 | bower_components 106 | 107 | # node-waf configuration 108 | .lock-wscript 109 | 110 | # Compiled binary addons (https://nodejs.org/api/addons.html) 111 | build/Release 112 | 113 | # Dependency directories 114 | node_modules/ 115 | jspm_packages/ 116 | .coveralls.yml 117 | # Optional npm cache directory 118 | .npm 119 | 120 | # Optional eslint cache 121 | .eslintcache 122 | 123 | # Optional REPL history 124 | .node_repl_history 125 | 126 | # Output of 'npm pack' 127 | *.tgz 128 | 129 | # Yarn Integrity file 130 | .yarn-integrity 131 | 132 | # dotenv environment variables file 133 | .env 134 | 135 | build/ 136 | notes.md 137 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppeteer-request-spy", 3 | "version": "1.4.0", 4 | "description": "watch, fake, modify or block requests from puppeteer matching patterns", 5 | "main": "build/src/index.js", 6 | "scripts": { 7 | "clean": "node task/clean.js", 8 | "pregenerate-typings": "npm run clean", 9 | "generate-typings": "tsc -p tsconfig.json -d", 10 | "preupdate-typings": "npm run generate-typings", 11 | "update-typings": "node task/copyTypes.js", 12 | "prebuild": "node_modules/.bin/tslint --project tsconfig-lint.json && npm run clean", 13 | "prebuild-dev": "npm run prebuild", 14 | "build": "node_modules/.bin/tsc -p tsconfig.json", 15 | "build-dev": "node_modules/.bin/tsc -p tsconfig.json --sourcemap", 16 | "pretest": "npm run build", 17 | "test": "node_modules/.bin/mocha --timeout 10000 --require source-map-support/register build/test/**/*.spec.js", 18 | "pretest-coverage": "npm run build-dev", 19 | "pretest-silent-full": "npm run build", 20 | "test-silent-full": "node_modules/.bin/mocha --timeout 10000 --require source-map-support/register build/test/**/*.spec.js > test-ts.log", 21 | "test-coverage": "node_modules/.bin/nyc --all --reporter=html npm run test-silent", 22 | "pretest-coverage-travis": "npm run build-dev", 23 | "test-silent": "node_modules/.bin/mocha --timeout 10000 --require source-map-support/register build/test/**/*.dev.spec.js > test-ts.log", 24 | "test-coverage-travis": "node_modules/.bin/nyc --all --reporter=text-lcov npm run test-silent | node node_modules/coveralls/bin/coveralls.js", 25 | "prerelease-dry": "npm run update-typings && npm run test-silent-full", 26 | "release-dry": "npm pack" 27 | }, 28 | "engines": { 29 | "node": ">=6.4.0" 30 | }, 31 | "nyc": { 32 | "extension": [ 33 | ".ts", 34 | ".tsx" 35 | ], 36 | "include": [ 37 | "build/src/**/*.js" 38 | ], 39 | "exclude": [ 40 | "**/Logger.js", 41 | "**/index.js", 42 | "**/interface/*.js", 43 | "**/types/*.js" 44 | ], 45 | "reporter": [ 46 | "html" 47 | ], 48 | "all": true 49 | }, 50 | "keywords": [ 51 | "puppeteer", 52 | "request", 53 | "spy", 54 | "testing", 55 | "test", 56 | "chrome", 57 | "headless" 58 | ], 59 | "types": "types/index.d.ts", 60 | "author": "Tobias Nießen", 61 | "license": "MIT", 62 | "devDependencies": { 63 | "@types/fs-extra": "^5.0.4", 64 | "@types/koa": "^2.11.4", 65 | "@types/koa-bodyparser": "^4.3.0", 66 | "@types/mime-types": "^2.1.0", 67 | "@types/minimatch": "^3.0.3", 68 | "@types/mocha": "^5.2.0", 69 | "@types/nock": "^9.3.1", 70 | "@types/node": "^10.0.4", 71 | "@types/puppeteer": "^1.3.1", 72 | "@types/sinon": "^4.3.1", 73 | "clear-module": "^3.1.0", 74 | "coveralls": "^3.0.1", 75 | "del": "^3.0.0", 76 | "fs-extra": "^7.0.1", 77 | "koa": "^2.13.0", 78 | "koa-bodyparser": "^4.3.0", 79 | "mime-types": "^2.1.27", 80 | "minimatch": "^3.0.4", 81 | "mocha": "^5.1.1", 82 | "nock": "^10.0.6", 83 | "nyc": "^11.7.1", 84 | "puppeteer": "^1.20.0", 85 | "sinon": "^4.5.0", 86 | "source-map-support": "^0.5.5", 87 | "ts-node": "^7.0.1", 88 | "tslint": "^5.8.0", 89 | "tslint-microsoft-contrib": "^5.0.1", 90 | "typescript": "^3.3.3333" 91 | }, 92 | "repository": { 93 | "type": "git", 94 | "url": "https://github.com/Tabueeee/puppeteer-request-spy.git" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/common/HttpRequestFactory.ts: -------------------------------------------------------------------------------- 1 | import {ClientRequest, IncomingHttpHeaders, IncomingMessage, request as httpRequest, RequestOptions} from 'http'; 2 | import {request as httpsRequest} from 'https'; 3 | import {Request, RespondOptions} from 'puppeteer'; 4 | import {URL} from 'url'; 5 | import {IRequestFactory} from '../interface/IRequestFactory'; 6 | import {UrlAccessor} from './urlAccessor/UrlAccessor'; 7 | import {UrlAccessorResolver} from './urlAccessor/UrlAccessorResolver'; 8 | 9 | export class HttpRequestFactory implements IRequestFactory { 10 | private timeout: number; 11 | 12 | public constructor(timeout: number = 30000) { 13 | this.timeout = timeout; 14 | } 15 | 16 | public createRequest(request: Request): Promise { 17 | let urlAccessor: UrlAccessor = UrlAccessorResolver.getUrlAccessor(request); 18 | let urlString: string = urlAccessor.getUrlFromRequest(request); 19 | 20 | return new Promise((resolve: (options: RespondOptions & { body: string }) => void, reject: (error: Error) => void): void => { 21 | let url: URL = new URL(urlString); 22 | 23 | let headers: { [index: string]: string } = {}; 24 | Object.assign(headers, request.headers()); 25 | 26 | let options: RequestOptions = { 27 | protocol: url.protocol, 28 | method: request.method(), 29 | hostname: url.hostname, 30 | port: url.port, 31 | path: url.pathname + url.search, 32 | headers: headers 33 | }; 34 | 35 | let timeout: NodeJS.Timeout; 36 | let requestInterface: typeof httpsRequest = url.protocol === 'https:' ? httpsRequest : httpRequest; 37 | 38 | let req: ClientRequest = requestInterface(options, (res: IncomingMessage) => { 39 | 40 | let chunks: Array = []; 41 | 42 | res.on('data', (chunk: Buffer) => { 43 | chunks.push(chunk); 44 | }); 45 | 46 | res.on('end', () => { 47 | let body: Buffer = Buffer.concat(chunks); 48 | 49 | clearTimeout(timeout); 50 | 51 | resolve( 52 | { 53 | body: body.toString(), 54 | contentType: res.headers['content-type'], 55 | status: res.statusCode, 56 | headers: Object 57 | .keys(res.headers) 58 | .reduce(this.convertHeaders.bind(this, res.headers), {}) 59 | } 60 | ); 61 | }); 62 | }); 63 | 64 | timeout = setTimeout( 65 | () => { 66 | req.end(); 67 | reject(new Error(`unable to load: ${url}. request timed out after ${this.timeout / 1000} seconds.`)); 68 | }, 69 | this.timeout 70 | ); 71 | 72 | req.end(); 73 | }); 74 | } 75 | 76 | private convertHeaders( 77 | responseHeaders: IncomingHttpHeaders, 78 | prev: { [index: string]: string }, 79 | key: string 80 | ): { [index: string]: string } { 81 | let currentHeader: string | Array | undefined = responseHeaders[key]; 82 | 83 | if (typeof currentHeader === 'string') { 84 | prev[key] = currentHeader; 85 | } else if (Array.isArray(currentHeader)) { 86 | prev[key] = currentHeader.join(', '); 87 | } else { 88 | prev[key] = ''; 89 | } 90 | 91 | return prev; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/isolation/ResponseFaker.dev.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {Request, RespondOptions} from 'puppeteer'; 3 | import {ResponseFaker} from '../../src/ResponseFaker'; 4 | import {getRequestDouble} from '../common/testDoubleFactories'; 5 | 6 | describe('class: ResponseFaker', (): void => { 7 | describe('happy path', (): void => { 8 | 9 | it('accepts single string pattern', (): void => { 10 | assert.doesNotThrow((): void => { 11 | // noinspection TsLint 12 | new ResponseFaker('some-pattern/**/*', {}); 13 | }); 14 | }); 15 | 16 | it('accepts multiple string patterns as array', (): void => { 17 | assert.doesNotThrow(() => { 18 | // noinspection TsLint 19 | new ResponseFaker(['some-pattern/**/*', 'some-pattern/**/*', 'some-pattern/**/*'], {}); 20 | }); 21 | }); 22 | 23 | it('returns accepted fake', async (): Promise => { 24 | let responseFaker: ResponseFaker = new ResponseFaker('some-pattern/**/*', { 25 | status: 200, 26 | contentType: 'text/plain', 27 | body: 'payload' 28 | }); 29 | 30 | assert.deepStrictEqual(await responseFaker.getResponseFake(( getRequestDouble())), { 31 | status: 200, 32 | contentType: 'text/plain', 33 | body: 'payload' 34 | }); 35 | }); 36 | 37 | 38 | it('returns accepted fake', async (): Promise => { 39 | let responseFaker: ResponseFaker = new ResponseFaker('some-pattern/**/*', (): RespondOptions => ({ 40 | status: 200, 41 | contentType: 'text/plain', 42 | body: 'payload' 43 | })); 44 | 45 | assert.deepStrictEqual(await responseFaker.getResponseFake(( getRequestDouble())), { 46 | status: 200, 47 | contentType: 'text/plain', 48 | body: 'payload' 49 | }); 50 | }); 51 | 52 | it('returns accepted patterns', (): void => { 53 | let responseFaker: ResponseFaker = new ResponseFaker('some-pattern/**/*', { 54 | status: 200, 55 | contentType: 'text/plain', 56 | body: 'payload' 57 | }); 58 | 59 | assert.deepStrictEqual(responseFaker.getPatterns(), ['some-pattern/**/*']); 60 | }); 61 | 62 | it('confirms request matches when matcher function matches', (): void => { 63 | let responseFaker: ResponseFaker = new ResponseFaker('some-pattern/**/*', { 64 | status: 200, 65 | contentType: 'text/plain', 66 | body: 'payload' 67 | }); 68 | 69 | assert.deepStrictEqual(responseFaker.isMatchingRequest(({url: (): string => ''}), () => true), true); 70 | }); 71 | 72 | it('confirms request does not matches when matcher function does not match', (): void => { 73 | let responseFaker: ResponseFaker = new ResponseFaker('some-pattern/**/*', { 74 | status: 200, 75 | contentType: 'text/plain', 76 | body: 'payload' 77 | }); 78 | 79 | assert.deepStrictEqual(responseFaker.isMatchingRequest(({url: (): string => ''}), () => false), false); 80 | }); 81 | }); 82 | 83 | describe('sad path', (): void => { 84 | it('rejects other input', (): void => { 85 | assert.throws((): void => { 86 | // @ts-ignore: ignore error to test invalid input from js 87 | new ResponseFaker(3, {}).getPatterns(); 88 | }); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/isolation/RequestModifier.dev.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {Request} from 'puppeteer'; 3 | import {RequestModifier} from '../../src/RequestModifier'; 4 | import {getRequestDouble} from '../common/testDoubleFactories'; 5 | 6 | describe('class: RequestModifier', (): void => { 7 | describe('happy path', (): void => { 8 | 9 | it('accepts single string pattern', (): void => { 10 | assert.doesNotThrow((): void => { 11 | // noinspection TsLint 12 | new RequestModifier('some-pattern/**/*', {}); 13 | }); 14 | }); 15 | 16 | it('accepts multiple string patterns as array', (): void => { 17 | assert.doesNotThrow(() => { 18 | // noinspection TsLint 19 | new RequestModifier(['some-pattern/**/*', 'some-pattern/**/*', 'some-pattern/**/*'], {}); 20 | }); 21 | }); 22 | 23 | it('returns accepted fake', async (): Promise => { 24 | let requestModifier: RequestModifier = new RequestModifier('some-pattern/**/*', { 25 | url: '', 26 | method: 'GET', 27 | postData: '', 28 | headers: {} 29 | }); 30 | 31 | assert.deepStrictEqual(await requestModifier.getOverride( getRequestDouble()), { 32 | url: '', 33 | method: 'GET', 34 | postData: '', 35 | headers: {} 36 | }); 37 | }); 38 | 39 | 40 | it('returns accepted fake', async (): Promise => { 41 | let requestModifier: RequestModifier = new RequestModifier('some-pattern/**/*', (request: Request) => ({ 42 | url: request.url(), 43 | method: 'GET', 44 | postData: '', 45 | headers: {} 46 | })); 47 | 48 | assert.deepStrictEqual(await requestModifier.getOverride( getRequestDouble()), { 49 | url: 'any-url', 50 | method: 'GET', 51 | postData: '', 52 | headers: {} 53 | }); 54 | }); 55 | 56 | it('returns accepted patterns', (): void => { 57 | let requestModifier: RequestModifier = new RequestModifier('some-pattern/**/*', { 58 | url: '', 59 | method: 'GET', 60 | postData: '', 61 | headers: {} 62 | }); 63 | 64 | assert.deepStrictEqual(requestModifier.getPatterns(), ['some-pattern/**/*']); 65 | }); 66 | 67 | it('confirms request matches when matcher function matches', (): void => { 68 | let requestModifier: RequestModifier = new RequestModifier('some-pattern/**/*', { 69 | url: '', 70 | method: 'GET', 71 | postData: '', 72 | headers: {} 73 | }); 74 | 75 | assert.deepStrictEqual(requestModifier.isMatchingRequest(({url: (): string => ''}), () => true), true); 76 | }); 77 | 78 | it('confirms request does not matches when matcher function does not match', (): void => { 79 | let requestModifier: RequestModifier = new RequestModifier('some-pattern/**/*', { 80 | url: '', 81 | method: 'GET', 82 | postData: '', 83 | headers: {} 84 | }); 85 | 86 | assert.deepStrictEqual(requestModifier.isMatchingRequest(({url: (): string => ''}), () => false), false); 87 | }); 88 | }); 89 | 90 | describe('sad path', (): void => { 91 | it('rejects other input', (): void => { 92 | assert.throws((): void => { 93 | // @ts-ignore: ignore error to test invalid input from js 94 | new RequestModifier(3, {}).getPatterns(); 95 | }); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/isolation/RequestSpy.dev.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {Request} from 'puppeteer'; 3 | import {RequestSpy} from '../../src/RequestSpy'; 4 | 5 | describe('class: RequestSpy', (): void => { 6 | describe('happy path', (): void => { 7 | it('accepts single string pattern', (): void => { 8 | assert.doesNotThrow((): void => { 9 | new RequestSpy('some-pattern/**/*').hasMatch(); 10 | }); 11 | }); 12 | 13 | it('accepts multiple string patterns in an array', (): void => { 14 | assert.doesNotThrow(() => { 15 | new RequestSpy(['some-pattern/**/*', 'some-pattern/**/*', 'some-pattern/**/*']).hasMatch(); 16 | }); 17 | }); 18 | 19 | it('returns accepted pattern', (): void => { 20 | let requestSpy: RequestSpy = new RequestSpy('some-pattern/**/*'); 21 | assert.deepStrictEqual(requestSpy.getPatterns(), ['some-pattern/**/*']); 22 | }); 23 | 24 | it('multiple matched requests increases matchCount', (): void => { 25 | let requestSpy: RequestSpy = new RequestSpy('some-pattern/**/*'); 26 | 27 | requestSpy.addMatch( {url: (): string => 'some-pattern/pattern'}); 28 | requestSpy.addMatch( {url: (): string => 'some-pattern/pattern_2'}); 29 | 30 | assert.strictEqual(requestSpy.getMatchCount(), 2, ''); 31 | }); 32 | 33 | it('multiple matched requests are stored in matchedUrls', (): void => { 34 | let requestSpy: RequestSpy = new RequestSpy('some-pattern/**/*'); 35 | 36 | requestSpy.addMatch( {url: (): string => 'some-pattern/pattern'}); 37 | requestSpy.addMatch( {url: (): string => 'some-pattern/pattern_2'}); 38 | 39 | let matches: Array = requestSpy.getMatchedRequests(); 40 | 41 | let expected: Array<{url: string}> = [ 42 | {url: 'some-pattern/pattern'}, 43 | {url: 'some-pattern/pattern_2'} 44 | ]; 45 | 46 | let actual: Array<{url: string}> = []; 47 | 48 | for (let match of matches) { 49 | actual.push({url: match.url()}); 50 | } 51 | 52 | assert.deepStrictEqual( 53 | actual, 54 | expected, 55 | 'requestSpy didn\'t add all urls' 56 | ); 57 | 58 | assert.deepStrictEqual(requestSpy.getMatchedUrls(), ['some-pattern/pattern', 'some-pattern/pattern_2'], ''); 59 | }); 60 | 61 | it('multiple matched requests sets matched to true', (): void => { 62 | let requestSpy: RequestSpy = new RequestSpy('some-pattern/**/*'); 63 | 64 | requestSpy.addMatch( {url: (): string => 'some-pattern/pattern'}); 65 | requestSpy.addMatch( {url: (): string => 'some-pattern/pattern_2'}); 66 | 67 | assert.strictEqual(requestSpy.hasMatch(), true, ''); 68 | }); 69 | 70 | it('confirms request matches when matcher function matches', (): void => { 71 | let requestSpy: RequestSpy = new RequestSpy('some-pattern/**/*'); 72 | 73 | assert.deepStrictEqual(requestSpy.isMatchingRequest(({url: (): string => ''}), () => true), true); 74 | }); 75 | 76 | it('confirms request does not matches when matcher function does not match', (): void => { 77 | let requestSpy: RequestSpy = new RequestSpy('some-pattern/**/*'); 78 | 79 | assert.deepStrictEqual(requestSpy.isMatchingRequest(({url: (): string => ''}), () => false), false); 80 | }); 81 | }); 82 | 83 | describe('sad path', (): void => { 84 | it('rejects other input', (): void => { 85 | assert.throws((): void => { 86 | // @ts-ignore: ignore error to test invalid input from js 87 | new RequestSpy(3).hasMatch(); 88 | }); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/isolation/RequestRedirector.dev.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {Request} from 'puppeteer'; 3 | import {HttpRequestFactory} from '../../src/common/HttpRequestFactory'; 4 | import {RequestRedirector} from '../../src/RequestRedirector'; 5 | import {getHttpRequestFactoryDouble, getRequestDouble} from '../common/testDoubleFactories'; 6 | 7 | describe('class: RequestRedirector', (): void => { 8 | describe('happy path', (): void => { 9 | 10 | it('accepts single string pattern', (): void => { 11 | assert.doesNotThrow((): void => { 12 | // noinspection TsLint 13 | new RequestRedirector( 'some-pattern/**/*', (): string => { 14 | return ''; 15 | }); 16 | }); 17 | }); 18 | 19 | it('accepts multiple string patterns as array', (): void => { 20 | assert.doesNotThrow(() => { 21 | // noinspection TsLint 22 | new RequestRedirector(['some-pattern/**/*', 'some-pattern/**/*', 'some-pattern/**/*'], ''); 23 | }); 24 | }); 25 | 26 | it('returns accepted fake', async (): Promise => { 27 | let requestRedirector: RequestRedirector = new RequestRedirector( 28 | 'some-pattern/**/*', 29 | (): string => 'some-url' 30 | ); 31 | 32 | let request: Request = getRequestDouble(); 33 | 34 | assert.deepStrictEqual(await requestRedirector.getOverride(request), { 35 | url: 'some-url', 36 | method: request.method(), 37 | headers: request.headers(), 38 | postData: request.postData() 39 | }); 40 | }); 41 | 42 | 43 | it('returns accepted fake', async (): Promise => { 44 | let requestRedirector: RequestRedirector = new RequestRedirector( 45 | 'some-pattern/**/*', 46 | 'some-url' 47 | ); 48 | 49 | let request: Request = getRequestDouble(); 50 | 51 | assert.deepStrictEqual(await requestRedirector.getOverride(request), { 52 | url: 'some-url', 53 | method: request.method(), 54 | headers: request.headers(), 55 | postData: request.postData() 56 | }); 57 | }); 58 | 59 | it('returns accepted patterns', (): void => { 60 | let requestRedirector: RequestRedirector = new RequestRedirector( 61 | 'some-pattern/**/*', 62 | (): string => 'some-url' 63 | ); 64 | 65 | assert.deepStrictEqual(requestRedirector.getPatterns(), ['some-pattern/**/*']); 66 | }); 67 | 68 | it('confirms request matches when matcher function matches', (): void => { 69 | let requestRedirector: RequestRedirector = new RequestRedirector( 70 | 'some-pattern/**/*', 71 | (): string => 'some-url' 72 | ); 73 | 74 | assert.deepStrictEqual(requestRedirector.isMatchingRequest(({url: (): string => ''}), () => true), true); 75 | }); 76 | 77 | it('confirms request does not matches when matcher function does not match', (): void => { 78 | let requestRedirector: RequestRedirector = new RequestRedirector( 79 | 'some-pattern/**/*', 80 | (): string => 'some-url' 81 | ); 82 | 83 | assert.deepStrictEqual(requestRedirector.isMatchingRequest(({url: (): string => ''}), () => false), false); 84 | }); 85 | }); 86 | 87 | describe('sad path', (): void => { 88 | it('rejects other input', (): void => { 89 | let httpRequestFactory: HttpRequestFactory = getHttpRequestFactoryDouble('any-response'); 90 | assert.throws((): void => { 91 | // @ts-ignore: ignore error to test invalid input from js 92 | new RequestRedirector(httpRequestFactory, 3, {}).getPatterns(); 93 | }); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /test/unit/unitTestHelpers.ts: -------------------------------------------------------------------------------- 1 | import {Overrides, Request, RespondOptions} from 'puppeteer'; 2 | import {RequestInterceptor, RequestModifier, RequestRedirector, ResponseModifier} from '../../src'; 3 | import {HttpRequestFactory} from '../../src/common/HttpRequestFactory'; 4 | import {RequestSpy} from '../../src/RequestSpy'; 5 | import {ResponseFaker} from '../../src/ResponseFaker'; 6 | import { 7 | getRequestDouble, 8 | respondingNock 9 | } from '../common/testDoubleFactories'; 10 | 11 | export module unitTestHelpers { 12 | 13 | export type RequestHandlers = { 14 | requestSpy: RequestSpy, 15 | responseFaker: ResponseFaker, 16 | requestModifier: RequestModifier, 17 | responseModifier: ResponseModifier, 18 | requestRedirector: RequestRedirector 19 | }; 20 | 21 | 22 | export function getRequestDoubles(): Array { 23 | let requestMatchingSpy: Request = getRequestDouble('spy'); 24 | let requestMatchingFaker: Request = getRequestDouble('faker'); 25 | let requestMatchingBlocker: Request = getRequestDouble('blocker'); 26 | let requestMatchingRequestModifier: Request = getRequestDouble('modifier'); 27 | // noinspection TsLint 28 | let requestMatchingRequestRedirector: Request = getRequestDouble('http://www.some-domain.com/requestRedirector', { 29 | nock: respondingNock.bind(null, 'requestRedirector', 'redirected'), 30 | requestCount: 3 31 | }); 32 | let requestMatchingResponseModifier: Request = getRequestDouble('http://www.example.com/responseModifier', { 33 | nock: respondingNock.bind(null, 'responseModifier', 'original'), 34 | requestCount: 3 35 | }); 36 | 37 | return [ 38 | requestMatchingSpy, 39 | requestMatchingFaker, 40 | requestMatchingBlocker, 41 | requestMatchingRequestModifier, 42 | requestMatchingResponseModifier, 43 | requestMatchingRequestRedirector 44 | ]; 45 | } 46 | 47 | 48 | export function createRequestHandlers( 49 | responseFake: RespondOptions, 50 | overrides: Overrides 51 | ): RequestHandlers { 52 | let requestSpy: RequestSpy = new RequestSpy('spy'); 53 | let responseFaker: ResponseFaker = new ResponseFaker('faker', responseFake); 54 | let requestModifier: RequestModifier = new RequestModifier('modifier', overrides); 55 | let responseModifier: ResponseModifier = new ResponseModifier( 56 | 'responseModifier', 57 | (err: Error | undefined, response: string): string => err ? err.toString() : response.replace(' body', ''), 58 | new HttpRequestFactory() 59 | ); 60 | 61 | let requestRedirector: RequestRedirector = new RequestRedirector( 62 | 'requestRedirector', 63 | (request: Request): string => { 64 | return request.url().replace('some-domain', 'example'); 65 | } 66 | ); 67 | 68 | return { 69 | requestSpy, 70 | responseFaker, 71 | requestModifier, 72 | responseModifier, 73 | requestRedirector 74 | }; 75 | } 76 | 77 | export function addRequestHandlers(requestInterceptor: RequestInterceptor, requestHandlers: RequestHandlers): void { 78 | requestInterceptor.block('blocker'); 79 | requestInterceptor.addSpy(requestHandlers.requestSpy); 80 | requestInterceptor.addFaker(requestHandlers.responseFaker); 81 | requestInterceptor.addRequestModifier(requestHandlers.requestModifier); 82 | requestInterceptor.addFaker(requestHandlers.responseModifier); 83 | requestInterceptor.addRequestModifier(requestHandlers.requestRedirector); 84 | } 85 | 86 | export async function simulateUsage(requestInterceptor: RequestInterceptor, requestDoubles: Array): Promise { 87 | for (let requestDouble of requestDoubles) { 88 | await requestInterceptor.intercept(requestDouble); 89 | await requestInterceptor.intercept(requestDouble); 90 | await requestInterceptor.intercept(requestDouble); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/common/testDoubleFactories.ts: -------------------------------------------------------------------------------- 1 | import * as nock from 'nock'; 2 | import {Overrides, Request, RespondOptions} from 'puppeteer'; 3 | import * as sinon from 'sinon'; 4 | import {SinonSpy} from 'sinon'; 5 | import {RequestModifier} from '../../src'; 6 | import {HttpRequestFactory} from '../../src/common/HttpRequestFactory'; 7 | import {ILogger} from '../../src/common/Logger'; 8 | import {RequestSpy} from '../../src/RequestSpy'; 9 | import {ResponseFaker} from '../../src/ResponseFaker'; 10 | import {serverSettings} from './ServerSettings'; 11 | import {TestDouble} from './TestDouble'; 12 | 13 | export function getRequestSpyDouble(matches: boolean): TestDouble { 14 | return { 15 | isMatchingRequest: sinon.stub().returns(matches), 16 | addMatch: sinon.spy(), 17 | hasMatch: undefined, 18 | getMatchedUrls: undefined, 19 | getMatchCount: undefined 20 | }; 21 | } 22 | 23 | export function getRequestModifierDouble(matches: boolean, override: Overrides): TestDouble { 24 | return { 25 | isMatchingRequest: sinon.stub().returns(matches), 26 | getOverride: sinon.stub().returns(override) 27 | }; 28 | } 29 | 30 | export function getRequestDouble(url: string = 'any-url', requestMock?: { nock(): void, requestCount: number }): TestDouble { 31 | if (typeof requestMock !== 'undefined') { 32 | for (let index: number = 0; index < requestMock.requestCount; index++) { 33 | requestMock.nock(); 34 | } 35 | } 36 | 37 | return { 38 | continue: sinon.spy(), 39 | abort: sinon.spy(), 40 | respond: sinon.spy(), 41 | url: (): string => url, 42 | method: (): string => 'GET', 43 | failure: (): boolean => false, 44 | headers: (): { [index: string]: string } => ({ 45 | 'test-header-single': 'val', 46 | 'test-header-multi': 'val, val2', 47 | 'text-header-empty': '' 48 | }), 49 | postData: (): { [index: string]: string } => ({}) 50 | }; 51 | } 52 | 53 | export function getHttpRequestFactoryDouble(fakeResponse: string, spy?: SinonSpy): TestDouble { 54 | return { 55 | createRequest: (request: Request): RespondOptions => { 56 | if (typeof spy !== 'undefined') { 57 | spy(request); 58 | } 59 | 60 | return { 61 | status: 200, 62 | body: fakeResponse, 63 | contentType: 'text/plain' 64 | }; 65 | } 66 | }; 67 | } 68 | 69 | export function getLowVersionRequestDouble(): TestDouble { 70 | return { 71 | continue: sinon.spy(), 72 | abort: sinon.spy(), 73 | respond: sinon.spy(), 74 | url: 'any-url' 75 | }; 76 | } 77 | 78 | export function getResponseFakerDouble(matches: boolean): TestDouble { 79 | return { 80 | getResponseFake: sinon.spy(), 81 | isMatchingRequest: sinon.stub().returns(matches) 82 | }; 83 | } 84 | 85 | export function getErrorRequestDouble(): TestDouble { 86 | return { 87 | continue: async (): Promise => { 88 | throw new Error('requestInterception is not set'); 89 | }, 90 | abort: async (): Promise => { 91 | throw new Error('requestInterception is not set'); 92 | }, 93 | respond: async (): Promise => { 94 | throw new Error('requestInterception is not set'); 95 | }, 96 | url: (): string => 'any-url' 97 | }; 98 | } 99 | 100 | const FAVICON_URL: string = `http://${serverSettings.host}/favicon.ico`; 101 | 102 | export function getLoggerFake(arrayPointer: Array): ILogger { 103 | return { 104 | log: (log: string): void => { 105 | if (log !== FAVICON_URL) { 106 | arrayPointer.push(log); 107 | } 108 | } 109 | }; 110 | } 111 | 112 | export function respondingNock(path: string, bodyPrefix: string): void { 113 | nock('http://www.example.com') 114 | .get(`/${path}`) 115 | .reply(200, `${bodyPrefix} response body`, {'content-type': 'text/html'}); 116 | } 117 | -------------------------------------------------------------------------------- /test/isolation/ResponseModifier.dev.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {Request} from 'puppeteer'; 3 | import * as sinon from 'sinon'; 4 | import {HttpRequestFactory} from '../../src/common/HttpRequestFactory'; 5 | import {ResponseModifier} from '../../src/ResponseModifier'; 6 | import {getHttpRequestFactoryDouble, getRequestDouble} from '../common/testDoubleFactories'; 7 | 8 | describe('class: ResponseModifier', (): void => { 9 | describe('happy path', (): void => { 10 | 11 | it('accepts single string pattern', (): void => { 12 | assert.doesNotThrow((): void => { 13 | let httpRequestFactory: HttpRequestFactory = getHttpRequestFactoryDouble('any-response'); 14 | // noinspection TsLint 15 | new ResponseModifier( 16 | 'some-pattern/**/*', 17 | (): any => { 18 | return {}; 19 | }, 20 | httpRequestFactory 21 | ); 22 | }); 23 | }); 24 | 25 | it('accepts multiple string patterns as array', (): void => { 26 | assert.doesNotThrow(() => { 27 | // noinspection TsLint 28 | new ResponseModifier( 29 | ['some-pattern/**/*', 'some-pattern/**/*', 'some-pattern/**/*'], 30 | (): any => { 31 | return {}; 32 | } 33 | ); 34 | }); 35 | }); 36 | 37 | it('returns accepted fake', async (): Promise => { 38 | let httpRequestFactory: HttpRequestFactory = getHttpRequestFactoryDouble('payload'); 39 | let responseModifier: ResponseModifier = new ResponseModifier( 40 | 'some-pattern/**/*', 41 | (err: Error | undefined, response: string): string => err ? err.toString() : `${response}1`, 42 | httpRequestFactory 43 | ); 44 | 45 | let request: Request = getRequestDouble(); 46 | 47 | assert.deepStrictEqual(await responseModifier.getResponseFake(request), { 48 | status: 200, 49 | contentType: 'text/plain', 50 | body: 'payload1' 51 | }); 52 | }); 53 | 54 | it('returns accepted patterns', (): void => { 55 | let httpRequestFactory: HttpRequestFactory = getHttpRequestFactoryDouble('any-response'); 56 | let responseModifier: ResponseModifier = new ResponseModifier( 57 | 'some-pattern/**/*', 58 | (): string => 'payload', 59 | httpRequestFactory 60 | ); 61 | 62 | assert.deepStrictEqual(responseModifier.getPatterns(), ['some-pattern/**/*']); 63 | }); 64 | 65 | it('confirms request matches when matcher function matches', (): void => { 66 | let httpRequestFactory: HttpRequestFactory = getHttpRequestFactoryDouble('any-response'); 67 | let responseModifier: ResponseModifier = new ResponseModifier( 68 | 'some-pattern/**/*', 69 | (): string => 'payload', 70 | httpRequestFactory 71 | ); 72 | 73 | assert.deepStrictEqual(responseModifier.isMatchingRequest(({url: (): string => ''}), () => true), true); 74 | }); 75 | 76 | it('confirms request does not matches when matcher function does not match', (): void => { 77 | let httpRequestFactory: HttpRequestFactory = getHttpRequestFactoryDouble('any-response'); 78 | let responseModifier: ResponseModifier = new ResponseModifier( 79 | 'some-pattern/**/*', 80 | (): string => 'payload', 81 | httpRequestFactory 82 | ); 83 | 84 | assert.deepStrictEqual(responseModifier.isMatchingRequest(({url: (): string => ''}), () => false), false); 85 | }); 86 | }); 87 | 88 | describe('sad path', (): void => { 89 | it('rejects other input', (): void => { 90 | let httpRequestFactory: HttpRequestFactory = getHttpRequestFactoryDouble('any-response'); 91 | assert.throws((): void => { 92 | // @ts-ignore: ignore error to test invalid input from js 93 | new ResponseModifier(httpRequestFactory, 3, {}).getPatterns(); 94 | }); 95 | }); 96 | 97 | it('passes error when resource is unavailable', async (): Promise => { 98 | let expectedError: Error = new Error('ERROR!'); 99 | 100 | let httpRequestFactory: HttpRequestFactory = ({ 101 | createRequest: () => Promise.reject(expectedError) 102 | }); 103 | 104 | let modifierCallbackSpy: sinon.SinonSpy = sinon.spy(); 105 | 106 | let responseModifier: ResponseModifier = new ResponseModifier( 107 | 'some-pattern/**/*', 108 | (err: Error | undefined, body: string) => { 109 | modifierCallbackSpy(err, body); 110 | 111 | return err ? 'oh no!' : body; 112 | }, 113 | httpRequestFactory 114 | ); 115 | 116 | 117 | let request: Request = getRequestDouble(); 118 | 119 | assert.deepStrictEqual(await responseModifier.getResponseFake(request), { 120 | body: 'oh no!' 121 | }); 122 | sinon.assert.callCount(modifierCallbackSpy, 1); 123 | sinon.assert.calledWithExactly(modifierCallbackSpy, expectedError, ''); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 100 | 102 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 111 | -------------------------------------------------------------------------------- /test/isolation/HttpRequestFactory.dev.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as nock from 'nock'; 3 | import {HttpHeaders} from 'nock'; 4 | import {Request, RespondOptions} from 'puppeteer'; 5 | import {HttpRequestFactory} from '../../src/common/HttpRequestFactory'; 6 | import {assertThrowsAsync} from '../common/AssertionHelpers'; 7 | import {getRequestDouble} from '../common/testDoubleFactories'; 8 | 9 | describe('class: HttpRequestFactory', () => { 10 | 11 | before(() => { 12 | if (!nock.isActive()) { 13 | nock.activate(); 14 | } 15 | nock.disableNetConnect(); 16 | }); 17 | 18 | after(() => { 19 | nock.cleanAll(); 20 | nock.enableNetConnect(); 21 | nock.restore(); 22 | }); 23 | 24 | describe('happy path', () => { 25 | it('should create a promise based loader from an url string', async () => { 26 | // noinspection TsLint 27 | nock('http://www.example.com') 28 | .get('/resource') 29 | .reply(200, 'path matched', {'content-type': 'text/plain'}); 30 | 31 | let httpRequestFactory: HttpRequestFactory = new HttpRequestFactory(); 32 | let response: RespondOptions = await httpRequestFactory.createRequest( 33 | getRequestDouble('http://www.example.com/resource') 34 | ); 35 | 36 | assert.deepStrictEqual(response, { 37 | body: 'path matched', 38 | contentType: 'text/plain', 39 | headers: { 40 | 'content-type': 'text/plain' 41 | }, 42 | status: 200 43 | }); 44 | }); 45 | 46 | it('should create a promise based loader from a https url string', async () => { 47 | // noinspection TsLint 48 | nock('https://www.example.com') 49 | .get('/resource') 50 | .reply(200, 'path matched', {'content-type': 'text/plain'}); 51 | 52 | let httpRequestFactory: HttpRequestFactory = new HttpRequestFactory(); 53 | let response: RespondOptions = await httpRequestFactory.createRequest( 54 | getRequestDouble('https://www.example.com/resource') 55 | ); 56 | 57 | assert.deepStrictEqual(response, { 58 | body: 'path matched', 59 | contentType: 'text/plain', 60 | headers: { 61 | 'content-type': 'text/plain' 62 | }, 63 | status: 200 64 | }); 65 | }); 66 | 67 | 68 | it('should create a promise based loader from an url string with get params', async () => { 69 | // noinspection TsLint 70 | nock('https://www.example.com') 71 | .get('/resource?some=1&get=params') 72 | .reply(200, 'path matched', {'content-type': 'text/plain'}); 73 | 74 | let httpRequestFactory: HttpRequestFactory = new HttpRequestFactory(); 75 | let response: RespondOptions = await httpRequestFactory.createRequest( 76 | getRequestDouble('https://www.example.com/resource?some=1&get=params') 77 | ); 78 | 79 | assert.deepStrictEqual(response, { 80 | body: 'path matched', 81 | contentType: 'text/plain', 82 | headers: { 83 | 'content-type': 'text/plain' 84 | }, 85 | status: 200 86 | }); 87 | }); 88 | 89 | 90 | it('should create correct headers from response', async () => { 91 | let headers: HttpHeaders = { 92 | 'content-type': 'text/plain', 93 | 'test-header-single': 'val', 94 | 'test-header-multi': ['val', 'val2'], 95 | 'text-header-empty': undefined 96 | }; 97 | 98 | nock('http://www.example.com') 99 | .get('/resource') 100 | .reply( 101 | 200, 102 | () => 'path matched', 103 | headers 104 | ); 105 | 106 | let httpRequestFactory: HttpRequestFactory = new HttpRequestFactory(); 107 | let response: RespondOptions = await httpRequestFactory.createRequest( 108 | getRequestDouble('http://www.example.com/resource') 109 | ); 110 | 111 | assert.deepStrictEqual(response, { 112 | body: 'path matched', 113 | contentType: 'text/plain', 114 | headers: { 115 | 'content-type': 'text/plain', 116 | 'test-header-single': 'val', 117 | 'test-header-multi': 'val, val2', 118 | 'text-header-empty': '' 119 | }, 120 | status: 200 121 | }); 122 | }); 123 | 124 | 125 | it('should create a promise based loader from a request', async () => { 126 | // noinspection TsLint 127 | nock('http://www.example.com') 128 | .get('/resource') 129 | .reply(200, 'path matched', {'content-type': 'text/plain'}); 130 | 131 | let httpRequestFactory: HttpRequestFactory = new HttpRequestFactory(); 132 | let response: RespondOptions = await httpRequestFactory.createRequest( 133 | { 134 | url: (): string => 'http://www.example.com/resource', method: (): string => 'GET', 135 | headers: (): { [index: string]: string } => ({}) 136 | } 137 | ); 138 | 139 | assert.deepStrictEqual(response, { 140 | body: 'path matched', 141 | contentType: 'text/plain', 142 | headers: { 143 | 'content-type': 'text/plain' 144 | }, 145 | status: 200 146 | }); 147 | }); 148 | }); 149 | describe('sad path', () => { 150 | it('should throw if timeout is reached', async () => { 151 | // noinspection TsLint 152 | nock('http://www.example.com') 153 | .get('/resource') 154 | .delay(7) 155 | .reply(200, 'path matched'); 156 | 157 | let httpRequestFactory: HttpRequestFactory = new HttpRequestFactory(5); 158 | await assertThrowsAsync( 159 | async () => { 160 | await httpRequestFactory.createRequest( 161 | getRequestDouble('http://www.example.com/resource') 162 | ); 163 | }, 164 | /Error/ 165 | ); 166 | }); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /src/RequestInterceptor.ts: -------------------------------------------------------------------------------- 1 | import {Overrides, Request, RespondOptions} from 'puppeteer'; 2 | import {instanceOfRequestBlocker} from './common/interfaceValidators/instanceOfRequestBlocker'; 3 | import {instanceOfRequestModifier} from './common/interfaceValidators/instanceOfRequestModifier'; 4 | import {instanceOfRequestSpy} from './common/interfaceValidators/instanceOfRequestSpy'; 5 | import {instanceOfResponseFaker} from './common/interfaceValidators/instanceOfResponseFaker'; 6 | import {ILogger} from './common/Logger'; 7 | import {resolveOptionalPromise} from './common/resolveOptionalPromise'; 8 | import {UrlAccessor} from './common/urlAccessor/UrlAccessor'; 9 | import {UrlAccessorResolver} from './common/urlAccessor/UrlAccessorResolver'; 10 | import {VoidLogger} from './common/VoidLogger'; 11 | import {IRequestBlocker} from './interface/IRequestBlocker'; 12 | import {IRequestModifier} from './interface/IRequestModifier'; 13 | import {IRequestSpy} from './interface/IRequestSpy'; 14 | import {IResponseFaker} from './interface/IResponseFaker'; 15 | import {RequestBlocker} from './RequestBlocker'; 16 | import {RequestMatcher} from './types/RequestMatcher'; 17 | 18 | export class RequestInterceptor { 19 | 20 | private requestSpies: Array = []; 21 | private responseFakers: Array = []; 22 | private requestModifiers: Array = []; 23 | private matcher: RequestMatcher; 24 | private logger: ILogger; 25 | private requestBlocker: IRequestBlocker; 26 | 27 | public constructor(matcher: RequestMatcher, logger?: ILogger) { 28 | if (typeof logger === 'undefined') { 29 | logger = new VoidLogger(); 30 | } 31 | 32 | this.logger = logger; 33 | this.matcher = matcher; 34 | this.requestBlocker = new RequestBlocker(); 35 | } 36 | 37 | public async intercept(interceptedRequest: Request): Promise { 38 | await this.matchSpies(interceptedRequest); 39 | 40 | if (await resolveOptionalPromise(this.requestBlocker.shouldBlockRequest(interceptedRequest, this.matcher))) { 41 | await this.blockUrl(interceptedRequest); 42 | 43 | return; 44 | } 45 | 46 | let requestOverride: Overrides | undefined = await this.getMatchingOverride(interceptedRequest); 47 | 48 | if (typeof requestOverride !== 'undefined') { 49 | let urlAccessor: UrlAccessor = UrlAccessorResolver.getUrlAccessor(interceptedRequest); 50 | await interceptedRequest.continue(requestOverride); 51 | this.logger.log(`modified: ${urlAccessor.getUrlFromRequest(interceptedRequest)}`); 52 | 53 | return; 54 | } 55 | 56 | let responseFaker: undefined | IResponseFaker = await this.getMatchingFaker(interceptedRequest); 57 | if (typeof responseFaker !== 'undefined') { 58 | let responseFake: RespondOptions | Promise = responseFaker.getResponseFake(interceptedRequest); 59 | await interceptedRequest.respond(await resolveOptionalPromise(responseFake)); 60 | this.logger.log(`faked: ${interceptedRequest.url()}`); 61 | 62 | return; 63 | } 64 | 65 | await this.acceptUrl(interceptedRequest); 66 | } 67 | 68 | public addSpy(requestSpy: IRequestSpy): void { 69 | if (!instanceOfRequestSpy(requestSpy)) { 70 | throw new Error('invalid RequestSpy provided. Please make sure to match the interface provided.'); 71 | } 72 | 73 | this.requestSpies.push(requestSpy); 74 | } 75 | 76 | public addFaker(responseFaker: IResponseFaker): void { 77 | if (!instanceOfResponseFaker(responseFaker)) { 78 | throw new Error('invalid ResponseFaker provided. Please make sure to match the interface provided.'); 79 | } 80 | 81 | this.responseFakers.push(responseFaker); 82 | } 83 | 84 | public addRequestModifier(requestModifier: IRequestModifier): void { 85 | if (!instanceOfRequestModifier(requestModifier)) { 86 | throw new Error('invalid RequestModifier provided. Please make sure to match the interface provided.'); 87 | } 88 | 89 | this.requestModifiers.push(requestModifier); 90 | } 91 | 92 | public block(urlsToBlock: Array | string): void { 93 | this.requestBlocker.addUrlsToBlock(urlsToBlock); 94 | } 95 | 96 | public clearSpies(): void { 97 | this.requestSpies = []; 98 | } 99 | 100 | public clearFakers(): void { 101 | this.responseFakers = []; 102 | } 103 | 104 | public clearRequestModifiers(): void { 105 | this.requestModifiers = []; 106 | } 107 | 108 | public clearUrlsToBlock(): void { 109 | this.requestBlocker.clearUrlsToBlock(); 110 | } 111 | 112 | public setUrlsToBlock(urlsToBlock: Array): void { 113 | this.requestBlocker.clearUrlsToBlock(); 114 | this.requestBlocker.addUrlsToBlock(urlsToBlock); 115 | } 116 | 117 | public setRequestBlocker(requestBlocker: IRequestBlocker): void { 118 | if (!instanceOfRequestBlocker(requestBlocker)) { 119 | throw new Error('invalid RequestBlocker provided. Please make sure to match the interface provided.'); 120 | } 121 | 122 | this.requestBlocker = requestBlocker; 123 | } 124 | 125 | private async getMatchingFaker(interceptedRequest: Request): Promise { 126 | for (let faker of this.responseFakers) { 127 | if (await resolveOptionalPromise(faker.isMatchingRequest(interceptedRequest, this.matcher))) { 128 | return faker; 129 | } 130 | } 131 | 132 | return undefined; 133 | } 134 | 135 | private async matchSpies(interceptedRequest: Request): Promise { 136 | for (let spy of this.requestSpies) { 137 | if (await resolveOptionalPromise(spy.isMatchingRequest(interceptedRequest, this.matcher))) { 138 | await resolveOptionalPromise(spy.addMatch(interceptedRequest)); 139 | } 140 | } 141 | } 142 | 143 | private async getMatchingOverride(interceptedRequest: Request): Promise { 144 | let requestOverride: Overrides | undefined; 145 | 146 | for (let requestModifier of this.requestModifiers) { 147 | if (await resolveOptionalPromise(requestModifier.isMatchingRequest(interceptedRequest, this.matcher))) { 148 | requestOverride = await resolveOptionalPromise(requestModifier.getOverride(interceptedRequest)); 149 | } 150 | } 151 | 152 | return requestOverride; 153 | } 154 | 155 | private async blockUrl(interceptedRequest: Request): Promise { 156 | let urlAccessor: UrlAccessor = UrlAccessorResolver.getUrlAccessor(interceptedRequest); 157 | 158 | try { 159 | await interceptedRequest.abort(); 160 | this.logger.log(`aborted: ${urlAccessor.getUrlFromRequest(interceptedRequest)}`); 161 | } catch (error) { 162 | this.logger.log((error).toString()); 163 | } 164 | } 165 | 166 | private async acceptUrl(interceptedRequest: Request): Promise { 167 | let urlAccessor: UrlAccessor = UrlAccessorResolver.getUrlAccessor(interceptedRequest); 168 | try { 169 | await interceptedRequest.continue(); 170 | this.logger.log(`loaded: ${urlAccessor.getUrlFromRequest(interceptedRequest)}`); 171 | } catch (error) { 172 | this.logger.log((error).toString()); 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /documentation/API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ## Table of Content 4 | 5 | - [Class: RequestInterceptor](#class-requestinterceptor) 6 | - [Class: RequestSpy](#class-requestspy-implements-irequestspy) 7 | - [Class: ResponseFaker](#class-responsefaker-implements-iresponsefaker) 8 | - [Class: ResponseModifier](#class-responsemodifier-implements-iresponsefaker) 9 | - [Class: RequestModifier](#class-requestmodifier-implements-irequestmodifier) 10 | - [Class: RequestRedirector](#class-requestredirector-implements-irequestmodifier) 11 | - [Class: RequestBlocker](#class-requestblocker-implements-iresponseblocker) 12 | 13 | ## class: RequestInterceptor 14 | The `RequestInterceptor` will call all spies, fakers and blocker to dertermine if an intercepted request matches. against the `matcher` function and notify all spies with a matching pattern and block requests matching any pattern in `urlsToBlock`. 15 | 16 | ### RequestInterceptor constructor(matcher, logger?) 17 | - `matcher`: \<(url: string, pattern: string) =\> boolean\>\> 18 | - `logger?`: \<{log: (text: string) =\> void}\> 19 | 20 | The `matcher` will be called for every url, testing the url against patterns of any `RequestSpy` provided and also any url added to `urlsToBlock`. 21 | 22 | The `logger` if provided will output any requested url with a 'loaded' or 'aborted' prefix and any exception caused by puppeteer's abort and continue functions. 23 | ### RequestInterceptor.intercept(interceptedRequest) 24 | - interceptedRequest: interceptedRequest provided by puppeteer's 'request' event 25 | 26 | Function to be registered with puppeteer's request event. 27 | 28 | ### RequestInterceptor.addSpy(requestSpy) 29 | - `requestSpy`: \ spy to register 30 | 31 | Register a `RequestSpy` with the `RequestInterceptor`. 32 | 33 | ### RequestInterceptor.clearSpies() 34 | Clears all registered spies. 35 | 36 | ### RequestInterceptor.addFaker(responseFaker) 37 | - `responseFaker`: \ faker to register 38 | 39 | Register a `ResonseFaker` with the `RequestInterceptor`. 40 | 41 | ### RequestInterceptor.clearFakers() 42 | Clears all registered fakers. 43 | 44 | ### RequestInterceptor.addRequestModifier(requestModifier) 45 | - `responseModifier`: \ modifier to register 46 | 47 | Register a `RequestModifier` with the `RequestInterceptor`. 48 | 49 | ### RequestInterceptor.clearRequestModifiers() 50 | Clears all registered modifiers. 51 | 52 | ### RequestInterceptor.block(urlsToBlock) 53 | - `urlsToBlock`: \ | \\> urls to be blocked if matched 54 | 55 | `block` will always add urls to the list of urls to block. Passed arrays will be merged with existing urls to block. 56 | 57 | ### RequestInterceptor.setUrlsToBlock(urlsToBlock) 58 | - `urlsToBlock`: > setter for `urlsToBlock` 59 | 60 | Setter to overwrite existing urls to block. 61 | 62 | ### RequestInterceptor.clearUrlsToBlock() 63 | Clears all registered patterns of urls to block. 64 | 65 | ### RequestInterceptor.setRequestBlocker(requestBlocker) 66 | - `requestBlocker` \ 67 | 68 | Allows you to replace the default RequestBlocker by your own implementation. 69 | 70 | ----- 71 | 72 | ## class: RequestSpy implements IRequestSpy 73 | `RequestSpy` is used to count and verify intercepted requests matching a specific pattern. 74 | 75 | ### RequestSpy constructor(pattern) 76 | - `pattern`: \> 77 | 78 | `pattern` passed to the `matcher` function of the `RequestInterceptor`. 79 | 80 | ### RequestSpy.hasMatch() 81 | - returns: \ returns whether any url matched the `pattern` 82 | 83 | ### RequestSpy.getMatchedUrls() 84 | - returns: \\> returns a list of urls that matched the `pattern` 85 | 86 | ### RequestSpy.getMatchedRequests() 87 | - returns: \\> returns a list of requests that matched the `pattern` 88 | 89 | ### RequestSpy.getMatchCount() 90 | - returns: \ number of urls that matched the `pattern` 91 | 92 | ### RequestSpy.isMatchingRequest(request, matcher) 93 | - request \ request object provided by puppeteer 94 | - matcher \<(url: string, pattern: string) =\> boolean\>\> matching function passed to RequestInterceptor's constructor 95 | - returns: \ returns true if any pattern provided to the `RequestSpy` matches the request url 96 | 97 | The `RequestInterceptor` calls this method to determine if an interceptedRequest matches the RequestSpy. 98 | 99 | ### RequestSpy.addMatch(matchedRequest) 100 | - matchedRequest: \ request that was matched 101 | 102 | The `RequestInterceptor` calls this method when an interceptedRequest matches the pattern. 103 | 104 | ----- 105 | 106 | ## class: ResponseFaker implements IResponseFaker 107 | `ResponseFaker` is used to provide a fake response when matched to a specific pattern. 108 | 109 | ### ResponseFaker constructor(pattern, responseFake) 110 | - `pattern`: \> 111 | - `responseFake`: \<\(\(request: Request\) => RespondOptions | Promise\\) | RespondOptions\> for details refer to [puppeteer API](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#requestrespondresponse) 112 | 113 | ### ResponseFaker.getPatterns() 114 | - returns: \\> return the `pattern` list of the faker 115 | 116 | ### ResponseFaker.getResponseFake() 117 | - returns: \\> return the fake response 118 | 119 | The `RequestInterceptor` calls this method when an interceptedUrl matches the pattern. 120 | 121 | ### ResponseFaker.isMatchingRequest(request, matcher) 122 | - request \ request object provided by puppeteer 123 | - matcher \<(url: string, pattern: string) =\> boolean\>\> matching function passed to RequestInterceptor's constructor 124 | - returns: \ returns true if any pattern provided to the `ResponseFaker` matches the request url 125 | 126 | The `RequestInterceptor` calls this method to determine if an interceptedRequest matches. 127 | 128 | ----- 129 | 130 | ## class: ResponseModifier implements IResponseFaker 131 | `ResponseModifier` is used to load the original response and modify it on the fly as a fake response when matched to a specific pattern. 132 | 133 | ### ResponseModifier constructor(pattern, responseModifierCallback) 134 | - `pattern`: \> 135 | - `responseModifierCallback`: \<\(err: Error | undefined, response: string, request: Request\) => string | Promise> 136 | 137 | ### ResponseModifier.getPatterns() 138 | - returns: \\> return the `pattern` list of the faker 139 | 140 | ### ResponseModifier.getResponseFake(request) 141 | - `request`: \ 142 | - returns: \\> return the fake response 143 | 144 | The `RequestInterceptor` calls this method when an interceptedUrl matches the pattern. 145 | 146 | ### ResponseModifier.isMatchingRequest(request, matcher) 147 | - request \ request object provided by puppeteer 148 | - matcher \<(url: string, pattern: string) =\> boolean\>\> matching function passed to RequestInterceptor's constructor 149 | - returns: \ returns true if any pattern provided to the `ResponseModifier` matches the request url 150 | 151 | The `RequestInterceptor` calls this method to determine if an interceptedRequest matches. 152 | 153 | ----- 154 | 155 | ## class: RequestModifier implements IRequestModifier 156 | `RequestModifier` is used to change the request when matched to a specific pattern. 157 | 158 | ### RequestModifier constructor(pattern, responseModifierCallback) 159 | - `pattern`: \\> 160 | - `requestOverride`: \<((request: Request) => Promise | Overrides) | Overrides\> 161 | - `httpRequestFactory?`: \ Factory to create a http request 162 | 163 | ### RequestModifier.getPatterns() 164 | - returns: \\> return the `pattern` list of the modifier 165 | 166 | ### RequestModifier.getOverride(request) 167 | - `request`: \ 168 | - returns: \\> return the request overrides 169 | 170 | The `RequestInterceptor` calls this method when an interceptedUrl matches the pattern. 171 | 172 | ### RequestModifier.isMatchingRequest(request, matcher) 173 | - request \ request object provided by puppeteer 174 | - matcher \<(url: string, pattern: string) =\> boolean\>\> matching function passed to RequestInterceptor's constructor 175 | - returns: \ returns true if any pattern provided to the `RequestModifier` matches the request url 176 | 177 | The `RequestInterceptor` calls this method to determine if an interceptedRequest matches. 178 | 179 | ----- 180 | 181 | ## class: RequestRedirector implements IRequestModifier 182 | `RequestRedirector` is used to change request url when matched to a specific pattern. 183 | 184 | ### RequestRedirector constructor(pattern, redirectionUrl) 185 | - `pattern`: \\> 186 | - `redirectionUrl`: \<((request: Request) => Promise | string) | string\> 187 | 188 | ### RequestRedirector.getPatterns() 189 | - returns: \\> return the `pattern` list of the modifier 190 | 191 | ### RequestRedirector.getOverride(request) 192 | - `request`: \ request object provided by puppeteer 193 | - returns: \\> return the request overrides 194 | 195 | The `RequestInterceptor` calls this method when an interceptedUrl matches the pattern. 196 | 197 | ### RequestRedirector.isMatchingRequest(request, matcher) 198 | - request \ request object provided by puppeteer 199 | - matcher \<(url: string, pattern: string) =\> boolean\>\> matching function passed to RequestInterceptor's constructor 200 | - returns: \ returns true if any pattern provided to the `RequestRedirector` matches the request url 201 | 202 | The `RequestInterceptor` calls this method to determine if an interceptedRequest matches. 203 | 204 | ----- 205 | 206 | ## class: RequestBlocker implements IResponseBlocker 207 | `RequestBlocker` is used to by the RequestInterceptor to match requests to block. 208 | 209 | ### RequestBlocker.shouldBlockRequest(request, matcher) 210 | - request \ request object provided by puppeteer 211 | - matcher \<(url: string, pattern: string) =\> boolean\>\> matching function passed to RequestInterceptor's constructor 212 | 213 | The `RequestInterceptor` calls this method to determine if an interceptedRequest matches. 214 | 215 | ### RequestBlocker.addUrlsToBlock(urls) 216 | - urls \ | string\> 217 | 218 | Adds new urls to the block list. 219 | 220 | ### RequestBlocker.clearUrlsToBlock() 221 | 222 | Removes all entries of the block list. 223 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # puppeteer-request-spy 2 | [![Build Status](https://travis-ci.org/Tabueeee/puppeteer-request-spy.svg?branch=master)](https://travis-ci.org/Tabueeee/puppeteer-request-spy) 3 | [![Coverage Status](https://coveralls.io/repos/github/Tabueeee/puppeteer-request-spy/badge.svg?branch=master)](https://coveralls.io/github/Tabueeee/puppeteer-request-spy?branch=master) 4 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FTabueeee%2Fpuppeteer-request-spy.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FTabueeee%2Fpuppeteer-request-spy?ref=badge_shield) 5 | > With puppeteer-request-spy you can easily watch, fake, modify or block requests from puppeteer matching patterns. 6 | 7 | - allows you to write tests verifying specific resources are loaded as expected 8 | - allows you to exclude unneeded requests from tests, speeding them up significantly 9 | - allows you to alter requests and responses with custom content and http status 10 | - avoids conflicts resulting from already aborted / continued or responded requests 11 | 12 | ## Install 13 | 14 | ```bash 15 | npm install puppeteer-request-spy --save-dev 16 | ``` 17 | 18 | ## Table Of Content 19 | 20 | - [Spying On Requests](#usage) 21 | - [Altering Requests](#altering-requests) 22 | - [Modifying Requests](#modifying-requests) 23 | - [Redirecting Requests](#redirecting-requests) 24 | - [Blocking Requests](#blocking-requests) 25 | - [Altering Responses](#altering-responses) 26 | - [Faking Responses](#faking-responses) 27 | - [Modifying Responses](#modifying-responses) 28 | - [Asynchronous Options](#asynchronous-options) 29 | - [Interception Order](#requestinterceptor-request-interception-order) 30 | - [Advanced Usage](#advanced-usage) 31 | - [Full API](./documentation/API.md) 32 | 33 | ## Usage 34 | 35 | ### Spying On Requests With A KeywordMatcher 36 | First create a new `RequestInterceptor` with a `matcher` function and an optional logger. 37 | ```js 38 | function KeywordMatcher(testee, keyword) { 39 | return testee.indexOf(keyword) > -1; 40 | } 41 | 42 | let requestInterceptor = new RequestInterceptor(KeywordMatcher, console); 43 | ``` 44 | Next create a new `RequestSpy` with a `pattern` to be matched against all requests. 45 | ```js 46 | let imageSpy = new RequestSpy('/pictures'); 47 | ``` 48 | The `RequestSpy` needs to be registered with the `RequestInterceptor`. 49 | ```js 50 | requestInterceptor.addSpy(imageSpy); 51 | ``` 52 | To use the puppeteer's request event the RequestInterception flag on the page object has to be set to true. 53 | ```js 54 | await page.setRequestInterception(true); 55 | ``` 56 | The `RequestInterceptor` must be registered with puppeteer. 57 | ```js 58 | page.on('request', requestInterceptor.intercept.bind(requestInterceptor)); 59 | ``` 60 | After puppeteer's page object finished navigating to any page, you can query the `RequestSpy`. 61 | ```js 62 | await page.goto('https://www.example.com'); 63 | 64 | assert.ok(!imageSpy.getMatchedRequests()[0].failure()); 65 | assert.ok(imageSpy.hasMatch() && imageSpy.getMatchCount() > 0); 66 | ``` 67 | When all responses have been loaded you can also query the response of any matched Request. You can ensure all responses have been loaded by using the networkidle0 option. For further information check the official [puppeteer API](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagegotourl-options). 68 | ```js 69 | for (let match of imagesSpy.getMatchedRequests()) { 70 | assert.ok(match.response().ok()); 71 | } 72 | ``` 73 | Note 74 | > Since unhandled Promise rejections causes the node process to keep running after test failure, the `RequestInterceptor` will catch and log puppeteer's exception, if the `requestInterception` flag is not set. 75 | 76 | 77 | ### Altering Requests 78 | 79 | #### Modifying Requests 80 | Intercepted requests can be modified by passing an overrides object to the RequestModifier. The response overrides have to match the Overrides object as specified in the official [puppeteer API](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#httprequestcontinueoverrides). 81 | 82 | ```js 83 | let requestModifier = new RequestModifier('/ajax/some-post-request', { 84 | url: '/ajax/some-get-request', 85 | method: 'GET', 86 | postData: '', 87 | headers: {} 88 | }); 89 | 90 | requestInterceptor.addRequestModifier(requestModifier); 91 | ``` 92 | 93 | #### Redirecting Requests 94 | If you just want to replace the url of an intercepted request, you can use the RequestRedirector. 95 | 96 | ```js 97 | let requestRedirector = new RequestRedirector('/ajax/some-request', 'some/new/url'); 98 | 99 | requestInterceptor.addRequestModifier(requestRedirector); 100 | ``` 101 | 102 | The RequestRedirector uses the IRequestModifier interface. 103 | 104 | #### Blocking Requests 105 | Optionally you can add `patterns` to block requests. Blocking requests speeds up page load since no data is loaded. Blocking requests takes precedence over overriding requests or faking responses, so any request blocked will not be replaced even when matching a `ResponseFaker`. Blocked or faked requests will still be counted by a `RequestSpy` with a matching pattern. 106 | 107 | ```js 108 | requestInterceptor.block(['scripts', 'track', '.png']); 109 | ``` 110 | 111 | ### Altering Responses 112 | 113 | #### Faking Responses 114 | The response of intercepted requests can be replaced by adding a ResponseFaker to the RequestInterceptor. The fake response has to match the Response object as specified in the official [puppeteer API](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#httprequestrespondresponse). 115 | ```js 116 | let responseFaker = new ResponseFaker('/ajax/some-request', { 117 | status: 200, 118 | contentType: 'application/json', 119 | body: JSON.stringify({successful: false, payload: []}) 120 | }); 121 | 122 | requestInterceptor.addFaker(responseFaker); 123 | ``` 124 | For further details on how to replace different formats of data like images, text or html, please refer to the examples provided in the [github repository](./examples/fake-test.spec.js). 125 | 126 | #### Modifying Responses 127 | It's also possible to replace the content of a response instead of replacing it: 128 | 129 | ```js 130 | let responseModifier = new ResponseModifier('/ajax/some-request', (response, request) => { 131 | return response.replace('', ''); 132 | }); 133 | 134 | requestInterceptor.addFaker(responseModifier); 135 | ``` 136 | 137 | Note: 138 | > The request is currently loaded in the node environment, not the browser environment. 139 | 140 | The ResponseModifier uses the IResponseFaker interface. 141 | 142 | ### Asynchronous Options 143 | 144 | All ResponseFakers and ResponseModifiers now accept a callback for resolving the passed options. This callback can also be async or return a promise. 145 | 146 | ```js 147 | // static options 148 | let requestRedirector = new RequestRedirector( 149 | '/ajax/some-request', 150 | 'some/other/url' 151 | ); 152 | 153 | // callback options 154 | let requestModifier = new RequestModifier( 155 | '/ajax/some-request', 156 | (matchedRequest) => ({url: '/ajax/some-different-request'}) 157 | ); 158 | 159 | // async callback options 160 | let requestRedirector = new RequestRedirector( 161 | '/ajax/some-request', 162 | async (matchedRequest) => 'some/new/url' 163 | ); 164 | 165 | // promise callback options 166 | let responseFaker = new ResponseFaker( 167 | '/ajax/some-request', 168 | (matchedRequest) => Promise.resolve(({ 169 | status: 200, 170 | contentType: 'application/json', 171 | body: JSON.stringify({successful: false, payload: []}) 172 | })) 173 | ); 174 | ``` 175 | 176 | ## RequestInterceptor Request Interception Order: 177 | 178 | ![image](./documentation/activity.png) 179 | 180 | ## Advanced Usage 181 | 182 | As long as you follow the interfaces provided in the [github repository](./src/interface) you can create your own Spies, Fakers, Modifiers or Blocker. 183 | 184 | ````js 185 | let interceptor = new RequestInterceptor( 186 | (testee, pattern) => testee.indexOf(pattern) > -1 187 | ); 188 | 189 | let count = 0; 190 | interceptor.addSpy({ 191 | isMatchingRequest: (_request, _matcher) => true, 192 | addMatch: (_request) => count++ 193 | }); 194 | 195 | interceptor.addFaker({ 196 | isMatchingRequest: (_request, _matcher) => true, 197 | getResponseFake: (request) => ({body: ''}) 198 | }); 199 | 200 | interceptor.addRequestModifier({ 201 | isMatchingRequest: (_request, _matcher) => true, 202 | getOverride: (interceptedRequest) => ({url: ''}) 203 | }); 204 | 205 | interceptor.setRequestBlocker({ 206 | shouldBlockRequest: (_request, _matcher) => true, 207 | clearUrlsToBlock: () => undefined, 208 | addUrlsToBlock: (urlsToBlock) => undefined 209 | }); 210 | ```` 211 | 212 | ### Minimatch 213 | puppeteer-request-spy works great with [minimatch](https://github.com/isaacs/minimatch), it can be passed as the `matcher` function. 214 | ```js 215 | const minimatch = require('minimatch'); 216 | 217 | let cssSpy = new RequestSpy('**/*.css'); 218 | let responseFaker = new ResponseFaker('**/*.jpg', someFakeResponse); 219 | 220 | let requestInterceptor = new RequestInterceptor(minimatch); 221 | requestInterceptor.addFaker(responseFaker); 222 | requestInterceptor.addSpy(cssSpy); 223 | requestInterceptor.block('**/*.js'); 224 | 225 | await page.setRequestInterception(true); 226 | page.on('request', requestInterceptor.intercept.bind(requestInterceptor)); 227 | await page.goto('https://www.example.com'); 228 | 229 | assert.ok(cssSpy.hasMatch() && cssSpy.getMatchCount() > 0); 230 | for (let matchedRequest of cssSpy.getMatchedRequests()) { 231 | assert.ok(matchedRequest.response().status() === 200); 232 | } 233 | ``` 234 | # API 235 | 236 | Full API can be found [here](./documentation/API.md). 237 | 238 | # Examples 239 | 240 | There are some usage examples included in the [github repository](./examples). Check them out to get started with writing a simple test with puppeteer and puppeteer-request-spy. 241 | 242 | # Related 243 | - [minimatch](https://github.com/isaacs/minimatch) - For easily matching path-like strings to patterns. 244 | - [puppeteer](https://github.com/GoogleChrome/puppeteer) - Control chrome in headless mode with puppeteer for automated testing. 245 | 246 | # License 247 | MIT 248 | 249 | 250 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FTabueeee%2Fpuppeteer-request-spy.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FTabueeee%2Fpuppeteer-request-spy?ref=badge_large) 251 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "prefer-const": false, 9 | "interface-name": [ 10 | true, 11 | "always-prefix" 12 | ], 13 | /** 14 | * Security Rules. The following rules should be turned on because they find security issues 15 | * or are recommended in the Microsoft Secure Development Lifecycle (SDL) 16 | */ 17 | "insecure-random": true, 18 | "no-banned-terms": true, 19 | "no-cookies": true, 20 | "no-delete-expression": true, 21 | "no-disable-auto-sanitization": true, 22 | "no-document-domain": true, 23 | "no-document-write": true, 24 | "no-eval": true, 25 | "no-exec-script": true, 26 | "no-function-constructor-with-string-args": true, 27 | "no-http-string": [ 28 | true, 29 | "http://www.example.com/?.*", 30 | "http://www.examples.com/?.*" 31 | ], 32 | "no-inner-html": true, 33 | "no-octal-literal": true, 34 | "no-reserved-keywords": true, 35 | "no-string-based-set-immediate": true, 36 | "no-string-based-set-interval": true, 37 | "no-string-based-set-timeout": true, 38 | "non-literal-require": true, 39 | "possible-timing-attack": true, 40 | "react-anchor-blank-noopener": true, 41 | "react-iframe-missing-sandbox": true, 42 | "react-no-dangerous-html": true, 43 | /** 44 | * Common Bugs and Correctness. The following rules should be turned on because they find 45 | * common bug patterns in the code or enforce type safety. 46 | */ 47 | "await-promise": true, 48 | "forin": true, 49 | "jquery-deferred-must-complete": true, 50 | "label-position": true, 51 | "match-default-export-name": true, 52 | "mocha-avoid-only": true, 53 | "mocha-no-side-effect-code": true, 54 | "no-any": true, 55 | "no-arg": true, 56 | "no-backbone-get-set-outside-model": true, 57 | "no-bitwise": true, 58 | "no-conditional-assignment": true, 59 | "no-console": [ 60 | true, 61 | "debug", 62 | "info", 63 | "log", 64 | "time", 65 | "timeEnd", 66 | "trace" 67 | ], 68 | "no-constant-condition": true, 69 | "no-control-regex": true, 70 | "no-debugger": true, 71 | "no-duplicate-switch-case": true, 72 | "no-duplicate-super": true, 73 | "no-duplicate-variable": true, 74 | "no-empty": true, 75 | "no-floating-promises": true, 76 | "no-for-in-array": true, 77 | "no-import-side-effect": true, 78 | "no-increment-decrement": false, 79 | "no-invalid-regexp": true, 80 | "no-invalid-template-strings": true, 81 | "no-invalid-this": true, 82 | "no-jquery-raw-elements": true, 83 | "no-misused-new": true, 84 | "no-non-null-assertion": true, 85 | "no-reference-import": true, 86 | "no-regex-spaces": true, 87 | "no-sparse-arrays": true, 88 | "no-unnecessary-class": true, 89 | "no-string-literal": true, 90 | "no-string-throw": true, 91 | "no-unnecessary-bind": true, 92 | "no-unnecessary-callback-wrapper": true, 93 | "no-unnecessary-initializer": true, 94 | "no-unnecessary-override": true, 95 | "no-unsafe-any": true, 96 | "no-unsafe-finally": true, 97 | "no-unused-expression": true, 98 | "no-use-before-declare": true, 99 | "no-with-statement": true, 100 | "promise-function-async": false, 101 | "promise-must-complete": true, 102 | "radix": true, 103 | "react-this-binding-issue": true, 104 | "react-unused-props-and-state": true, 105 | "restrict-plus-operands": true, 106 | // the plus operand should really only be used for strings and numbers 107 | "strict-boolean-expressions": true, 108 | "switch-default": true, 109 | "triple-equals": [ 110 | true, 111 | "allow-null-check" 112 | ], 113 | "use-isnan": true, 114 | "use-named-parameter": true, 115 | /** 116 | * Code Clarity. The following rules should be turned on because they make the code 117 | * generally more clear to the reader. 118 | */ 119 | "adjacent-overload-signatures": true, 120 | "array-type": [ 121 | true, 122 | "generic" 123 | ], 124 | "arrow-parens": false, 125 | // for simple functions the parens on arrow functions are not needed 126 | "callable-types": true, 127 | "chai-prefer-contains-to-index-of": true, 128 | "chai-vague-errors": true, 129 | "class-name": true, 130 | "comment-format": true, 131 | "completed-docs": false, 132 | "export-name": true, 133 | "function-name": true, 134 | "import-name": true, 135 | "jsdoc-format": false, 136 | "max-classes-per-file": [ 137 | true, 138 | 3 139 | ], 140 | // we generally recommend making one public class per file 141 | "max-file-line-count": true, 142 | "max-func-body-length": [ 143 | true, 144 | 100, 145 | { 146 | "ignore-parameters-to-function-regex": "describe" 147 | } 148 | ], 149 | "max-line-length": [ 150 | true, 151 | 140 152 | ], 153 | "member-access": true, 154 | "member-ordering": [ 155 | true, 156 | { 157 | "order": "fields-first" 158 | } 159 | ], 160 | "missing-jsdoc": false, 161 | "mocha-unneeded-done": true, 162 | "new-parens": true, 163 | "no-construct": true, 164 | "no-default-export": true, 165 | "no-empty-interface": true, 166 | "no-for-in": true, 167 | "no-function-expression": true, 168 | "no-inferrable-types": false, 169 | // turn no-inferrable-types off in order to make the code consistent in its use of type decorations 170 | "no-multiline-string": true, 171 | // multiline-strings often introduce unnecessary whitespace into the string literals 172 | "no-null-keyword": false, 173 | // turn no-null-keyword off and use undefined to mean not initialized and null to mean without a value 174 | "no-parameter-properties": true, 175 | "no-relative-imports": false, 176 | "no-require-imports": true, 177 | "no-shadowed-variable": true, 178 | "no-suspicious-comment": true, 179 | "no-typeof-undefined": false, 180 | "no-unnecessary-field-initialization": true, 181 | "no-unnecessary-local-variable": true, 182 | "no-unnecessary-qualifier": true, 183 | "no-unsupported-browser-code": true, 184 | "no-useless-files": true, 185 | "no-var-keyword": true, 186 | "no-var-requires": true, 187 | "no-this-assignment": true, 188 | "no-void-expression": true, 189 | "object-literal-sort-keys": false, 190 | // turn object-literal-sort-keys off and sort keys in a meaningful manner 191 | "one-variable-per-declaration": true, 192 | "only-arrow-functions": false, 193 | // there are many valid reasons to declare a function 194 | "ordered-imports": true, 195 | "prefer-array-literal": false, 196 | "prefer-for-of": true, 197 | "prefer-method-signature": true, 198 | "prefer-template": true, 199 | "return-undefined": false, 200 | // this actually affect the readability of the code 201 | "typedef": [ 202 | true, 203 | "call-signature", 204 | "arrow-call-signature", 205 | "parameter", 206 | "arrow-parameter", 207 | "property-declaration", 208 | "variable-declaration", 209 | "member-variable-declaration" 210 | ], 211 | "underscore-consistent-invocation": true, 212 | "unified-signatures": true, 213 | "variable-name": true, 214 | /** 215 | * Accessibility. The following rules should be turned on to guarantee the best user 216 | * experience for keyboard and screen reader users. 217 | */ 218 | "react-a11y-anchors": true, 219 | "react-a11y-aria-unsupported-elements": true, 220 | "react-a11y-event-has-role": true, 221 | "react-a11y-image-button-has-alt": true, 222 | "react-a11y-img-has-alt": true, 223 | "react-a11y-lang": true, 224 | "react-a11y-meta": true, 225 | "react-a11y-props": true, 226 | "react-a11y-proptypes": true, 227 | "react-a11y-role": true, 228 | "react-a11y-role-has-required-aria-props": true, 229 | "react-a11y-role-supports-aria-props": true, 230 | "react-a11y-tabindex-no-positive": true, 231 | "react-a11y-titles": true, 232 | /** 233 | * Whitespace related rules. The only recommended whitespace strategy is to pick a single format and 234 | * be consistent. 235 | */ 236 | "align": [ 237 | true, 238 | "parameters", 239 | "arguments", 240 | "statements" 241 | ], 242 | "curly": true, 243 | "eofline": true, 244 | "import-spacing": true, 245 | "indent": [ 246 | true, 247 | "spaces" 248 | ], 249 | "linebreak-style": false, 250 | "newline-before-return": true, 251 | "no-consecutive-blank-lines": true, 252 | "no-empty-line-after-opening-brace": false, 253 | "no-single-line-block-comment": true, 254 | "no-trailing-whitespace": true, 255 | "no-unnecessary-semicolons": true, 256 | "object-literal-key-quotes": [ 257 | true, 258 | "as-needed" 259 | ], 260 | "one-line": [ 261 | true, 262 | "check-open-brace", 263 | "check-catch", 264 | "check-else", 265 | "check-whitespace" 266 | ], 267 | "quotemark": [ 268 | true, 269 | "single" 270 | ], 271 | "react-tsx-curly-spacing": true, 272 | "semicolon": [ 273 | true, 274 | "always" 275 | ], 276 | "trailing-comma": [ 277 | true, 278 | { 279 | "singleline": "never", 280 | "multiline": "never" 281 | } 282 | ], 283 | // forcing trailing commas for multi-line 284 | // lists results in lists that are easier to reorder and version control diffs that are more clear. 285 | // Many teams like to have multiline be 'always'. There is no clear consensus on this rule but the 286 | // internal MS JavaScript coding standard does discourage it. 287 | "typedef-whitespace": false, 288 | "whitespace": [ 289 | true, 290 | "check-branch", 291 | "check-decl", 292 | "check-operator", 293 | "check-separator", 294 | "check-type" 295 | ], 296 | /** 297 | * Controversial/Configurable rules. 298 | */ 299 | "ban": false, 300 | // only enable this if you have some code pattern that you want to ban 301 | "ban-types": true, 302 | "cyclomatic-complexity": true, 303 | "file-header": false, 304 | // enable this rule only if you are legally required to add a file header 305 | "import-blacklist": false, 306 | // enable and configure this as you desire 307 | "interface-over-type-literal": false, 308 | // there are plenty of reasons to prefer interfaces 309 | "no-angle-bracket-type-assertion": false, 310 | // pick either type-cast format and use it consistently 311 | "no-inferred-empty-object-type": false, 312 | // if the compiler is satisfied then this is probably not an issue 313 | "no-internal-module": false, 314 | // only enable this if you are not using internal modules 315 | "no-magic-numbers": false, 316 | // by default it will find too many false positives 317 | "no-mergeable-namespace": false, 318 | // your project may require mergeable namespaces 319 | "no-namespace": false, 320 | // only enable this if you are not using modules/namespaces 321 | "no-reference": true, 322 | // in general you should use a module system and not /// reference imports 323 | "no-unexternalized-strings": false, 324 | // the VS Code team has a specific localization process that this rule enforces 325 | "object-literal-shorthand": false, 326 | // object-literal-shorthand offers an abbreviation not an abstraction 327 | "prefer-type-cast": true, 328 | // pick either type-cast format and use it consistently 329 | "space-before-function-paren": false, 330 | // turn this on if this is really your coding standard 331 | 332 | /** 333 | * Deprecated rules. The following rules are deprecated for various reasons. 334 | */ 335 | "missing-optional-annotation": false, 336 | // now supported by TypeScript compiler 337 | "no-duplicate-parameter-names": false, 338 | // now supported by TypeScript compiler 339 | "no-empty-interfaces": false, 340 | // use tslint no-empty-interface rule instead 341 | "no-missing-visibility-modifiers": false, 342 | // use tslint member-access rule instead 343 | "no-multiple-var-decl": false, 344 | // use tslint one-variable-per-declaration rule instead 345 | "no-switch-case-fall-through": false 346 | // now supported by TypeScript compiler 347 | }, 348 | "rulesDirectory": [ 349 | "node_modules/tslint-microsoft-contrib/" 350 | ] 351 | } 352 | -------------------------------------------------------------------------------- /test/unit/puppeteer-request-spy.dev.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as nock from 'nock'; 3 | import {Overrides, Request, RespondOptions} from 'puppeteer'; 4 | import * as sinon from 'sinon'; 5 | import {SinonSpy} from 'sinon'; 6 | import {RequestMatcher, RequestModifier, RequestRedirector, ResponseModifier} from '../../src'; 7 | import {HttpRequestFactory} from '../../src/common/HttpRequestFactory'; 8 | import {RequestInterceptor} from '../../src/RequestInterceptor'; 9 | import {RequestSpy} from '../../src/RequestSpy'; 10 | import {ResponseFaker} from '../../src/ResponseFaker'; 11 | import {getRequestDouble} from '../common/testDoubleFactories'; 12 | import {unitTestHelpers} from './unitTestHelpers'; 13 | import getRequestDoubles = unitTestHelpers.getRequestDoubles; 14 | import createRequestHandlers = unitTestHelpers.createRequestHandlers; 15 | import addRequestHandlers = unitTestHelpers.addRequestHandlers; 16 | import RequestHandlers = unitTestHelpers.RequestHandlers; 17 | import simulateUsage = unitTestHelpers.simulateUsage; 18 | 19 | describe('unit', async (): Promise => { 20 | let requestInterceptor: RequestInterceptor; 21 | 22 | let requestSpy: RequestSpy; 23 | let responseFaker: ResponseFaker; 24 | let requestModifier: RequestModifier; 25 | let responseModifier: ResponseModifier; 26 | let requestRedirector: RequestRedirector; 27 | //@ts-ignore 28 | let requestMatchingSpy: Request; 29 | let requestMatchingFaker: Request; 30 | let requestMatchingBlocker: Request; 31 | let requestMatchingRequestModifier: Request; 32 | let requestMatchingResponseModifier: Request; 33 | let requestMatchingRequestRedirector: Request; 34 | 35 | let overrides: Overrides = { 36 | headers: {key: 'value'} 37 | }; 38 | let responseFake: RespondOptions = { 39 | status: 200, 40 | contentType: 'text/plain', 41 | body: 'payload' 42 | }; 43 | 44 | before(() => { 45 | if (!nock.isActive()) { 46 | nock.activate(); 47 | } 48 | nock.disableNetConnect(); 49 | }); 50 | 51 | after(() => { 52 | nock.cleanAll(); 53 | nock.enableNetConnect(); 54 | nock.restore(); 55 | }); 56 | 57 | before(async () => { 58 | let matcher: RequestMatcher = (testString: string, pattern: string): boolean => testString.indexOf(pattern) > -1; 59 | requestInterceptor = new RequestInterceptor(matcher); 60 | let requestDoubles: Array = getRequestDoubles(); 61 | let requestHandlers: RequestHandlers = createRequestHandlers( 62 | responseFake, 63 | overrides 64 | ); 65 | 66 | ([ 67 | requestMatchingSpy, 68 | requestMatchingFaker, 69 | requestMatchingBlocker, 70 | requestMatchingRequestModifier, 71 | requestMatchingResponseModifier, 72 | requestMatchingRequestRedirector 73 | ] = requestDoubles); 74 | 75 | ({ 76 | requestSpy, 77 | responseFaker, 78 | requestModifier, 79 | responseModifier, 80 | requestRedirector 81 | } = requestHandlers); 82 | 83 | addRequestHandlers(requestInterceptor, requestHandlers); 84 | await simulateUsage(requestInterceptor, requestDoubles); 85 | }); 86 | 87 | describe('class: RequestSpy', async (): Promise => { 88 | it('should have recognized a match', () => { 89 | assert.ok(requestSpy.hasMatch(), 'requestSpy did not match url'); 90 | }); 91 | 92 | it('should have a matchCount of 3', () => { 93 | assert.strictEqual(requestSpy.getMatchCount(), 3, 'requestSpy did not increase count on match'); 94 | 95 | }); 96 | 97 | it('should return passed pattern as Array', () => { 98 | assert.deepStrictEqual(requestSpy.getPatterns(), ['spy']); 99 | 100 | }); 101 | 102 | it('should return passed patterns as Array', () => { 103 | let requestSpyWithArray: RequestSpy = new RequestSpy(['some/pattern/**/*']); 104 | assert.deepStrictEqual(requestSpyWithArray.getPatterns(), ['some/pattern/**/*']); 105 | }); 106 | 107 | it('should retrieve expected urls from matched Requests', () => { 108 | assert.deepStrictEqual( 109 | requestSpy.getMatchedRequests().map((request: Request) => request.url()), 110 | [ 111 | 'spy', 112 | 'spy', 113 | 'spy' 114 | ], 115 | 'requestSpy didn\'t add all urls' 116 | ); 117 | }); 118 | 119 | it('should provide urls as an Array', () => { 120 | assert.deepStrictEqual( 121 | requestSpy.getMatchedUrls(), 122 | [ 123 | 'spy', 124 | 'spy', 125 | 'spy' 126 | ], 127 | 'requestSpy didn\'t add all urls' 128 | ); 129 | }); 130 | 131 | it('should throw an Error on invalid patterns', () => { 132 | assert.throws(() => { 133 | // @ts-ignore: ignore error to test invalid input from js 134 | // noinspection TsLint 135 | new RequestSpy(3); 136 | }); 137 | }); 138 | }); 139 | 140 | describe('class: ResponseFaker', async (): Promise => { 141 | it('should have responded three fakes', async (): Promise => { 142 | sinon.assert.callCount(requestMatchingFaker.respond, 3); 143 | }); 144 | 145 | it('should respond with provided fake', async (): Promise => { 146 | sinon.assert.calledWithExactly(requestMatchingFaker.respond, responseFake); 147 | }); 148 | 149 | it('should return provided fake', async (): Promise => { 150 | assert.deepStrictEqual(await responseFaker.getResponseFake(getRequestDouble()), responseFake); 151 | }); 152 | 153 | it('should return passed pattern as Array', async (): Promise => { 154 | assert.deepStrictEqual(responseFaker.getPatterns(), ['faker']); 155 | }); 156 | 157 | it('should return passed patterns as Array', async (): Promise => { 158 | let responseFakerWithArray: ResponseFaker = new ResponseFaker(['some/pattern/**/*'], responseFake); 159 | assert.deepStrictEqual(responseFakerWithArray.getPatterns(), ['some/pattern/**/*']); 160 | }); 161 | 162 | it('should throw an Error on invalid patterns', () => { 163 | assert.throws(() => { 164 | // @ts-ignore: ignore error to test invalid input from js 165 | // noinspection TsLint 166 | new ResponseFaker(3); 167 | }); 168 | }); 169 | }); 170 | 171 | describe('class: ResponseModifier', async (): Promise => { 172 | it('should have modified three fakes', async (): Promise => { 173 | sinon.assert.callCount(requestMatchingResponseModifier.respond, 3); 174 | }); 175 | 176 | it('should respond with modified response', async (): Promise => { 177 | sinon.assert.calledWithExactly(requestMatchingResponseModifier.respond, { 178 | status: 200, 179 | contentType: 'text/html', 180 | headers: { 181 | 'content-type': 'text/html' 182 | }, 183 | body: 'original response' 184 | }); 185 | }); 186 | 187 | it('should return passed pattern as Array', async (): Promise => { 188 | assert.deepStrictEqual(responseModifier.getPatterns(), ['responseModifier']); 189 | }); 190 | 191 | it('should return passed patterns as Array', async (): Promise => { 192 | let responseModifierWithArray: ResponseModifier = new ResponseModifier( 193 | ['some/pattern/**/*'], 194 | (err: Error | undefined, response: string): string => err ? err.toString() : response.replace(' body', ''), 195 | new HttpRequestFactory() 196 | ); 197 | assert.deepStrictEqual(responseModifierWithArray.getPatterns(), ['some/pattern/**/*']); 198 | }); 199 | 200 | it('should throw an Error on invalid patterns', () => { 201 | assert.throws(() => { 202 | // @ts-ignore: ignore error to test invalid input from js 203 | // noinspection TsLint 204 | new ResponseModifier(new HttpRequestFactory(), 3); 205 | }); 206 | }); 207 | }); 208 | 209 | describe('class: RequestRedirector', async (): Promise => { 210 | it('should have redirected three requests', async (): Promise => { 211 | sinon.assert.callCount(requestMatchingRequestRedirector.continue, 3); 212 | 213 | // todo improve 214 | sinon.assert.alwaysCalledWith( 215 | requestMatchingRequestRedirector.continue, 216 | { 217 | headers: { 218 | 'test-header-single': 'val', 219 | 'test-header-multi': 'val, val2', 220 | 'text-header-empty': '' 221 | }, 222 | method: 'GET', 223 | postData: {}, 224 | url: 'http://www.example.com/requestRedirector' 225 | } 226 | ); 227 | }); 228 | 229 | it('should respond with redirected fake', async (): Promise => { 230 | // todo improve 231 | sinon.assert.alwaysCalledWith( 232 | requestMatchingRequestRedirector.continue, 233 | { 234 | headers: { 235 | 'test-header-single': 'val', 236 | 'test-header-multi': 'val, val2', 237 | 'text-header-empty': '' 238 | }, 239 | method: 'GET', 240 | postData: {}, 241 | url: 'http://www.example.com/requestRedirector' 242 | } 243 | ); 244 | }); 245 | 246 | it('should return passed pattern as Array', async (): Promise => { 247 | assert.deepStrictEqual(requestRedirector.getPatterns(), ['requestRedirector']); 248 | }); 249 | 250 | it('should return passed patterns as Array', async (): Promise => { 251 | let requestRedirectorWithArray: RequestRedirector = new RequestRedirector( 252 | ['some/pattern/**/*'], 253 | sinon.stub().returns('some-url') 254 | ); 255 | 256 | assert.deepStrictEqual(requestRedirectorWithArray.getPatterns(), ['some/pattern/**/*']); 257 | }); 258 | 259 | it('should throw an Error on invalid patterns', () => { 260 | assert.throws(() => { 261 | // @ts-ignore: ignore error to test invalid input from js 262 | // noinspection TsLint 263 | new RequestRedirector(new HttpRequestFactory(), 3); 264 | }); 265 | }); 266 | }); 267 | 268 | describe('class: RequestBlocker', () => { 269 | it('should block request if urlsToBlock is added and matched', async (): Promise => { 270 | sinon.assert.callCount(requestMatchingBlocker.abort, 3); 271 | }); 272 | }); 273 | 274 | describe('class: RequestModifier', () => { 275 | it('should call continue with provided override options', () => { 276 | sinon.assert.calledWithExactly(requestMatchingRequestModifier.continue, overrides); 277 | }); 278 | 279 | it('should return passed pattern as Array', async (): Promise => { 280 | assert.deepStrictEqual(requestModifier.getPatterns(), ['modifier']); 281 | }); 282 | 283 | it('should return passed patterns as Array', async (): Promise => { 284 | let requestModifierWithArray: RequestModifier = new RequestModifier(['some/pattern/**/*'], overrides); 285 | assert.deepStrictEqual(requestModifierWithArray.getPatterns(), ['some/pattern/**/*']); 286 | }); 287 | 288 | it('should return provided override', async () => { 289 | assert.deepStrictEqual(await requestModifier.getOverride(getRequestDouble()), overrides); 290 | }); 291 | 292 | it('should throw an Error on invalid patterns', () => { 293 | assert.throws(() => { 294 | // @ts-ignore: ignore error to test invalid input from js 295 | // noinspection TsLint 296 | new RequestModifier(3); 297 | }); 298 | }); 299 | }); 300 | }); 301 | -------------------------------------------------------------------------------- /test/regression/unit.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {Request} from 'puppeteer'; 3 | import * as sinon from 'sinon'; 4 | import {SinonSpy} from 'sinon'; 5 | import {RequestInterceptor} from '../../src/RequestInterceptor'; 6 | import {RequestSpy} from '../../src/RequestSpy'; 7 | import {ResponseFaker} from '../../src/ResponseFaker'; 8 | import {TestDouble} from '../common/TestDouble'; 9 | import {getRequestDouble} from '../common/testDoubleFactories'; 10 | 11 | describe( 12 | 'puppeteer-request-spy: regression-unit: #4 Locks after starting to intercept requests when there are no urls to block added ', 13 | (): void => { 14 | describe('happy-path: ensure continue is called, when requestInterceptor does not match', () => { 15 | let requestInterceptor: RequestInterceptor; 16 | 17 | before(() => { 18 | let matcher: (testString: string, pattern: string) => boolean = sinon.stub().returns(false); 19 | requestInterceptor = new RequestInterceptor(matcher, {log: (): void => undefined}); 20 | }); 21 | 22 | beforeEach(() => { 23 | requestInterceptor.clearUrlsToBlock(); 24 | requestInterceptor.clearSpies(); 25 | requestInterceptor.clearFakers(); 26 | }); 27 | 28 | it('RequestSpy only', async (): Promise => { 29 | let requestSpy: TestDouble = new RequestSpy('some/pattern/**/*'); 30 | let request: TestDouble = getRequestDouble(); 31 | 32 | requestInterceptor.addSpy( requestSpy); 33 | await requestInterceptor.intercept( request); 34 | await requestInterceptor.intercept( request); 35 | await requestInterceptor.intercept( request); 36 | 37 | assert.ok( 38 | ( request.continue).called === true 39 | && ( request.respond).called === false 40 | && ( request.abort).called === false 41 | ); 42 | }); 43 | 44 | it('ResponseFaker only', async (): Promise => { 45 | let responseFaker: TestDouble = new ResponseFaker('some/pattern/**/*', {}); 46 | let request: TestDouble = getRequestDouble(); 47 | 48 | requestInterceptor.addFaker( responseFaker); 49 | await requestInterceptor.intercept( request); 50 | await requestInterceptor.intercept( request); 51 | await requestInterceptor.intercept( request); 52 | 53 | assert.ok( 54 | ( request.continue).called === true 55 | && ( request.respond).called === false 56 | && ( request.abort).called === false 57 | ); 58 | }); 59 | 60 | it('block only', async (): Promise => { 61 | let request: TestDouble = getRequestDouble(); 62 | 63 | requestInterceptor.block(['not-matching-pattern1', 'not-matching-pattern2']); 64 | await requestInterceptor.intercept( request); 65 | await requestInterceptor.intercept( request); 66 | await requestInterceptor.intercept( request); 67 | 68 | assert.ok( 69 | ( request.continue).called === true 70 | && ( request.respond).called === false 71 | && ( request.abort).called === false 72 | ); 73 | }); 74 | 75 | it('block and Faker', async (): Promise => { 76 | let request: TestDouble = getRequestDouble(); 77 | let responseFaker: TestDouble = new ResponseFaker('some/pattern/**/*', {}); 78 | 79 | requestInterceptor.addFaker( responseFaker); 80 | requestInterceptor.block(['not-matching-pattern1', 'not-matching-pattern2']); 81 | await requestInterceptor.intercept( request); 82 | await requestInterceptor.intercept( request); 83 | await requestInterceptor.intercept( request); 84 | 85 | assert.ok(( 86 | request.continue).called === true 87 | && ( request.respond).called === false 88 | && ( request.abort).called === false 89 | ); 90 | }); 91 | 92 | it('Spy and Faker', async (): Promise => { 93 | let request: TestDouble = getRequestDouble(); 94 | let responseFaker: TestDouble = new ResponseFaker('some/pattern/**/*', {}); 95 | let requestSpy: TestDouble = new RequestSpy('some/pattern/**/*'); 96 | 97 | requestInterceptor.addSpy( requestSpy); 98 | requestInterceptor.addFaker( responseFaker); 99 | 100 | await requestInterceptor.intercept( request); 101 | await requestInterceptor.intercept( request); 102 | await requestInterceptor.intercept( request); 103 | 104 | assert.ok( 105 | ( request.continue).called === true 106 | && ( request.respond).called === false 107 | && ( request.abort).called === false 108 | ); 109 | }); 110 | 111 | it('Spy and block', async (): Promise => { 112 | let request: TestDouble = getRequestDouble(); 113 | let requestSpy: TestDouble = new RequestSpy('some/pattern/**/*'); 114 | 115 | requestInterceptor.addSpy( requestSpy); 116 | requestInterceptor.block(['not-matching-pattern1', 'not-matching-pattern2']); 117 | 118 | await requestInterceptor.intercept( request); 119 | await requestInterceptor.intercept( request); 120 | await requestInterceptor.intercept( request); 121 | 122 | assert.ok(( 123 | request.continue).called === true 124 | && ( request.respond).called === false 125 | && ( request.abort).called === false 126 | ); 127 | }); 128 | 129 | it('Spy, block and Faker', async (): Promise => { 130 | let request: TestDouble = getRequestDouble(); 131 | let requestSpy: TestDouble = new RequestSpy('some/pattern/**/*'); 132 | let responseFaker: TestDouble = new ResponseFaker('some/pattern/**/*', {}); 133 | 134 | requestInterceptor.addFaker( responseFaker); 135 | requestInterceptor.addSpy( requestSpy); 136 | requestInterceptor.block(['not-matching-pattern1', 'not-matching-pattern2']); 137 | 138 | await requestInterceptor.intercept( request); 139 | await requestInterceptor.intercept( request); 140 | await requestInterceptor.intercept( request); 141 | 142 | assert.ok(( 143 | request.continue).called === true 144 | && ( request.respond).called === false 145 | && ( request.abort).called === false 146 | ); 147 | }); 148 | }); 149 | 150 | describe('sad-path: ensure only one request action is called, when requestInterceptor does match', () => { 151 | let requestInterceptor: RequestInterceptor; 152 | 153 | before(() => { 154 | let matcher: (testString: string, pattern: string) => boolean = sinon.stub().returns(true); 155 | requestInterceptor = new RequestInterceptor(matcher, {log: (): void => undefined}); 156 | }); 157 | 158 | beforeEach(() => { 159 | requestInterceptor.clearUrlsToBlock(); 160 | requestInterceptor.clearSpies(); 161 | requestInterceptor.clearFakers(); 162 | }); 163 | 164 | it('RequestSpy only', async (): Promise => { 165 | let requestSpy: TestDouble = new RequestSpy('some/pattern/**/*'); 166 | let request: TestDouble = getRequestDouble(); 167 | 168 | requestInterceptor.addSpy( requestSpy); 169 | await requestInterceptor.intercept( request); 170 | await requestInterceptor.intercept( request); 171 | await requestInterceptor.intercept( request); 172 | 173 | assert.ok( 174 | ( request.continue).called === true 175 | && ( request.respond).called === false 176 | && ( request.abort).called === false 177 | ); 178 | }); 179 | 180 | it('RequestSpy only', async (): Promise => { 181 | let requestSpy: TestDouble = new RequestSpy('some/pattern/**/*'); 182 | let request: TestDouble = getRequestDouble(); 183 | 184 | requestInterceptor.addSpy( requestSpy); 185 | await requestInterceptor.intercept( request); 186 | await requestInterceptor.intercept( request); 187 | await requestInterceptor.intercept( request); 188 | 189 | assert.ok( 190 | ( request.continue).called === true 191 | && ( request.respond).called === false 192 | && ( request.abort).called === false 193 | ); 194 | }); 195 | 196 | it('ResponseFaker only', async (): Promise => { 197 | let responseFaker: TestDouble = new ResponseFaker('some/pattern/**/*', {}); 198 | let request: TestDouble = getRequestDouble(); 199 | 200 | requestInterceptor.addFaker( responseFaker); 201 | await requestInterceptor.intercept( request); 202 | await requestInterceptor.intercept( request); 203 | await requestInterceptor.intercept( request); 204 | 205 | assert.ok( 206 | ( request.continue).called === false 207 | && ( request.respond).called === true 208 | && ( request.abort).called === false 209 | ); 210 | }); 211 | 212 | it('block only', async (): Promise => { 213 | let request: TestDouble = getRequestDouble(); 214 | 215 | requestInterceptor.block(['not-matching-pattern1', 'not-matching-pattern2']); 216 | await requestInterceptor.intercept( request); 217 | await requestInterceptor.intercept( request); 218 | await requestInterceptor.intercept( request); 219 | 220 | assert.ok( 221 | ( request.continue).called === false 222 | && ( request.respond).called === false 223 | && ( request.abort).called === true 224 | ); 225 | }); 226 | 227 | it('block and Faker', async (): Promise => { 228 | let request: TestDouble = getRequestDouble(); 229 | let responseFaker: TestDouble = new ResponseFaker('some/pattern/**/*', {}); 230 | 231 | requestInterceptor.addFaker( responseFaker); 232 | requestInterceptor.block(['not-matching-pattern1', 'not-matching-pattern2']); 233 | await requestInterceptor.intercept( request); 234 | await requestInterceptor.intercept( request); 235 | await requestInterceptor.intercept( request); 236 | 237 | assert.ok( 238 | ( request.continue).called === false 239 | && ( request.respond).called === false 240 | && ( request.abort).called === true 241 | ); 242 | }); 243 | 244 | it('Spy and Faker', async (): Promise => { 245 | let request: TestDouble = getRequestDouble(); 246 | let responseFaker: TestDouble = new ResponseFaker('some/pattern/**/*', {}); 247 | let requestSpy: TestDouble = new RequestSpy('some/pattern/**/*'); 248 | 249 | requestInterceptor.addSpy( requestSpy); 250 | requestInterceptor.addFaker( responseFaker); 251 | 252 | await requestInterceptor.intercept( request); 253 | await requestInterceptor.intercept( request); 254 | await requestInterceptor.intercept( request); 255 | 256 | assert.ok( 257 | ( request.continue).called === false 258 | && ( request.respond).called === true 259 | && ( request.abort).called === false 260 | ); 261 | }); 262 | 263 | it('Spy and block', async (): Promise => { 264 | let request: TestDouble = getRequestDouble(); 265 | let requestSpy: TestDouble = new RequestSpy('some/pattern/**/*'); 266 | 267 | requestInterceptor.addSpy( requestSpy); 268 | requestInterceptor.block(['not-matching-pattern1', 'not-matching-pattern2']); 269 | 270 | await requestInterceptor.intercept( request); 271 | await requestInterceptor.intercept( request); 272 | await requestInterceptor.intercept( request); 273 | 274 | assert.ok( 275 | ( request.continue).called === false 276 | && ( request.respond).called === false 277 | && ( request.abort).called === true 278 | ); 279 | }); 280 | 281 | it('Spy, block and Faker', async (): Promise => { 282 | let request: TestDouble = getRequestDouble(); 283 | let requestSpy: TestDouble = new RequestSpy('some/pattern/**/*'); 284 | let responseFaker: TestDouble = new ResponseFaker('some/pattern/**/*', {}); 285 | 286 | requestInterceptor.addFaker( responseFaker); 287 | requestInterceptor.addSpy( requestSpy); 288 | requestInterceptor.block(['not-matching-pattern1', 'not-matching-pattern2']); 289 | 290 | await requestInterceptor.intercept( request); 291 | await requestInterceptor.intercept( request); 292 | await requestInterceptor.intercept( request); 293 | 294 | assert.ok( 295 | ( request.continue).called === false 296 | && ( request.respond).called === false 297 | && ( request.abort).called === true 298 | ); 299 | }); 300 | }); 301 | } 302 | ); 303 | --------------------------------------------------------------------------------