diff --git a/README.md b/README.md index 615c911..8d3e3a3 100644 --- a/README.md +++ b/README.md @@ -22,28 +22,21 @@ test('can create client', async () => { const mocketeer = Mocketeer.setup(page); // Set up a mock - const mock = await mocketeer.mockREST( - { - method: 'POST', - path: '/api/user', + const mock = await mocketeer.mockPost('/api/user', { + status: 201, + body: { + userId: '123', }, - { - status: 201, - body: { - userId: '123', - }, - } - ); + }); // Do something on the page await page.type('.email', 'email@example.com'); await page.click('.submit-button'); - // Verify request content - await expect(mock.getRequest()).resolves.toMatchObject({ - body: { - user_email: 'email@example.com', - }, + // Wait for mock to be called and verify request content + const request = await mock.getRequest(); + expect(request.body).toEqual({ + user_email: 'email@example.com', }); }); ``` @@ -64,27 +57,22 @@ import { Mocketeer } from '@hltech/mocketeer'; const mocketeer = Mocketeer.setup(page); // Set up a mock - const mock = await mocketeer.mockREST( - { - method: 'POST', - path: '/api/user', + const mock = await mocketeer.mockPost('/api/user', { + status: 201, + body: { + userId: '123', }, - { - status: 201, - body: { - error: false, - }, - } - ); + }); // Do something on the page await page.type('.email', 'email@example.com'); await page.click('.submit-button'); // Wait for mock to be called and verify request content - const req = await mock.getRequest(); - console.log(req.method); // POST - console.log(req.body); // { user_email: "email@example.com" } + const request = await mock.getRequest(); + expect(request.body).toEqual({ + user_email: 'email@example.com', + }); })(); ``` @@ -96,7 +84,7 @@ import { Mocketeer } from '@hltech/mocketeer'; Factory method used to set-up request mocking on provided Puppeteer Page. It creates and returns an instance of Mocketeer -Mocketeer will intercept all requests made by the page and match them to mocks set up with `mocketeer.addRestMock`. +Mocketeer will intercept all requests made by the page and match them to mocks set up with `mocketeer.mock`. If request does not match any mocks, it will be responded with `404 Not Found`. ###### Arguments @@ -113,10 +101,6 @@ const page = await browser.newPage(); const mocketeer = await Mocketeer.setup(page); ``` -#### ~~.activate(page: Page): Promise\~~ (depracated) - -Activate mocketeer on a given page. - ###### Arguments - `page` _(Page)_ instance of Puppeteer [Page](https://pptr.dev/#?product=Puppeteer&show=api-class-page) @@ -133,7 +117,7 @@ const page = await browser.newPage(); await mocketeer.activate(page); ``` -#### .mockREST(filter: RequestFilter, response: MockedResponse, options?): RestMock +#### .mock(filter: RequestFilter, response: MockedResponse, options?): RestMock Respond to xhr and fetch requests that match the `filter` with provided `response`. Request are matched based on adding order - most recently added first. @@ -161,7 +145,7 @@ Newly created instance of `RestMock`. ###### Example ```typescript -mocketeer.mockREST( +mocketeer.mock( { method: 'POST', url: '/api/clients', @@ -178,7 +162,7 @@ mocketeer.mockREST( ###### Example with query passed as an argument ```typescript -mocketeer.mockREST( +mocketeer.mock( { method: 'GET', url: '/api/clients', @@ -199,7 +183,7 @@ mocketeer.mockREST( ###### Example with queryString appended to the url ```typescript -mocketeer.mockREST( +mocketeer.mock( { method: 'GET', url: '/api/clients?city=Bristol&limit=10', diff --git a/jest.config.js b/jest.config.js index f64cbdc..f7949ef 100644 --- a/jest.config.js +++ b/jest.config.js @@ -40,7 +40,7 @@ module.exports = { tsConfig: 'test/integration/tsconfig.json', }, }, - resetMocks: true, + restoreMocks: true, }, ], }; diff --git a/package.json b/package.json index 48326c8..1293272 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "puppeteer": "1.13", "semantic-release": "^15.13.3", "ts-jest": "^24.0.1", - "typescript": "^3.4.1" + "typescript": "^3.6.3" }, "files": [ "dist" diff --git a/src/index.ts b/src/index.ts index 628672c..bc0f498 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ export * from './mocketeer'; -export * from './rest-mock'; +export * from './mock'; export * from './types'; diff --git a/src/rest-mock.ts b/src/mock.ts similarity index 90% rename from src/rest-mock.ts rename to src/mock.ts index c87fdca..5d4e087 100644 --- a/src/rest-mock.ts +++ b/src/mock.ts @@ -1,4 +1,3 @@ -import { ResourceType } from 'puppeteer'; import dbg from 'debug'; import { IMock, @@ -9,7 +8,7 @@ import { PathParameters, QueryObject, ReceivedRequest, - RequestFilter, + RequestMatcherObject, } from './types'; import { waitFor, TimeoutError, nth, arePathsDifferent } from './utils'; import isEqual from 'lodash.isequal'; @@ -23,7 +22,7 @@ const GET_REQUEST_TIMEOUT = 100; let debugId = 1; -export class RestMock implements IMock { +export class Mock implements IMock { private filter: ParsedFilterRequest; private response: MockedResponse; private requests: Array = []; @@ -36,7 +35,7 @@ export class RestMock implements IMock { private paramNames: (string | number)[] = []; constructor( - filter: RequestFilter, + filter: RequestMatcherObject, response: MockedResponse, options: Partial = {} ) { @@ -103,31 +102,29 @@ export class RestMock implements IMock { } this.requests.push({ ...request, params: this.getParams(request) }); + + const headers = { + ...{ 'Content-Type': 'application/json' }, + ...this.response.headers, + }; + + const body = + headers['Content-Type'] === 'application/json' + ? JSON.stringify(this.response.body) + : this.response.body; + return { status: this.response.status, - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(this.response.body), + headers, + body, }; } - private static allowedTypes: ResourceType[] = ['fetch', 'xhr']; - private isMatchingRequest( request: ReceivedRequest, pageOrigin: string ): boolean { - if (RestMock.allowedTypes.indexOf(request.type) === -1) { - this.debugMiss( - 'type', - request.type, - RestMock.allowedTypes.toString() - ); - return false; - } - - if (request.method !== this.filter.method) { + if (this.filter.method && request.method !== this.filter.method) { this.debugMiss('method', request.method, this.filter.method); return false; } @@ -167,7 +164,7 @@ export class RestMock implements IMock { } private createParsedFilterRequest( - filter: RequestFilter + filter: RequestMatcherObject ): ParsedFilterRequest { // TODO find a better alternative for url.parse const { protocol, host, pathname, query } = parse(filter.url, true); diff --git a/src/mocketeer.ts b/src/mocketeer.ts index 9e534f8..cede51f 100644 --- a/src/mocketeer.ts +++ b/src/mocketeer.ts @@ -1,15 +1,20 @@ import { parse } from 'url'; import { Page, Request, ResourceType } from 'puppeteer'; import dbg from 'debug'; -import { RestMock } from './rest-mock'; +import { Mock } from './mock'; import { MockedResponse, MockOptions, - RequestFilter, - RequestMethodFilter, + RequestMatcher, + RequestMatcherShort, REST_METHOD, } from './types'; -import { addMockByPriority, printRequest, requestToPlainObject } from './utils'; +import { + addMockByPriority, + printRequest, + requestToPlainObject, + createRequestFilter, +} from './utils'; const interceptedTypes: ResourceType[] = ['xhr', 'fetch']; @@ -20,7 +25,7 @@ export interface MocketeerOptions { } export class Mocketeer { - private mocks: RestMock[] = []; + private mocks: Mock[] = []; constructor(options: Partial = {}) { if (options.debug) { @@ -38,106 +43,67 @@ export class Mocketeer { return mocketeer; } - /** - * use Mocketeer.setup instead - * @deprecated - */ - public async activate(page: Page): Promise { + private async activate(page: Page): Promise { await page.setRequestInterception(true); page.on('request', request => this.onRequest(request)); } - /* - * Use mockREST instead - * @deprecated - */ - public addRestMock( - filter: RequestFilter, - response: MockedResponse, - options?: Partial - ): RestMock { - const mock = new RestMock(filter, response, { - ...options, - }); - - addMockByPriority(this.mocks, mock); - return mock; - } - - public mockREST( - filter: RequestFilter, + public mock( + filter: RequestMatcher, response: MockedResponse, options?: Partial - ): RestMock { - const mock = new RestMock(filter, response, { - ...options, - }); - + ): Mock { + const filterObject = createRequestFilter(filter); + const mock = new Mock(filterObject, response, { ...options }); addMockByPriority(this.mocks, mock); return mock; } public mockGET( - filter: RequestMethodFilter | string, + filter: RequestMatcherShort, response: MockedResponse, options?: Partial - ): RestMock { - const filterObject = - typeof filter === 'string' ? { url: filter } : filter; - - return this.mockREST( - { ...filterObject, method: REST_METHOD.GET }, - response, - options - ); + ): Mock { + const filterObject = createRequestFilter(filter, { + method: REST_METHOD.GET, + }); + return this.mock(filterObject, response, options); } public mockPOST( - filter: RequestMethodFilter | string, + filter: RequestMatcherShort, response: MockedResponse, options?: Partial - ): RestMock { - const filterObject = - typeof filter === 'string' ? { url: filter } : filter; - - return this.mockREST( - { ...filterObject, method: REST_METHOD.POST }, - response, - options - ); + ): Mock { + const filterObject = createRequestFilter(filter, { + method: REST_METHOD.POST, + }); + return this.mock(filterObject, response, options); } public mockPUT( - filter: RequestMethodFilter | string, + filter: RequestMatcherShort, response: MockedResponse, options?: Partial - ): RestMock { - const filterObject = - typeof filter === 'string' ? { url: filter } : filter; - - return this.mockREST( - { ...filterObject, method: REST_METHOD.PUT }, - response, - options - ); + ): Mock { + const filterObject = createRequestFilter(filter, { + method: REST_METHOD.PUT, + }); + return this.mock(filterObject, response, options); } public mockDELETE( - filter: RequestMethodFilter | string, + filter: RequestMatcherShort, response: MockedResponse, options?: Partial - ): RestMock { - const filterObject = - typeof filter === 'string' ? { url: filter } : filter; - - return this.mockREST( - { ...filterObject, method: REST_METHOD.DELETE }, - response, - options - ); + ): Mock { + const filterObject = createRequestFilter(filter, { + method: REST_METHOD.DELETE, + }); + return this.mock(filterObject, response, options); } - public removeMock(mock: RestMock): RestMock | void { + public removeMock(mock: Mock): Mock | void { const index = this.mocks.indexOf(mock); if (index > -1) { return this.mocks.splice(index, 1)[0]; @@ -147,7 +113,7 @@ export class Mocketeer { private async onRequest(request: Request): Promise { // Serialize request const requestData = requestToPlainObject(request); - const typeMatch = + const should404 = interceptedTypes.indexOf(request.resourceType()) !== -1; debug( @@ -156,17 +122,6 @@ export class Mocketeer { } ` ); - // Do not intercept non xhr/fetch requests - if (!typeMatch) { - debug(`< res: continue`); - try { - return await request.continue(); - } catch (e) { - // Request could be already handled so ignore this error - return; - } - } - // Obtain request url from originating frame url const originFrame = request.frame(); const originFrameUrl = originFrame ? await originFrame.url() : ''; @@ -186,7 +141,9 @@ export class Mocketeer { response.body }` ); - return await request.respond(response); + return await request.respond({ + ...response, + }); } catch (e) { console.error( `Failed to reply with mocked response for ${printRequest( @@ -200,11 +157,24 @@ export class Mocketeer { } // Request was not matched - log error and return 404 - debug(`< res: status=404`); - console.error(`Mock not found for request: ${printRequest(request)}`); - return request.respond({ - status: 404, - body: 'No mock provided for request', - }); + if (should404) { + debug(`< res: status=404`); + console.error( + `Mock not found for request: ${printRequest(request)}` + ); + return request.respond({ + status: 404, + body: 'No mock provided for request', + }); + } + + // Do not intercept non xhr/fetch requests + debug(`< res: continue`); + try { + return await request.continue(); + } catch (e) { + // Request could be already handled so ignore this error + return; + } } } diff --git a/src/types.ts b/src/types.ts index 1d2dca7..5ed77c2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,17 +2,18 @@ import { ResourceType } from 'puppeteer'; export type QueryObject = Record; -export interface RequestMethodFilter { +export type RequestMatcher = RequestMatcherObject | string; + +export interface RequestMatcherObject { + method?: string; url: string; query?: QueryObject; } -export interface RequestFilter extends RequestMethodFilter { - method: string; -} +export type RequestMatcherShort = Omit | string; export interface ParsedFilterRequest { - method: string; + method?: string; hostname: string | undefined; path: string | undefined; query: QueryObject; @@ -23,7 +24,7 @@ export interface ParsedFilterRequest { export interface MockedResponse { status: number; headers?: Record; - body: any; + body?: any; } export interface ReceivedRequest { diff --git a/src/utils.ts b/src/utils.ts index 29e046b..607351d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ import { parse } from 'url'; import { Request } from 'puppeteer'; -import { ReceivedRequest } from './types'; +import { ReceivedRequest, RequestMatcher, RequestMatcherObject } from './types'; export function requestToPlainObject(request: Request): ReceivedRequest { const url = request.url(); @@ -87,3 +87,20 @@ export function arePathsDifferent(path?: string, paramsRegexp?: RegExp) { return false; } + +export function createRequestFilter( + input: RequestMatcher, + defaults: Omit = {} +): RequestMatcherObject { + if (typeof input === 'string') { + return { + ...defaults, + url: input, + }; + } else { + return { + ...defaults, + ...input, + }; + } +} diff --git a/test/integration/fixture/mock-fixtures.ts b/test/integration/fixture/mock-fixtures.ts index 89025b8..49b4938 100644 --- a/test/integration/fixture/mock-fixtures.ts +++ b/test/integration/fixture/mock-fixtures.ts @@ -1,22 +1,22 @@ -import { RequestFilter, RequestMethodFilter } from '../../../src/types'; +import { RequestMatcherObject, RequestMatcherShort } from '../../../src/types'; -export const requestFoo: RequestMethodFilter = { +export const requestFoo: RequestMatcherShort = { url: '/foo', }; -export const requestFooWithQuery: RequestMethodFilter = { +export const requestFooWithQuery: RequestMatcherShort = { url: '/foo', query: { param: 'fooParam', }, }; -export const requestGetFoo: RequestFilter = { +export const requestGetFoo: RequestMatcherObject = { method: 'GET', url: '/foo', }; -export const requestGetFooWithQuery: RequestFilter = { +export const requestGetFooWithQuery: RequestMatcherObject = { method: 'GET', url: '/foo', query: { @@ -24,17 +24,17 @@ export const requestGetFooWithQuery: RequestFilter = { }, }; -export const requestPostFoo: RequestFilter = { +export const requestPostFoo: RequestMatcherObject = { method: 'POST', url: '/foo', }; -export const requestPutFoo: RequestFilter = { +export const requestPutFoo: RequestMatcherObject = { method: 'PUT', url: '/foo', }; -export const requestDeleteFoo: RequestFilter = { +export const requestDeleteFoo: RequestMatcherObject = { method: 'DELETE', url: '/foo', }; diff --git a/test/integration/fixture/page1.html b/test/integration/fixture/page1.html new file mode 100644 index 0000000..99efc01 --- /dev/null +++ b/test/integration/fixture/page1.html @@ -0,0 +1,10 @@ + + + + + page1 + + + + + diff --git a/test/integration/fixture/script.js b/test/integration/fixture/script.js new file mode 100644 index 0000000..7a663e5 --- /dev/null +++ b/test/integration/fixture/script.js @@ -0,0 +1 @@ +window.scriptLoaded = true; diff --git a/test/integration/fixture/style.css b/test/integration/fixture/style.css new file mode 100644 index 0000000..ece8c56 --- /dev/null +++ b/test/integration/fixture/style.css @@ -0,0 +1,3 @@ +body { + color: rgb(255, 0, 0); +} diff --git a/test/integration/mocketeer.int.test.ts b/test/integration/mocketeer.int.test.ts index e930cb7..bcbd2d4 100644 --- a/test/integration/mocketeer.int.test.ts +++ b/test/integration/mocketeer.int.test.ts @@ -46,7 +46,7 @@ describe('Mocketeer integration', () => { describe('mockREST', () => { it('mocks fetch GET request', async () => { - await mocketeer.mockREST(requestGetFoo, response200Ok); + await mocketeer.mock(requestGetFoo, response200Ok); await page.evaluate(() => { fetch('/foo') @@ -61,7 +61,7 @@ describe('Mocketeer integration', () => { }); it('mocks fetch GET request with query', async () => { - await mocketeer.mockREST(requestGetFooWithQuery, response200Ok); + await mocketeer.mock(requestGetFooWithQuery, response200Ok); await page.evaluate(() => { fetch('/foo?param=fooParam') @@ -76,7 +76,7 @@ describe('Mocketeer integration', () => { }); it('mocks fetch POST request ', async () => { - await mocketeer.mockREST(requestPostFoo, response200Ok); + await mocketeer.mock(requestPostFoo, response200Ok); await page.evaluate(() => { fetch('/foo', { method: 'POST' }) @@ -91,7 +91,7 @@ describe('Mocketeer integration', () => { }); it('mocks fetch PUT request ', async () => { - await mocketeer.mockREST(requestPutFoo, response200Ok); + await mocketeer.mock(requestPutFoo, response200Ok); await page.evaluate(() => { fetch('/foo', { method: 'PUT' }) @@ -106,7 +106,7 @@ describe('Mocketeer integration', () => { }); it('mocks fetch DELETE request ', async () => { - await mocketeer.mockREST(requestDeleteFoo, response200Ok); + await mocketeer.mock(requestDeleteFoo, response200Ok); await page.evaluate(() => { fetch('/foo', { method: 'DELETE' }) @@ -303,7 +303,7 @@ describe('Mocketeer integration', () => { }); it('mocks multiple requests', async () => { - await mocketeer.mockREST(requestGetFoo, response200Empty); + await mocketeer.mock(requestGetFoo, response200Empty); await page.evaluate(() => { fetch('/foo').then(() => (document.body.innerHTML += '1')); @@ -317,7 +317,7 @@ describe('Mocketeer integration', () => { }); it('mocks response with status 500', async () => { - await mocketeer.mockREST(requestGetFoo, { + await mocketeer.mock(requestGetFoo, { status: 500, body: {}, }); @@ -335,7 +335,7 @@ describe('Mocketeer integration', () => { }); it('records intercepted request', async () => { - const mock = await mocketeer.mockREST(requestPostFoo, response200Empty); + const mock = await mocketeer.mock(requestPostFoo, response200Empty); await page.evaluate(() => { fetch('/foo', { @@ -360,7 +360,7 @@ describe('Mocketeer integration', () => { }); it('.getRequest() rejects when request matching mock was not found', async () => { - const mock = await mocketeer.addRestMock( + const mock = await mocketeer.mock( { method: 'GET', url: '/some_endpoint' }, { status: 200, body: 'OK' } ); @@ -384,7 +384,7 @@ describe('Mocketeer integration', () => { }); it('notifies when mock was called', async () => { - const mock = await mocketeer.mockREST(requestGetFoo, response200Empty); + const mock = await mocketeer.mock(requestGetFoo, response200Empty); await page.evaluate(() => { setTimeout(() => { @@ -396,9 +396,9 @@ describe('Mocketeer integration', () => { }); it('can set priorities on mocks', async () => { - const mock = await mocketeer.mockREST(requestGetFoo, response200Empty); + const mock = await mocketeer.mock(requestGetFoo, response200Empty); - const mockWithPriority = await mocketeer.mockREST( + const mockWithPriority = await mocketeer.mock( requestGetFoo, response200Empty, { @@ -419,7 +419,7 @@ describe('Mocketeer integration', () => { }); it('can remove mock so it is no longer called', async () => { - const mock = await mocketeer.mockREST(requestGetFoo, { + const mock = await mocketeer.mock(requestGetFoo, { status: 200, body: { id: 1 }, }); @@ -436,7 +436,7 @@ describe('Mocketeer integration', () => { mocketeer.removeMock(mock); - await mocketeer.mockREST(requestGetFoo, { + await mocketeer.mock(requestGetFoo, { status: 200, body: { id: 2 }, }); @@ -453,7 +453,7 @@ describe('Mocketeer integration', () => { }); it('can inspect requests that are invoke asynchronously', async () => { - const mock = await mocketeer.mockREST(requestGetFoo, response200Empty); + const mock = await mocketeer.mock(requestGetFoo, response200Empty); await page.evaluate(() => { document.body.innerHTML = 'content'; @@ -473,7 +473,7 @@ describe('Mocketeer integration', () => { it('matches only once request with once set to true', async () => { spyOn(console, 'error'); - await mocketeer.mockREST(requestGetFoo, response200Ok, { + await mocketeer.mock(requestGetFoo, response200Ok, { once: true, }); @@ -484,9 +484,9 @@ describe('Mocketeer integration', () => { }); it('matches only once request with once set to true', async () => { - await mocketeer.mockREST(requestGetFoo, { status: 200, body: {} }); + await mocketeer.mock(requestGetFoo, { status: 200, body: {} }); - await mocketeer.mockREST( + await mocketeer.mock( requestGetFoo, { status: 201, body: {} }, { @@ -500,13 +500,13 @@ describe('Mocketeer integration', () => { }); it('matches only once every request in order with once set to true', async () => { - await mocketeer.mockREST( + await mocketeer.mock( requestGetFoo, { status: 200, body: {} }, { once: true } ); - await mocketeer.mockREST( + await mocketeer.mock( requestGetFoo, { status: 201, body: {} }, { @@ -519,40 +519,40 @@ describe('Mocketeer integration', () => { }); it('matches newest request when added mock with same filter', async () => { - await mocketeer.mockREST(requestGetFoo, { status: 200, body: {} }); + await mocketeer.mock(requestGetFoo, { status: 200, body: {} }); await expect(makeRequest()).resolves.toBe(200); - await mocketeer.mockREST(requestGetFoo, { status: 201, body: {} }); + await mocketeer.mock(requestGetFoo, { status: 201, body: {} }); await expect(makeRequest()).resolves.toBe(201); }); it('matches newest request when multiple mocks have same filter', async () => { - await mocketeer.mockREST(requestGetFoo, { status: 200, body: {} }); - await mocketeer.mockREST(requestGetFoo, { status: 201, body: {} }); + await mocketeer.mock(requestGetFoo, { status: 200, body: {} }); + await mocketeer.mock(requestGetFoo, { status: 201, body: {} }); await expect(makeRequest()).resolves.toBe(201); }); it('matches newest request when added mock with same filter and older mock has once set to true ', async () => { - await mocketeer.mockREST( + await mocketeer.mock( requestGetFoo, { status: 200, body: {} }, { once: true } ); await expect(makeRequest()).resolves.toBe(200); - await mocketeer.mockREST(requestGetFoo, { status: 201, body: {} }); + await mocketeer.mock(requestGetFoo, { status: 201, body: {} }); await expect(makeRequest()).resolves.toBe(201); }); it('matches requests with once set to true in correct order when multiple mocks have same filter', async () => { - await mocketeer.mockREST( + await mocketeer.mock( requestGetFoo, { status: 200, body: {} }, { once: true } ); - await mocketeer.mockREST( + await mocketeer.mock( requestGetFoo, { status: 201, body: {} }, { @@ -560,7 +560,7 @@ describe('Mocketeer integration', () => { } ); - await mocketeer.mockREST( + await mocketeer.mock( requestGetFoo, { status: 202, body: {} }, { @@ -574,15 +574,15 @@ describe('Mocketeer integration', () => { }); it('matches request with highest priority when multiple mocks have same filter', async () => { - await mocketeer.mockREST( + await mocketeer.mock( requestGetFoo, { status: 200, body: {} }, { priority: 10 } ); - await mocketeer.mockREST(requestGetFoo, { status: 201, body: {} }); + await mocketeer.mock(requestGetFoo, { status: 201, body: {} }); - await mocketeer.mockREST( + await mocketeer.mock( requestGetFoo, { status: 202, body: {} }, { priority: 5 } @@ -592,37 +592,37 @@ describe('Mocketeer integration', () => { }); it('matches request in correct order with priority and once set to true when multiple mocks have same filter', async () => { - await mocketeer.mockREST( + await mocketeer.mock( requestGetFoo, { status: 200, body: {} }, { once: true } ); - await mocketeer.mockREST( + await mocketeer.mock( requestGetFoo, { status: 201, body: {} }, { once: true, priority: 10 } ); - await mocketeer.mockREST( + await mocketeer.mock( requestGetFoo, { status: 202, body: {} }, { once: true } ); - await mocketeer.mockREST( + await mocketeer.mock( requestGetFoo, { status: 203, body: {} }, { once: true, priority: 10 } ); - await mocketeer.mockREST( + await mocketeer.mock( requestGetFoo, { status: 204, body: {} }, { once: true, priority: 5 } ); - await mocketeer.mockREST( + await mocketeer.mock( requestGetFoo, { status: 205, body: {} }, { once: true } @@ -665,7 +665,7 @@ describe('Mocketeer integration', () => { }); it('mocks fetch GET request with path variable and query', async () => { - const mock = await mocketeer.mockREST( + const mock = await mocketeer.mock( { url: '/foo/:id?param=fooParam', method: 'GET' }, response200Empty ); @@ -681,7 +681,7 @@ describe('Mocketeer integration', () => { }); it('mocks fetch GET request with schema, origin, path variable and query', async () => { - const mock = await mocketeer.mockREST( + const mock = await mocketeer.mock( { url: 'https://localhost:3000/foo/:id?param=fooParam', method: 'GET', @@ -700,7 +700,7 @@ describe('Mocketeer integration', () => { }); it('mocks fetch GET request with multiple path variables', async () => { - const mock = await mocketeer.mockREST( + const mock = await mocketeer.mock( { url: '/foo/:id/:name', method: 'GET' }, response200Empty ); @@ -716,4 +716,70 @@ describe('Mocketeer integration', () => { await expect(request.params.name).toBe('mike'); }); }); + + test('does not mock request to assets (scripts, styles) by default', async () => { + await page.goto(`http://localhost:${PORT}/page1.html`); + await expect(page.title()).resolves.toEqual('page1'); + await expect( + page.evaluate(() => window['scriptLoaded']) + ).resolves.toEqual(true); + await expect( + page.evaluate(() => + window.getComputedStyle(document.body).getPropertyValue('color') + ) + ).resolves.toEqual('rgb(255, 0, 0)'); + }); + + test('respond with 404 for unmocked fetch requests', async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + await expect( + page.evaluate(() => fetch('/unmocked').then(res => res.status)) + ).resolves.toEqual(404); + }); + + test('respond with 404 for unmocked XHR requests', async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + await expect( + page.evaluate( + () => + new Promise(resolve => { + var xhr = new XMLHttpRequest(); + xhr.open('GET', '/unmocked', true); + xhr.onloadend = () => resolve(xhr.status); + xhr.send(null); + }) + ) + ).resolves.toEqual(404); + }); + + test('can mock redirect requests', async () => { + await mocketeer.mock('/redirect', { + status: 302, + headers: { + Location: `http://localhost:${PORT}/page1.html`, + }, + }); + + await page.evaluate(() => { + // @ts-ignore + window.location = '/redirect'; + }); + + await page.waitForNavigation(); + await expect(page.title()).resolves.toEqual('page1'); + }); + + test('can mock request to assets', async () => { + mocketeer.mock('/script.js', { + headers: { + 'Content-Type': 'text/javascript; charset=UTF-8', + }, + status: 200, + body: `window.mockLoaded = true`, + }); + await page.goto(`http://localhost:${PORT}/page1.html`); + await expect( + page.evaluate(() => window['mockLoaded']) + ).resolves.toEqual(true); + }); }); diff --git a/test/unit/fixtures/request.ts b/test/unit/fixtures/request.ts index e60d3b3..3d89c24 100644 --- a/test/unit/fixtures/request.ts +++ b/test/unit/fixtures/request.ts @@ -1,5 +1,5 @@ import { Request } from 'puppeteer'; -import { MockOptions, RequestFilter, RestMock } from '../../../src'; +import { MockOptions, RequestMatcherObject, Mock } from '../../../src'; export const createMockRequest = (): jest.Mocked => ({ postData: jest.fn().mockReturnValue(''), @@ -27,10 +27,10 @@ const mockedResponse = { }; export const createRestMock = ( - change: Partial = {}, + change: Partial = {}, options?: Partial ) => { - return new RestMock( + return new Mock( { url: '/foo', method: 'GET', diff --git a/test/unit/mocketeer.test.ts b/test/unit/mocketeer.test.ts index 58293bb..f684a14 100644 --- a/test/unit/mocketeer.test.ts +++ b/test/unit/mocketeer.test.ts @@ -1,19 +1,13 @@ -import { - Mocketeer, - RequestMethodFilter, - REST_METHOD, - RestMock, -} from '../../src'; +import { Mocketeer, RequestMatcherShort, REST_METHOD, Mock } from '../../src'; import { createMockPage } from './fixtures/page'; import { createMockRequest } from './fixtures/request'; -jest.mock('../../src/rest-mock'); +jest.mock('../../src/mock'); describe('Mocketeer', () => { describe('.activate', () => { it('.calls page.setRequestInterception and page.on ', async () => { - const mocketeer = new Mocketeer(); const page = createMockPage(); - await mocketeer.activate(page); + await Mocketeer.setup(page as any); expect(page.setRequestInterception).toHaveBeenCalledWith(true); expect(page.on).toHaveBeenCalledWith( 'request', @@ -46,8 +40,7 @@ describe('Mocketeer', () => { beforeEach(async () => { page = createMockPage(); - const mocketeer = new Mocketeer({ debug: true }); - await mocketeer.activate(page as any); + await Mocketeer.setup(page); }); test('calls continue request of unsupported resource type ', () => { @@ -62,8 +55,8 @@ describe('Mocketeer', () => { describe('mock http methods', () => { let mocketeer: Mocketeer; const url = 'url'; - const filter: RequestMethodFilter = { url }; - const filterWithQuery: RequestMethodFilter = { + const filter: RequestMatcherShort = { url }; + const filterWithQuery: RequestMatcherShort = { url, query: { param: 'fooParam', @@ -77,7 +70,7 @@ describe('Mocketeer', () => { test('should create RestMock using GET method and filter as object', () => { mocketeer.mockGET(filter, mockResponse); - expect(RestMock).toHaveBeenCalledWith( + expect(Mock).toHaveBeenCalledWith( expect.objectContaining({ method: REST_METHOD.GET, ...filter }), mockResponse, expect.anything() @@ -86,7 +79,7 @@ describe('Mocketeer', () => { test('should create RestMock using GET method with filter and query as objects', () => { mocketeer.mockGET(filterWithQuery, mockResponse); - expect(RestMock).toHaveBeenCalledWith( + expect(Mock).toHaveBeenCalledWith( expect.objectContaining({ method: REST_METHOD.GET, ...filterWithQuery, @@ -98,7 +91,7 @@ describe('Mocketeer', () => { test('should create RestMock using GET method and filter as url string', () => { mocketeer.mockGET(url, mockResponse); - expect(RestMock).toHaveBeenCalledWith( + expect(Mock).toHaveBeenCalledWith( expect.objectContaining({ method: REST_METHOD.GET, ...filter }), mockResponse, expect.anything() @@ -107,7 +100,7 @@ describe('Mocketeer', () => { test('should create RestMock using POST method and filter as object', () => { mocketeer.mockPOST(filter, mockResponse); - expect(RestMock).toHaveBeenCalledWith( + expect(Mock).toHaveBeenCalledWith( expect.objectContaining({ method: REST_METHOD.POST, ...filter, @@ -119,7 +112,7 @@ describe('Mocketeer', () => { test('should create RestMock using POST method with filter and query as objects', () => { mocketeer.mockPOST(filterWithQuery, mockResponse); - expect(RestMock).toHaveBeenCalledWith( + expect(Mock).toHaveBeenCalledWith( expect.objectContaining({ method: REST_METHOD.POST, ...filterWithQuery, @@ -131,7 +124,7 @@ describe('Mocketeer', () => { test('should create RestMock using POST method and filter as url string', () => { mocketeer.mockPOST(url, mockResponse); - expect(RestMock).toHaveBeenCalledWith( + expect(Mock).toHaveBeenCalledWith( expect.objectContaining({ method: REST_METHOD.POST, ...filter, @@ -143,7 +136,7 @@ describe('Mocketeer', () => { test('should create RestMock using PUT method and filter as object', () => { mocketeer.mockPUT(filter, mockResponse); - expect(RestMock).toHaveBeenCalledWith( + expect(Mock).toHaveBeenCalledWith( expect.objectContaining({ method: REST_METHOD.PUT, ...filter }), mockResponse, expect.anything() @@ -152,7 +145,7 @@ describe('Mocketeer', () => { test('should create RestMock using PUT method with filter and query as objects', () => { mocketeer.mockPUT(filterWithQuery, mockResponse); - expect(RestMock).toHaveBeenCalledWith( + expect(Mock).toHaveBeenCalledWith( expect.objectContaining({ method: REST_METHOD.PUT, ...filterWithQuery, @@ -164,7 +157,7 @@ describe('Mocketeer', () => { test('should create RestMock using PUT method and filter as url string', () => { mocketeer.mockPUT(url, mockResponse); - expect(RestMock).toHaveBeenCalledWith( + expect(Mock).toHaveBeenCalledWith( expect.objectContaining({ method: REST_METHOD.PUT, ...filter }), mockResponse, expect.anything() @@ -173,7 +166,7 @@ describe('Mocketeer', () => { test('should create RestMock using DELETE method and filter as object', () => { mocketeer.mockDELETE(filter, mockResponse); - expect(RestMock).toHaveBeenCalledWith( + expect(Mock).toHaveBeenCalledWith( expect.objectContaining({ method: REST_METHOD.DELETE, ...filter, @@ -185,7 +178,7 @@ describe('Mocketeer', () => { test('should create RestMock using DELETE method with filter and query as objects', () => { mocketeer.mockDELETE(filterWithQuery, mockResponse); - expect(RestMock).toHaveBeenCalledWith( + expect(Mock).toHaveBeenCalledWith( expect.objectContaining({ method: REST_METHOD.DELETE, ...filterWithQuery, @@ -197,7 +190,7 @@ describe('Mocketeer', () => { test('should create RestMock using DELETE method and filter as url string', () => { mocketeer.mockDELETE(url, mockResponse); - expect(RestMock).toHaveBeenCalledWith( + expect(Mock).toHaveBeenCalledWith( expect.objectContaining({ method: REST_METHOD.DELETE, ...filter, diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts index 08db9ea..9ac1215 100644 --- a/test/unit/utils.test.ts +++ b/test/unit/utils.test.ts @@ -2,133 +2,177 @@ import { requestToPlainObject, waitFor, addMockByPriority, + createRequestFilter, } from '../../src/utils'; import { createMockRequest, createRestMock } from './fixtures/request'; -describe('utils', () => { - describe('requestToPlainObject', () => { - test('returns serialized request object', () => { - const req = createMockRequest(); - req.postData.mockReturnValue(JSON.stringify({ foo: 'bar' })); - req.url.mockReturnValue( - 'http://example.com:8000/some/path?foo=bar#baz' - ); - req.method.mockReturnValue('GET'); - req.headers.mockReturnValue({ header: 'header' }); - req.resourceType.mockReturnValue('xhr'); - expect(requestToPlainObject(req)).toMatchObject({ - headers: { - header: 'header', - }, - hostname: 'http://example.com:8000', - method: 'GET', - path: '/some/path', - query: { - foo: 'bar', - }, - }); +describe('requestToPlainObject', () => { + test('returns serialized request object', () => { + const req = createMockRequest(); + req.postData.mockReturnValue(JSON.stringify({ foo: 'bar' })); + req.url.mockReturnValue( + 'http://example.com:8000/some/path?foo=bar#baz' + ); + req.method.mockReturnValue('GET'); + req.headers.mockReturnValue({ header: 'header' }); + req.resourceType.mockReturnValue('xhr'); + expect(requestToPlainObject(req)).toMatchObject({ + headers: { + header: 'header', + }, + hostname: 'http://example.com:8000', + method: 'GET', + path: '/some/path', + query: { + foo: 'bar', + }, }); + }); - test('returns only rawBody without body if postData() returns non-JSON string', () => { - const req = createMockRequest(); - req.postData.mockReturnValue('somestring'); + test('returns only rawBody without body if postData() returns non-JSON string', () => { + const req = createMockRequest(); + req.postData.mockReturnValue('somestring'); - expect(requestToPlainObject(req)).toMatchObject({ - body: undefined, - rawBody: 'somestring', - }); + expect(requestToPlainObject(req)).toMatchObject({ + body: undefined, + rawBody: 'somestring', }); + }); - test('returns correct path and url when origin contains trailing slash', () => { - const req = createMockRequest(); - req.url.mockReturnValue('http://origin:8000/some/path'); + test('returns correct path and url when origin contains trailing slash', () => { + const req = createMockRequest(); + req.url.mockReturnValue('http://origin:8000/some/path'); - expect(requestToPlainObject(req)).toMatchObject({ - url: 'http://origin:8000/some/path', - path: '/some/path', - }); + expect(requestToPlainObject(req)).toMatchObject({ + url: 'http://origin:8000/some/path', + path: '/some/path', }); }); +}); + +test('waitFor', async () => { + const now = Date.now(); + await expect(waitFor(() => true)).resolves.toEqual(undefined); + await expect(waitFor(() => Date.now() > now)).resolves.toEqual(undefined); + await expect(waitFor(() => Date.now() > now + 50)).resolves.toEqual( + undefined + ); + await expect(waitFor(() => Date.now() > now - 50)).resolves.toEqual( + undefined + ); +}); + +test('waitFor throws after 100ms by default', async () => { + expect.assertions(1); + const now = Date.now(); + try { + await waitFor(() => Date.now() > now + 150); + } catch (e) { + expect(e).not.toBeFalsy(); + } +}); - test('waitFor', async () => { - const now = Date.now(); - await expect(waitFor(() => true)).resolves.toEqual(undefined); - await expect(waitFor(() => Date.now() > now)).resolves.toEqual( - undefined +test('waitFor throws after provided timeout', async () => { + expect.assertions(1); + const now = Date.now(); + try { + await waitFor(() => Date.now() > now + 55, 50); + } catch (e) { + expect(e).not.toBeFalsy(); + } +}); + +describe('addMockByPriority', () => { + test('adds mocks with higher priority first', () => { + const mocks = [createRestMock()]; + const higherPriorityMock = createRestMock({}, { priority: 10 }); + + expect(addMockByPriority(mocks, higherPriorityMock)[0]).toBe( + higherPriorityMock ); - await expect(waitFor(() => Date.now() > now + 50)).resolves.toEqual( - undefined + }); + + test('adds mock in correct order basing on priority', () => { + const mocks = [createRestMock()]; + const higherPriorityMock = createRestMock({}, { priority: 10 }); + const middlePriorityMock = createRestMock({}, { priority: 5 }); + + expect(addMockByPriority(mocks, higherPriorityMock)[0]).toBe( + higherPriorityMock ); - await expect(waitFor(() => Date.now() > now - 50)).resolves.toEqual( - undefined + expect(addMockByPriority(mocks, middlePriorityMock)[1]).toBe( + middlePriorityMock ); }); - test('waitFor throws after 100ms by default', async () => { - expect.assertions(1); - const now = Date.now(); - try { - await waitFor(() => Date.now() > now + 150); - } catch (e) { - expect(e).not.toBeFalsy(); - } - }); + test('adds mock to end when mock has lowest priority', () => { + const mocks = [ + createRestMock({}, { priority: 10 }), + createRestMock({}, { priority: 5 }), + ]; + const lowestPriorityMock = createRestMock({}, { priority: 3 }); - test('waitFor throws after provided timeout', async () => { - expect.assertions(1); - const now = Date.now(); - try { - await waitFor(() => Date.now() > now + 55, 50); - } catch (e) { - expect(e).not.toBeFalsy(); - } + expect(addMockByPriority(mocks, lowestPriorityMock)[2]).toBe( + lowestPriorityMock + ); }); - describe('addMockByPriority', () => { - test('adds mocks with higher priority first', () => { - const mocks = [createRestMock()]; - const higherPriorityMock = createRestMock({}, { priority: 10 }); + test('adds mock before mock with same priority', () => { + const mocks = [ + createRestMock({}, { priority: 10 }), + createRestMock({}, { priority: 5 }), + ]; + const samePriorityMock = createRestMock({}, { priority: 5 }); - expect(addMockByPriority(mocks, higherPriorityMock)[0]).toBe( - higherPriorityMock - ); - }); + expect(addMockByPriority(mocks, samePriorityMock)[1]).toBe( + samePriorityMock + ); + }); +}); - test('adds mock in correct order basing on priority', () => { - const mocks = [createRestMock()]; - const higherPriorityMock = createRestMock({}, { priority: 10 }); - const middlePriorityMock = createRestMock({}, { priority: 5 }); - - expect(addMockByPriority(mocks, higherPriorityMock)[0]).toBe( - higherPriorityMock - ); - expect(addMockByPriority(mocks, middlePriorityMock)[1]).toBe( - middlePriorityMock - ); +describe('createRequestMatcher', () => { + test('return object when provided input as string', () => { + expect(createRequestFilter('http://example.com')).toEqual({ + url: 'http://example.com', }); + }); - test('adds mock to end when mock has lowest priority', () => { - const mocks = [ - createRestMock({}, { priority: 10 }), - createRestMock({}, { priority: 5 }), - ]; - const lowestPriorityMock = createRestMock({}, { priority: 3 }); - - expect(addMockByPriority(mocks, lowestPriorityMock)[2]).toBe( - lowestPriorityMock - ); + test('return object when provided input as object', () => { + expect( + createRequestFilter({ + url: 'http://example.com', + method: 'POST', + }) + ).toEqual({ + url: 'http://example.com', + method: 'POST', }); + }); - test('adds mock before mock with same priority', () => { - const mocks = [ - createRestMock({}, { priority: 10 }), - createRestMock({}, { priority: 5 }), - ]; - const samePriorityMock = createRestMock({}, { priority: 5 }); + test('return expected object when provided defaults', () => { + expect( + createRequestFilter('http://example.com', { + method: 'POST', + }) + ).toEqual({ + method: 'POST', + url: 'http://example.com', + }); - expect(addMockByPriority(mocks, samePriorityMock)[1]).toBe( - samePriorityMock - ); + expect( + createRequestFilter( + { + url: 'http://foo.com', + query: { param: 'value' }, + }, + { + method: 'POST', + } + ) + ).toEqual({ + method: 'POST', + url: 'http://foo.com', + query: { param: 'value' }, }); }); }); diff --git a/yarn.lock b/yarn.lock index 093fb6c..9785838 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6245,10 +6245,10 @@ typedarray@^0.0.6: resolved "https://nexus.hltech.dev/repository/npm/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^3.4.1: - version "3.4.5" - resolved "https://nexus.hltech.dev/repository/npm/typescript/-/typescript-3.4.5.tgz#2d2618d10bb566572b8d7aad5180d84257d70a99" - integrity sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw== +typescript@^3.6.3: + version "3.6.3" + resolved "https://nexus.hltech.dev/repository/npm/typescript/-/typescript-3.6.3.tgz#fea942fabb20f7e1ca7164ff626f1a9f3f70b4da" + integrity sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw== uglify-js@^3.1.4: version "3.5.15"