diff --git a/README.md b/README.md index c7f1fc4..5100e75 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ test('can create client', async () => { const mocketeer = Mocketeer.setup(page); // Set up a mock - const mock = await mocketeer.addRestMock( + const mock = await mocketeer.mockREST( { method: 'POST', path: '/api/user', @@ -64,7 +64,7 @@ import { Mocketeer } from '@hltech/mocketeer'; const mocketeer = Mocketeer.setup(page); // Set up a mock - const mock = await mocketeer.addRestMock( + const mock = await mocketeer.mockREST( { method: 'POST', path: '/api/user', @@ -133,7 +133,7 @@ const page = await browser.newPage(); await mocketeer.activate(page); ``` -#### .addRestMock(filter: RequestFilter, response: MockedResponse, options?): RestMock +#### .mockREST(filter: RequestFilter, response: MockedResponse, options?): RestMock Respond to xhr and fetch requests that match the `filter` with provided `response`. Pass query params through `query` argument in `filter` object or simply append text to `url` @@ -158,7 +158,7 @@ Newly created instance of `RestMock`. ###### Example ```typescript -mocketeer.addRestMock( +mocketeer.mockREST( { method: 'POST', url: '/api/clients', @@ -175,7 +175,7 @@ mocketeer.addRestMock( ###### Example with query passed as an argument ```typescript -mocketeer.addRestMock( +mocketeer.mockREST( { method: 'GET', url: '/api/clients', @@ -196,7 +196,7 @@ mocketeer.addRestMock( ###### Example with queryString appended to the url ```typescript -mocketeer.addRestMock( +mocketeer.mockREST( { method: 'GET', url: '/api/clients?city=Bristol&limit=10', @@ -210,6 +210,98 @@ mocketeer.addRestMock( ); ``` +#### .mockGET(filter: RequestMethodFilter | string, response: MockedResponse, options?): RestMock + +#### .mockPOST(filter: RequestMethodFilter | string, response: MockedResponse, options?): RestMock + +#### .mockPUT(filter: RequestMethodFilter | string, response: MockedResponse, options?): RestMock + +#### .mockDELETE(filter: RequestMethodFilter | string, response: MockedResponse, options?): RestMock + +Respond to xhr and fetch requests with adequate rest method that match the `filter` with provided `response`. +Pass `filter` as an object or as an `url` string. +Pass query params through `query` argument in `filter` object or simply append text to `url` + +###### Arguments + +- `filter` _(RequestMethodFilter)_ used to determine if request matches the mock + - `url: string` + - `query(optional): QueryObject` object literal which accepts strings and arrays of strings as values, transformed to queryString +- `filter` _(string)_ `url` used to determine if request matches the mock +- `response` _(MockedResponse)_ content of mocked response + - `status: number` + - `headers: object` + - `body: any` +- `options` _(object)_ optional config object + - `prority` _(number)_ when intercepted request matches multiple mock, mocketeer will use the one with highest priority + +###### Returns + +Newly created instance of `RestMock`. + +###### Example of GET request + +```typescript +mocketeer.mockGET( + { + url: '/api/clients', + }, + { + status: 201, + body: { + clientId: 12345, + }, + } +); +``` + +###### Example of GET request with query passed as an argument + +```typescript +mocketeer.mockGET( + { + url: '/api/clients', + query: { + city: 'Bristol', + limit: 10, + }, + }, + { + status: 200, + body: { + clientId: 12345, + }, + } +); +``` + +###### Example of GET request with queryString appended to the url + +```typescript +mocketeer.mockGET( + { + url: '/api/clients?city=Bristol&limit=10', + }, + { + status: 200, + body: { + clientId: 12345, + }, + } +); +``` + +###### Example of GET request with filter as a string with query + +```typescript +mocketeer.mockGET('/api/clients?city=Bristol&limit=10', { + status: 200, + body: { + clientId: 12345, + }, +}); +``` + --- ### RestMock @@ -239,7 +331,7 @@ Promise resolved with `MatchedRequest` - object representing request that matche ###### Example ```typescript -const createClientMock = mocketeer.addRestMock( +const createClientMock = mocketeer.mockREST( { method: 'POST', url: '/api/clients', diff --git a/src/mocketeer.ts b/src/mocketeer.ts index bec5181..9898e15 100644 --- a/src/mocketeer.ts +++ b/src/mocketeer.ts @@ -2,7 +2,13 @@ import { parse } from 'url'; import { Page, Request, ResourceType } from 'puppeteer'; import dbg from 'debug'; import { RestMock } from './rest-mock'; -import { MockedResponse, MockOptions, RequestFilter } from './types'; +import { + MockedResponse, + MockOptions, + RequestFilter, + RequestMethodFilter, + REST_METHOD, +} from './types'; import { printRequest, requestToPlainObject } from './utils'; const interceptedTypes: ResourceType[] = ['xhr', 'fetch']; @@ -41,6 +47,10 @@ export class Mocketeer { page.on('request', request => this.onRequest(request)); } + /* + * Use mockREST instead + * @deprecated + */ public addRestMock( filter: RequestFilter, response: MockedResponse, @@ -53,6 +63,78 @@ export class Mocketeer { return mock; } + public mockREST( + filter: RequestFilter, + response: MockedResponse, + options?: Partial + ): RestMock { + const mock = new RestMock(filter, response, { + ...options, + }); + this.mocks.push(mock); + return mock; + } + + public mockGET( + filter: RequestMethodFilter | string, + response: MockedResponse, + options?: Partial + ): RestMock { + const filterObject = + typeof filter === 'string' ? { url: filter } : filter; + + return this.mockREST( + { ...filterObject, method: REST_METHOD.GET }, + response, + options + ); + } + + public mockPOST( + filter: RequestMethodFilter | string, + response: MockedResponse, + options?: Partial + ): RestMock { + const filterObject = + typeof filter === 'string' ? { url: filter } : filter; + + return this.mockREST( + { ...filterObject, method: REST_METHOD.POST }, + response, + options + ); + } + + public mockPUT( + filter: RequestMethodFilter | string, + response: MockedResponse, + options?: Partial + ): RestMock { + const filterObject = + typeof filter === 'string' ? { url: filter } : filter; + + return this.mockREST( + { ...filterObject, method: REST_METHOD.PUT }, + response, + options + ); + } + + public mockDELETE( + filter: RequestMethodFilter | string, + response: MockedResponse, + options?: Partial + ): RestMock { + const filterObject = + typeof filter === 'string' ? { url: filter } : filter; + + return this.mockREST( + { ...filterObject, method: REST_METHOD.DELETE }, + response, + options + ); + } + public removeMock(mock: RestMock): RestMock | void { const index = this.mocks.indexOf(mock); if (index > -1) { diff --git a/src/types.ts b/src/types.ts index 15bd46a..428065d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,12 +2,15 @@ import { ResourceType } from 'puppeteer'; export type QueryObject = Record; -export interface RequestFilter { - method: string; +export interface RequestMethodFilter { url: string; query?: QueryObject; } +export interface RequestFilter extends RequestMethodFilter { + method: string; +} + export interface ParsedFilterRequest { method: string; hostname: string | undefined; @@ -44,3 +47,10 @@ export interface IMock { origin: string ): MockedResponse | null; } + +export enum REST_METHOD { + GET = 'GET', + POST = 'POST', + PUT = 'PUT', + DELETE = 'DELETE', +} diff --git a/test/integration/fixture/mock-fixtures.ts b/test/integration/fixture/mock-fixtures.ts index 1a344d8..89025b8 100644 --- a/test/integration/fixture/mock-fixtures.ts +++ b/test/integration/fixture/mock-fixtures.ts @@ -1,4 +1,15 @@ -import { RequestFilter } from '../../../src/types'; +import { RequestFilter, RequestMethodFilter } from '../../../src/types'; + +export const requestFoo: RequestMethodFilter = { + url: '/foo', +}; + +export const requestFooWithQuery: RequestMethodFilter = { + url: '/foo', + query: { + param: 'fooParam', + }, +}; export const requestGetFoo: RequestFilter = { method: 'GET', @@ -18,6 +29,16 @@ export const requestPostFoo: RequestFilter = { url: '/foo', }; +export const requestPutFoo: RequestFilter = { + method: 'PUT', + url: '/foo', +}; + +export const requestDeleteFoo: RequestFilter = { + method: 'DELETE', + url: '/foo', +}; + export const response200Empty = { status: 200, body: {}, diff --git a/test/integration/mocketeer.int.test.ts b/test/integration/mocketeer.int.test.ts index a524dd5..5eee81b 100644 --- a/test/integration/mocketeer.int.test.ts +++ b/test/integration/mocketeer.int.test.ts @@ -6,6 +6,10 @@ import { requestPostFoo, response200Empty, requestGetFooWithQuery, + requestFoo, + requestFooWithQuery, + requestPutFoo, + requestDeleteFoo, } from './fixture/mock-fixtures'; const PORT = 9000; @@ -40,53 +44,266 @@ describe('Mocketeer integration', () => { await page.close(); }); - it('mocks fetch GET request', async () => { - await mocketeer.addRestMock(requestGetFoo, response200Ok); + describe('mockREST', () => { + it('mocks fetch GET request', async () => { + await mocketeer.mockREST(requestGetFoo, response200Ok); - await page.evaluate(() => { - fetch('/foo') - .then(res => res.json()) - .then(data => (document.body.innerHTML = data.payload)); + await page.evaluate(() => { + fetch('/foo') + .then(res => res.json()) + .then(data => (document.body.innerHTML = data.payload)); + }); + + await page.waitFor(100); + expect(await page.evaluate(() => document.body.innerHTML)).toEqual( + response200Ok.body.payload + ); }); - await page.waitFor(100); - expect(await page.evaluate(() => document.body.innerHTML)).toEqual( - response200Ok.body.payload - ); - }); + it('mocks fetch GET request with query', async () => { + await mocketeer.mockREST(requestGetFooWithQuery, response200Ok); - it('mocks fetch GET request with query', async () => { - await mocketeer.addRestMock(requestGetFooWithQuery, response200Ok); + await page.evaluate(() => { + fetch('/foo?param=fooParam') + .then(res => res.json()) + .then(data => (document.body.innerHTML = data.payload)); + }); - await page.evaluate(() => { - fetch('/foo?param=fooParam') - .then(res => res.json()) - .then(data => (document.body.innerHTML = data.payload)); + await page.waitFor(100); + expect(await page.evaluate(() => document.body.innerHTML)).toEqual( + response200Ok.body.payload + ); }); - await page.waitFor(100); - expect(await page.evaluate(() => document.body.innerHTML)).toEqual( - response200Ok.body.payload - ); + it('mocks fetch POST request ', async () => { + await mocketeer.mockREST(requestPostFoo, response200Ok); + + await page.evaluate(() => { + fetch('/foo', { method: 'POST' }) + .then(res => res.json()) + .then(data => (document.body.innerHTML = data.payload)); + }); + + await page.waitFor(100); + expect(await page.evaluate(() => document.body.innerHTML)).toEqual( + response200Ok.body.payload + ); + }); + + it('mocks fetch PUT request ', async () => { + await mocketeer.mockREST(requestPutFoo, response200Ok); + + await page.evaluate(() => { + fetch('/foo', { method: 'PUT' }) + .then(res => res.json()) + .then(data => (document.body.innerHTML = data.payload)); + }); + + await page.waitFor(100); + expect(await page.evaluate(() => document.body.innerHTML)).toEqual( + response200Ok.body.payload + ); + }); + + it('mocks fetch DELETE request ', async () => { + await mocketeer.mockREST(requestDeleteFoo, response200Ok); + + await page.evaluate(() => { + fetch('/foo', { method: 'DELETE' }) + .then(res => res.json()) + .then(data => (document.body.innerHTML = data.payload)); + }); + + await page.waitFor(100); + expect(await page.evaluate(() => document.body.innerHTML)).toEqual( + response200Ok.body.payload + ); + }); }); - it('mocks fetch POST request ', async () => { - await mocketeer.addRestMock(requestPostFoo, response200Ok); + describe('mock http methods', () => { + it('mocks fetch GET request', async () => { + const mock = await mocketeer.mockGET(requestFoo, response200Empty); - await page.evaluate(() => { - fetch('/foo', { method: 'POST' }) - .then(res => res.json()) - .then(data => (document.body.innerHTML = data.payload)); + await page.evaluate(() => { + fetch('/foo', { + method: 'GET', + }); + }); + + await expect(mock.getRequest()).resolves.toMatchObject({ + method: 'GET', + }); }); - await page.waitFor(100); - expect(await page.evaluate(() => document.body.innerHTML)).toEqual( - response200Ok.body.payload - ); + it('mocks fetch GET request using filter as string with query', async () => { + await mocketeer.mockGET('/foo?param=fooParam', response200Ok); + + await page.evaluate(() => { + fetch('/foo?param=fooParam') + .then(res => res.json()) + .then(data => (document.body.innerHTML = data.payload)); + }); + + await page.waitFor(100); + expect(await page.evaluate(() => document.body.innerHTML)).toEqual( + response200Ok.body.payload + ); + }); + + it('mocks fetch GET request using filter object and query object', async () => { + await mocketeer.mockGET(requestFooWithQuery, response200Ok); + + await page.evaluate(() => { + fetch('/foo?param=fooParam') + .then(res => res.json()) + .then(data => (document.body.innerHTML = data.payload)); + }); + + await page.waitFor(100); + expect(await page.evaluate(() => document.body.innerHTML)).toEqual( + response200Ok.body.payload + ); + }); + + it('mocks fetch POST request', async () => { + const mock = await mocketeer.mockPOST(requestFoo, response200Empty); + + await page.evaluate(() => { + fetch('/foo', { + method: 'POST', + }); + }); + + await expect(mock.getRequest()).resolves.toMatchObject({ + method: 'POST', + }); + }); + + it('mocks fetch POST request using filter as string with query', async () => { + await mocketeer.mockPOST('/foo?param=fooParam', response200Ok); + + await page.evaluate(() => { + fetch('/foo?param=fooParam', { method: 'POST' }) + .then(res => res.json()) + .then(data => (document.body.innerHTML = data.payload)); + }); + + await page.waitFor(100); + expect(await page.evaluate(() => document.body.innerHTML)).toEqual( + response200Ok.body.payload + ); + }); + + it('mocks fetch POST request using filter object and query object', async () => { + await mocketeer.mockPOST(requestFooWithQuery, response200Ok); + + await page.evaluate(() => { + fetch('/foo?param=fooParam', { method: 'POST' }) + .then(res => res.json()) + .then(data => (document.body.innerHTML = data.payload)); + }); + + await page.waitFor(100); + expect(await page.evaluate(() => document.body.innerHTML)).toEqual( + response200Ok.body.payload + ); + }); + + it('mocks fetch PUT request', async () => { + const mock = await mocketeer.mockPUT(requestFoo, response200Empty); + + await page.evaluate(() => { + fetch('/foo', { + method: 'PUT', + }); + }); + + await expect(mock.getRequest()).resolves.toMatchObject({ + method: 'PUT', + }); + }); + + it('mocks fetch PUT request using filter as string with query', async () => { + await mocketeer.mockPUT('/foo?param=fooParam', response200Ok); + + await page.evaluate(() => { + fetch('/foo?param=fooParam', { method: 'PUT' }) + .then(res => res.json()) + .then(data => (document.body.innerHTML = data.payload)); + }); + + await page.waitFor(100); + expect(await page.evaluate(() => document.body.innerHTML)).toEqual( + response200Ok.body.payload + ); + }); + + it('mocks fetch PUT request using filter object and query object', async () => { + await mocketeer.mockPUT(requestFooWithQuery, response200Ok); + + await page.evaluate(() => { + fetch('/foo?param=fooParam', { method: 'PUT' }) + .then(res => res.json()) + .then(data => (document.body.innerHTML = data.payload)); + }); + + await page.waitFor(100); + expect(await page.evaluate(() => document.body.innerHTML)).toEqual( + response200Ok.body.payload + ); + }); + + it('mocks fetch DELETE request', async () => { + const mock = await mocketeer.mockDELETE( + requestFoo, + response200Empty + ); + + await page.evaluate(() => { + fetch('/foo', { + method: 'DELETE', + }); + }); + + await expect(mock.getRequest()).resolves.toMatchObject({ + method: 'DELETE', + }); + }); + + it('mocks fetch DELETE request using filter as string with query', async () => { + await mocketeer.mockDELETE('/foo?param=fooParam', response200Ok); + + await page.evaluate(() => { + fetch('/foo?param=fooParam', { method: 'DELETE' }) + .then(res => res.json()) + .then(data => (document.body.innerHTML = data.payload)); + }); + + await page.waitFor(100); + expect(await page.evaluate(() => document.body.innerHTML)).toEqual( + response200Ok.body.payload + ); + }); + + it('mocks fetch DELETE request using filter object and query object', async () => { + await mocketeer.mockDELETE(requestFooWithQuery, response200Ok); + + await page.evaluate(() => { + fetch('/foo?param=fooParam', { method: 'DELETE' }) + .then(res => res.json()) + .then(data => (document.body.innerHTML = data.payload)); + }); + + await page.waitFor(100); + expect(await page.evaluate(() => document.body.innerHTML)).toEqual( + response200Ok.body.payload + ); + }); }); it('mocks multiple requests', async () => { - await mocketeer.addRestMock(requestGetFoo, response200Empty); + await mocketeer.mockREST(requestGetFoo, response200Empty); await page.evaluate(() => { fetch('/foo').then(() => (document.body.innerHTML += '1')); @@ -100,7 +317,7 @@ describe('Mocketeer integration', () => { }); it('mocks response with status 500', async () => { - await mocketeer.addRestMock(requestGetFoo, { + await mocketeer.mockREST(requestGetFoo, { status: 500, body: {}, }); @@ -118,10 +335,7 @@ describe('Mocketeer integration', () => { }); it('records intercepted request', async () => { - const mock = await mocketeer.addRestMock( - requestPostFoo, - response200Empty - ); + const mock = await mocketeer.mockREST(requestPostFoo, response200Empty); await page.evaluate(() => { fetch('/foo', { @@ -146,10 +360,7 @@ describe('Mocketeer integration', () => { }); it('notifies when mock was called', async () => { - const mock = await mocketeer.addRestMock( - requestGetFoo, - response200Empty - ); + const mock = await mocketeer.mockREST(requestGetFoo, response200Empty); await page.evaluate(() => { setTimeout(() => { @@ -161,12 +372,9 @@ describe('Mocketeer integration', () => { }); it('can set priorities on mocks', async () => { - const mock = await mocketeer.addRestMock( - requestGetFoo, - response200Empty - ); + const mock = await mocketeer.mockREST(requestGetFoo, response200Empty); - const mockWithPriority = await mocketeer.addRestMock( + const mockWithPriority = await mocketeer.mockREST( requestGetFoo, response200Empty, { @@ -187,7 +395,7 @@ describe('Mocketeer integration', () => { }); it('can remove mock so it is no longer called', async () => { - const mock = await mocketeer.addRestMock(requestGetFoo, { + const mock = await mocketeer.mockREST(requestGetFoo, { status: 200, body: { id: 1 }, }); @@ -204,7 +412,7 @@ describe('Mocketeer integration', () => { mocketeer.removeMock(mock); - await mocketeer.addRestMock(requestGetFoo, { + await mocketeer.mockREST(requestGetFoo, { status: 200, body: { id: 2 }, }); @@ -221,10 +429,7 @@ describe('Mocketeer integration', () => { }); it('can inspect requests that are invoke asynchronously', async () => { - const mock = await mocketeer.addRestMock( - requestGetFoo, - response200Empty - ); + const mock = await mocketeer.mockREST(requestGetFoo, response200Empty); await page.evaluate(() => { document.body.innerHTML = 'content'; diff --git a/test/unit/mocketeer.test.ts b/test/unit/mocketeer.test.ts index 166e297..58293bb 100644 --- a/test/unit/mocketeer.test.ts +++ b/test/unit/mocketeer.test.ts @@ -1,6 +1,12 @@ -import { Mocketeer } from '../../src'; +import { + Mocketeer, + RequestMethodFilter, + REST_METHOD, + RestMock, +} from '../../src'; import { createMockPage } from './fixtures/page'; import { createMockRequest } from './fixtures/request'; +jest.mock('../../src/rest-mock'); describe('Mocketeer', () => { describe('.activate', () => { @@ -52,4 +58,153 @@ describe('Mocketeer', () => { expect(request.continue).toHaveBeenCalled(); }); }); + + describe('mock http methods', () => { + let mocketeer: Mocketeer; + const url = 'url'; + const filter: RequestMethodFilter = { url }; + const filterWithQuery: RequestMethodFilter = { + url, + query: { + param: 'fooParam', + }, + }; + const mockResponse = { status: 404, body: {} }; + + beforeEach(async () => { + mocketeer = new Mocketeer(); + }); + + test('should create RestMock using GET method and filter as object', () => { + mocketeer.mockGET(filter, mockResponse); + expect(RestMock).toHaveBeenCalledWith( + expect.objectContaining({ method: REST_METHOD.GET, ...filter }), + mockResponse, + expect.anything() + ); + }); + + test('should create RestMock using GET method with filter and query as objects', () => { + mocketeer.mockGET(filterWithQuery, mockResponse); + expect(RestMock).toHaveBeenCalledWith( + expect.objectContaining({ + method: REST_METHOD.GET, + ...filterWithQuery, + }), + mockResponse, + expect.anything() + ); + }); + + test('should create RestMock using GET method and filter as url string', () => { + mocketeer.mockGET(url, mockResponse); + expect(RestMock).toHaveBeenCalledWith( + expect.objectContaining({ method: REST_METHOD.GET, ...filter }), + mockResponse, + expect.anything() + ); + }); + + test('should create RestMock using POST method and filter as object', () => { + mocketeer.mockPOST(filter, mockResponse); + expect(RestMock).toHaveBeenCalledWith( + expect.objectContaining({ + method: REST_METHOD.POST, + ...filter, + }), + mockResponse, + expect.anything() + ); + }); + + test('should create RestMock using POST method with filter and query as objects', () => { + mocketeer.mockPOST(filterWithQuery, mockResponse); + expect(RestMock).toHaveBeenCalledWith( + expect.objectContaining({ + method: REST_METHOD.POST, + ...filterWithQuery, + }), + mockResponse, + expect.anything() + ); + }); + + test('should create RestMock using POST method and filter as url string', () => { + mocketeer.mockPOST(url, mockResponse); + expect(RestMock).toHaveBeenCalledWith( + expect.objectContaining({ + method: REST_METHOD.POST, + ...filter, + }), + mockResponse, + expect.anything() + ); + }); + + test('should create RestMock using PUT method and filter as object', () => { + mocketeer.mockPUT(filter, mockResponse); + expect(RestMock).toHaveBeenCalledWith( + expect.objectContaining({ method: REST_METHOD.PUT, ...filter }), + mockResponse, + expect.anything() + ); + }); + + test('should create RestMock using PUT method with filter and query as objects', () => { + mocketeer.mockPUT(filterWithQuery, mockResponse); + expect(RestMock).toHaveBeenCalledWith( + expect.objectContaining({ + method: REST_METHOD.PUT, + ...filterWithQuery, + }), + mockResponse, + expect.anything() + ); + }); + + test('should create RestMock using PUT method and filter as url string', () => { + mocketeer.mockPUT(url, mockResponse); + expect(RestMock).toHaveBeenCalledWith( + expect.objectContaining({ method: REST_METHOD.PUT, ...filter }), + mockResponse, + expect.anything() + ); + }); + + test('should create RestMock using DELETE method and filter as object', () => { + mocketeer.mockDELETE(filter, mockResponse); + expect(RestMock).toHaveBeenCalledWith( + expect.objectContaining({ + method: REST_METHOD.DELETE, + ...filter, + }), + mockResponse, + expect.anything() + ); + }); + + test('should create RestMock using DELETE method with filter and query as objects', () => { + mocketeer.mockDELETE(filterWithQuery, mockResponse); + expect(RestMock).toHaveBeenCalledWith( + expect.objectContaining({ + method: REST_METHOD.DELETE, + ...filterWithQuery, + }), + mockResponse, + expect.anything() + ); + }); + + test('should create RestMock using DELETE method and filter as url string', () => { + mocketeer.mockDELETE(url, mockResponse); + expect(RestMock).toHaveBeenCalledWith( + expect.objectContaining({ + method: REST_METHOD.DELETE, + ...filter, + }), + mockResponse, + expect.anything() + ); + }); + }); });