mirror of
https://github.com/hanzoai/msw.git
synced 2026-09-24 17:00:34 +00:00
Response: Adopts functional composition of "res"
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
__*
|
||||
.DS_*
|
||||
node_modules
|
||||
lib
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"semi": false,
|
||||
"useTabs": false,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
### Problems of traditional mocking:
|
||||
|
||||
* Often relies on a mocking server which you need to run and maintain;
|
||||
* Doesn't really mock requests, rather **replaces** requests' urls, so they go to the mocking server, instead of the production server;
|
||||
* Brings extra dependencies to your application, instead of being a dependency-free development tool;
|
||||
- Often relies on a mocking server which you need to run and maintain;
|
||||
- Doesn't really mock requests, rather **replaces** requests' urls, so they go to the mocking server, instead of the production server;
|
||||
- Brings extra dependencies to your application, instead of being a dependency-free development tool;
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -23,22 +23,20 @@ import { MSW } from 'not-published-yet'
|
||||
const msw = new MSW()
|
||||
|
||||
/* Configure mocking routes */
|
||||
msw.get('https://api.github.com/repo/:repoName', (req, res) => {
|
||||
/* Access request's params */
|
||||
const { repoName } = req.params
|
||||
msw.get(
|
||||
'https://api.github.com/repo/:repoName',
|
||||
(req, res, { status, set, delay, json }) => {
|
||||
const { repoName } = req.params // acces request's params
|
||||
|
||||
res
|
||||
/* Set custom response status */
|
||||
.status(403)
|
||||
/* Set headers */
|
||||
.set({ 'Custom-Header': 'foo' })
|
||||
/* Delay the response */
|
||||
.delay(1000)
|
||||
/* Mock the body */
|
||||
.json({
|
||||
errorMessage: `Repository "${repoName}" not found`
|
||||
})
|
||||
})
|
||||
return res(
|
||||
status(403), // set custom response status
|
||||
set({ 'Custom-Header': 'foo' }), // set headers
|
||||
delay(1000), // delay the response
|
||||
json({
|
||||
errorMessage: `Repository "${repoName}" not found`,
|
||||
}),
|
||||
)
|
||||
)
|
||||
|
||||
/* Start the Service Worker */
|
||||
msw.start()
|
||||
|
||||
+28
-27
@@ -2,34 +2,35 @@ const { MSW } = MockServiceWorker
|
||||
|
||||
const msw = new MSW()
|
||||
|
||||
msw.get('https://github.com/user/:username', (req, res) => {
|
||||
console.log({req})
|
||||
res
|
||||
.status(301, 'Custom status text')
|
||||
.set({
|
||||
'Header-One': 'first',
|
||||
'Header-Two': 'second',
|
||||
})
|
||||
.json({
|
||||
...req.params,
|
||||
message: `This is not a GitHub API, but it may be.`,
|
||||
param: 'value'
|
||||
})
|
||||
})
|
||||
msw.get(
|
||||
'https://github.com/user/:username',
|
||||
(req, res, { status, set, json }) => {
|
||||
return res(
|
||||
status(301, 'Custom status text'),
|
||||
set({
|
||||
'Header-One': 'foo',
|
||||
'Header-Two': 'bar',
|
||||
}),
|
||||
json({
|
||||
message: 'This is not a GitHub API',
|
||||
username: req.params.username,
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
msw.post('https://github.com/repo/:repoName', (req, res) => {
|
||||
res
|
||||
.set('Custom-Header', 'value')
|
||||
.json({
|
||||
msw.post('https://github.com/repo/:repoName', (req, res, { set, json }) => {
|
||||
return res(
|
||||
set('Custom-Header', 'value'),
|
||||
json({
|
||||
repository: req.params.repoName,
|
||||
message: 'This repo is amazing'
|
||||
})
|
||||
message: 'This repo is amazing',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
msw.post('https://api.website.com', (req, res) => {
|
||||
res
|
||||
.delay(2000)
|
||||
.json({ message: 'Delayed response' })
|
||||
msw.post('https://api.website.com', (req, res, { json, delay }) => {
|
||||
return res(delay(2000), json({ message: 'Delayed response message' }))
|
||||
})
|
||||
|
||||
msw.start()
|
||||
@@ -39,10 +40,10 @@ msw.start()
|
||||
document.getElementById('btn').addEventListener('click', () => {
|
||||
fetch('https://github.com/user/kettanaito', {
|
||||
mode: 'no-cors',
|
||||
cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
|
||||
credentials: "same-origin", // include, *same-origin, omit
|
||||
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
|
||||
credentials: 'same-origin', // include, *same-origin, omit
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
+36
-38
@@ -4,19 +4,22 @@ self.addEventListener('install', (event) => {
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(self.clients.claim())
|
||||
console.log('%cMockServiceWorker is activated!', 'color:green;font-weight:bold;')
|
||||
console.log(
|
||||
'%cMockServiceWorker is activated!',
|
||||
'color:green;font-weight:bold;',
|
||||
)
|
||||
})
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
switch (event.data) {
|
||||
case 'mock-activate': {
|
||||
self.__mockActive = true
|
||||
break;
|
||||
break
|
||||
}
|
||||
|
||||
case 'mock-deactivate': {
|
||||
self.__mockActive = false
|
||||
break;
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -40,45 +43,40 @@ const sendMessageToClient = (client, message) => {
|
||||
self.addEventListener('fetch', async (event) => {
|
||||
const { clientId, request: req } = event
|
||||
|
||||
const defaultResponse = () => {
|
||||
return fetch(req)
|
||||
}
|
||||
const defaultResponse = () => fetch(req)
|
||||
|
||||
return event.respondWith(new Promise(async (resolve, reject) => {
|
||||
const client = await event.target.clients.get(clientId)
|
||||
if (!client || !self.__mockActive) {
|
||||
return resolve(defaultResponse())
|
||||
}
|
||||
event.respondWith(
|
||||
new Promise(async (resolve, reject) => {
|
||||
const client = await event.target.clients.get(clientId)
|
||||
if (!client || !self.__mockActive) {
|
||||
return resolve(defaultResponse())
|
||||
}
|
||||
|
||||
const reqHeaders = {}
|
||||
req.headers.forEach((value, name) => {
|
||||
reqHeaders[name] = value
|
||||
})
|
||||
const reqHeaders = {}
|
||||
req.headers.forEach((value, name) => {
|
||||
reqHeaders[name] = value
|
||||
})
|
||||
|
||||
const clientResponse = await sendMessageToClient(client, {
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
headers: reqHeaders,
|
||||
cache: req.cache,
|
||||
mode: req.mode,
|
||||
credentials: req.credentials,
|
||||
redirect: req.redirect,
|
||||
referrer: req.referrer,
|
||||
referrerPolicy: req.referrerPolicy
|
||||
})
|
||||
const clientResponse = await sendMessageToClient(client, {
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
headers: reqHeaders,
|
||||
cache: req.cache,
|
||||
mode: req.mode,
|
||||
credentials: req.credentials,
|
||||
redirect: req.redirect,
|
||||
referrer: req.referrer,
|
||||
referrerPolicy: req.referrerPolicy,
|
||||
})
|
||||
|
||||
if (clientResponse === 'not-found') {
|
||||
return resolve(defaultResponse())
|
||||
}
|
||||
if (clientResponse === 'not-found') {
|
||||
return resolve(defaultResponse())
|
||||
}
|
||||
|
||||
const res = JSON.parse(clientResponse)
|
||||
const { body, timeout, headers, statusCode, statusText } = res
|
||||
const mockedResponse = new Response(body, {
|
||||
headers,
|
||||
status: statusCode,
|
||||
statusText,
|
||||
})
|
||||
const res = JSON.parse(clientResponse)
|
||||
const mockedResponse = new Response(res.body, res)
|
||||
|
||||
setTimeout(resolve.bind(this, mockedResponse), timeout)
|
||||
}).catch(console.error))
|
||||
setTimeout(resolve.bind(this, mockedResponse), res.delay)
|
||||
}).catch(console.error),
|
||||
)
|
||||
})
|
||||
|
||||
Generated
+6
@@ -6594,6 +6594,12 @@
|
||||
"integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=",
|
||||
"dev": true
|
||||
},
|
||||
"prettier": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.15.2.tgz",
|
||||
"integrity": "sha512-YgPLFFA0CdKL4Eg2IHtUSjzj/BWgszDHiNQAe0VAIBse34148whfdzLagRL+QiKS+YfK5ftB6X4v/MBw8yCoug==",
|
||||
"dev": true
|
||||
},
|
||||
"pretty-format": {
|
||||
"version": "23.6.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"cross-env": "^5.2.0",
|
||||
"express": "^4.16.4",
|
||||
"jest": "^23.6.0",
|
||||
"prettier": "^1.15.2",
|
||||
"typescript": "^3.1.6",
|
||||
"webpack": "^4.25.1",
|
||||
"webpack-cli": "^3.1.2"
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import * as R from 'ramda'
|
||||
|
||||
/**
|
||||
* Sets the given header, or the Map of headers to the response.
|
||||
* @example
|
||||
* res(set('Content-Type', 'plain/text'))
|
||||
* res(set({ 'Content-Type': 'plain/text', 'My-Header': 'foo' }))
|
||||
*/
|
||||
export const set = (name: string | Object, value?: string) => {
|
||||
return R.ifElse(
|
||||
R.always(R.is(Object, name)),
|
||||
R.mergeDeepLeft({ headers: name }),
|
||||
R.assocPath(['headers', name], value),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a status code and optional status text of the response.
|
||||
* @example
|
||||
* res(status(301)) // status code only
|
||||
* res(status(200, 'Fine')) // with status code
|
||||
*/
|
||||
export const status = (statusCode: number, statusText?: string) => {
|
||||
return R.compose(
|
||||
R.assoc('status', statusCode),
|
||||
R.when(
|
||||
R.complement(R.always(R.isNil(statusText))),
|
||||
R.assoc('statusText', statusText),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the given text as the body of the response.
|
||||
* @example
|
||||
* res(text('Message'))
|
||||
*/
|
||||
export const text = (body: string) => {
|
||||
return R.compose(
|
||||
R.assoc('body', body),
|
||||
set('Content-Type', 'text/plain'),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the given XML as the body of the response.
|
||||
* @example
|
||||
* res(xml('<message>Foo</message>'))
|
||||
*/
|
||||
export const xml = (body: string) => {
|
||||
return R.compose(
|
||||
R.assoc('body', body),
|
||||
set('Content-Type', 'text/xml'),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the given Object as the JSON body of the response.
|
||||
* @example
|
||||
* res(json({ foo: 'bar' }))
|
||||
*/
|
||||
export const json = (body: Object) => {
|
||||
return R.compose(
|
||||
R.assoc('body', JSON.stringify(body)),
|
||||
set('Content-Type', 'application/json'),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delays the current response for the given duration (in ms)
|
||||
* @example
|
||||
* res(delay(1500), json({ foo: 'bar' }))
|
||||
*/
|
||||
export const delay = (duration: number) => {
|
||||
return R.assoc('delay', duration)
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import * as R from 'ramda'
|
||||
|
||||
export interface ResponseMock {
|
||||
readonly headers: Object
|
||||
readonly body: any
|
||||
readonly statusCode: number
|
||||
readonly statusText: string
|
||||
readonly timeout: number
|
||||
|
||||
set(name: string | Object, value?: any): ResponseMock
|
||||
status(statusCode: number, statusText: string): ResponseMock
|
||||
delay(duration: number): ResponseMock
|
||||
|
||||
text(body: string): ResponseMock
|
||||
json(body: Object): ResponseMock
|
||||
xml(body: string): ResponseMock
|
||||
}
|
||||
|
||||
const createRes = (): ResponseMock => ({
|
||||
headers: {
|
||||
Mocked: true,
|
||||
},
|
||||
body: null,
|
||||
statusCode: 200,
|
||||
statusText: 'OK',
|
||||
timeout: 0,
|
||||
set(name, value) {
|
||||
this.headers = R.ifElse(
|
||||
R.always(R.is(Object, name)),
|
||||
R.mergeDeepLeft(name),
|
||||
R.assoc(name, value)
|
||||
)(this.headers)
|
||||
|
||||
return this
|
||||
},
|
||||
status(statusCode, statusText) {
|
||||
this.statusCode = statusCode
|
||||
this.statusText = statusText
|
||||
return this
|
||||
},
|
||||
text(body) {
|
||||
this.body = body
|
||||
this.set('Content-Type', 'text/plain')
|
||||
return this
|
||||
},
|
||||
json(body) {
|
||||
this.body = JSON.stringify(body)
|
||||
this.set('Content-Type', 'application/json')
|
||||
return this
|
||||
},
|
||||
xml(body) {
|
||||
this.body = body
|
||||
this.set('Content-Type', 'text/xml')
|
||||
return this
|
||||
},
|
||||
delay(duration) {
|
||||
this.timeout = duration
|
||||
return this
|
||||
}
|
||||
})
|
||||
|
||||
export default createRes
|
||||
@@ -1,23 +0,0 @@
|
||||
import * as R from 'ramda'
|
||||
|
||||
export const response = (...handlers) => {
|
||||
return R.compose(...handlers)({
|
||||
status: 200,
|
||||
body: null,
|
||||
})
|
||||
}
|
||||
|
||||
export const json = (payload: Object) => (res) => {
|
||||
return {
|
||||
...res,
|
||||
body: JSON.stringify(payload),
|
||||
}
|
||||
}
|
||||
|
||||
export const status = (code: number, text: string) => (res) => {
|
||||
return {
|
||||
...res,
|
||||
status: code,
|
||||
statusText: text,
|
||||
}
|
||||
}
|
||||
+27
-19
@@ -1,6 +1,7 @@
|
||||
import * as R from 'ramda'
|
||||
import parseRoute from './utils/parseRoutes'
|
||||
import createRes, { ResponseMock } from './createRes'
|
||||
import res, { MockedResponse } from './response'
|
||||
import * as context from './context'
|
||||
|
||||
enum RESTMethod {
|
||||
get = 'get',
|
||||
@@ -9,9 +10,9 @@ enum RESTMethod {
|
||||
delete = 'delete',
|
||||
}
|
||||
|
||||
type Handler = (req: Request, res: ResponseMock) => void
|
||||
type Handler = (req: Request, res: MockedResponse) => void
|
||||
interface Routes {
|
||||
[method: string] : {
|
||||
[method: string]: {
|
||||
[route: string]: Handler
|
||||
}
|
||||
}
|
||||
@@ -46,28 +47,32 @@ export default class MockServiceWorker {
|
||||
const req = JSON.parse(event.data)
|
||||
const { method, url } = req
|
||||
const relevantRoutes = this.routes[method.toLowerCase()] || {}
|
||||
const relevantRoute = Object.keys(relevantRoutes)
|
||||
.reduce((acc, mask) => {
|
||||
const parsedRoute = parseRoute(mask, url)
|
||||
return parsedRoute.matches
|
||||
? {
|
||||
const relevantRoute = Object.keys(relevantRoutes).reduce((acc, mask) => {
|
||||
const parsedRoute = parseRoute(mask, url)
|
||||
return parsedRoute.matches
|
||||
? {
|
||||
handler: relevantRoutes[mask],
|
||||
parsedRoute,
|
||||
}
|
||||
: acc
|
||||
},
|
||||
null
|
||||
)
|
||||
: acc
|
||||
}, null)
|
||||
|
||||
if (relevantRoute === null) {
|
||||
return this.postMessage(event, 'not-found')
|
||||
}
|
||||
|
||||
const { handler, parsedRoute } = relevantRoute
|
||||
const res = createRes()
|
||||
handler({ ...req, params: parsedRoute.params }, res)
|
||||
const resolvedResponse =
|
||||
handler({ ...req, params: parsedRoute.params }, res, context) || {}
|
||||
|
||||
return this.postMessage(event, JSON.stringify(res))
|
||||
if (!resolvedResponse) {
|
||||
console.warn(
|
||||
'Expected a mocking handler function to return an Object, but got: %s. ',
|
||||
resolvedResponse,
|
||||
)
|
||||
}
|
||||
|
||||
return this.postMessage(event, JSON.stringify(resolvedResponse))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,11 +88,14 @@ export default class MockServiceWorker {
|
||||
}
|
||||
|
||||
if (!('serviceWorker' in navigator)) {
|
||||
console.error('Failed to start MockServiceWorker: Your current browser does not support Service Workers.')
|
||||
return void(null)
|
||||
console.error(
|
||||
'Failed to start MockServiceWorker: Your current browser does not support Service Workers.',
|
||||
)
|
||||
return void null
|
||||
}
|
||||
|
||||
navigator.serviceWorker.register(serviceWorkerPath, { scope: '/' })
|
||||
navigator.serviceWorker
|
||||
.register(serviceWorkerPath, { scope: '/' })
|
||||
.then((reg) => {
|
||||
const workerInstance = reg.active || reg.installing || reg.waiting
|
||||
|
||||
@@ -111,7 +119,7 @@ export default class MockServiceWorker {
|
||||
})
|
||||
}
|
||||
|
||||
addRoute = R.curry((method: RESTMethod, route:string, handler: Handler) => {
|
||||
addRoute = R.curry((method: RESTMethod, route: string, handler: Handler) => {
|
||||
this.routes = R.assocPath([method, route], handler, this.routes)
|
||||
return this
|
||||
})
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as R from 'ramda'
|
||||
|
||||
export interface MockedResponse {
|
||||
body: any
|
||||
status: number
|
||||
statusText: string
|
||||
headers: Object
|
||||
delay: number
|
||||
}
|
||||
|
||||
type ResponseTransformer = (res: MockedResponse) => MockedResponse
|
||||
|
||||
const defaultResponse: MockedResponse = {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
body: null,
|
||||
delay: 0,
|
||||
headers: {
|
||||
Mocked: true,
|
||||
},
|
||||
}
|
||||
|
||||
export default function response(...transformers: ResponseTransformer[]) {
|
||||
return R.pipe(...transformers)(defaultResponse)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import serialize from './serialize'
|
||||
|
||||
test('Serializes an object with methods', () => {
|
||||
const obj = {
|
||||
a: 2,
|
||||
first: () => 'foo',
|
||||
second() {
|
||||
return 'bar'
|
||||
}
|
||||
}
|
||||
|
||||
expect(serialize(obj)).toEqual('{"a":2,first:"() => \'foo\'",second:"function(){return \'bar\'"}')
|
||||
})
|
||||
@@ -1,11 +0,0 @@
|
||||
/**
|
||||
* Serializes the given Object and converts any of its methods
|
||||
* into strings in order to be deserialized back with "eval()".
|
||||
*/
|
||||
export default function serialize(obj: Object): string {
|
||||
return JSON.stringify(obj, (key, value) => {
|
||||
return typeof value === 'function'
|
||||
? `__FUNC__${value.toString()}`
|
||||
: value
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user