Response: Adopts functional composition of "res"

This commit is contained in:
Artem Zakharchenko
2018-11-18 14:18:28 +01:00
parent 05889ac75a
commit e7a694e0f6
15 changed files with 227 additions and 211 deletions
+1
View File
@@ -1,3 +1,4 @@
__*
.DS_*
node_modules
lib
+8
View File
@@ -0,0 +1,8 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"semi": false,
"useTabs": false,
"trailingComma": "all",
"singleQuote": true
}
+3
View File
@@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}
+16 -18
View File
@@ -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
View File
@@ -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
View File
@@ -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),
)
})
+6
View File
@@ -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",
+1
View File
@@ -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"
+76
View File
@@ -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)
}
-62
View File
@@ -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
-23
View File
@@ -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
View File
@@ -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
})
+25
View File
@@ -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)
}
-13
View File
@@ -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\'"}')
})
-11
View File
@@ -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
})
}