-
Notifications
You must be signed in to change notification settings - Fork 27k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0fc344e
commit 8dae5a7
Showing
11 changed files
with
383 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# Mock Service Worker Example | ||
|
||
[Mock Service Worker](https://github.com/mswjs/msw) is an API mocking library for browser and Node. It provides seamless mocking by interception of actual requests on the network level using Service Worker API. This makes your application unaware of any mocking being at place. | ||
|
||
In this example we integrate Mock Service Worker with Next by following the next steps: | ||
|
||
1. Define a set of [request handlers](./mocks/handlers.js) shared between client and server. | ||
1. Setup a [Service Worker instance](./mocks/browser.js) that would intercept all runtime client-side requests via `setupWorker` function. | ||
1. Setup a ["server" instance](./mocks/server.js) to intercept any server/build time requests (e.g. the one happening in `getInitialProps` or `getServerSideProps`) via `setupServer` function. | ||
|
||
## How to use | ||
|
||
### Using `create-next-app` | ||
|
||
Execute `create-next-app` with `Yarn` or `npx` to bootstrap the example: | ||
|
||
```bash | ||
npx create-next-app --example with-msw | ||
# or | ||
yarn create next-app --example with-msw | ||
``` | ||
|
||
### Download manually | ||
|
||
Download the example: | ||
|
||
```bash | ||
curl https://codeload.github.com/vercel/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-msw | ||
cd with-msw | ||
``` | ||
|
||
Install it and run: | ||
|
||
```bash | ||
npm install | ||
npm run dev | ||
# or | ||
yarn | ||
yarn dev | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { setupWorker } from 'msw' | ||
import { handlers } from './handlers' | ||
|
||
export const worker = setupWorker(...handlers) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { rest } from 'msw' | ||
|
||
export const handlers = [ | ||
rest.get('https://my.backend/book', (req, res, ctx) => { | ||
return res( | ||
ctx.json({ | ||
title: 'Lord of the Rings', | ||
imageUrl: '/book-cover.jpg', | ||
description: | ||
'The Lord of the Rings is an epic high-fantasy novel written by English author and scholar J. R. R. Tolkien.', | ||
}) | ||
) | ||
}), | ||
rest.get('/reviews', (req, res, ctx) => { | ||
return res( | ||
ctx.json([ | ||
{ | ||
id: '60333292-7ca1-4361-bf38-b6b43b90cb16', | ||
author: 'John Maverick', | ||
text: | ||
'Lord of The Rings, is with no absolute hesitation, my most favored and adored book by‑far. The triology is wonderful‑ and I really consider this a legendary fantasy series. It will always keep you at the edge of your seat‑ and the characters you will grow and fall in love with!', | ||
}, | ||
]) | ||
) | ||
}), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
if (typeof window === 'undefined') { | ||
const { server } = require('./server') | ||
server.listen() | ||
} else { | ||
const { worker } = require('./browser') | ||
worker.start() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { setupServer } from 'msw/node' | ||
import { handlers } from './handlers' | ||
|
||
export const server = setupServer(...handlers) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
module.exports = () => { | ||
return { | ||
env: { | ||
// Enable API mocking in development only. | ||
enableApiMocking: process.env.NODE_ENV === 'development', | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"name": "with-msw", | ||
"version": "1.0.0", | ||
"scripts": { | ||
"dev": "next", | ||
"build": "next build", | ||
"start": "next start" | ||
}, | ||
"license": "MIT", | ||
"dependencies": { | ||
"msw": "^0.20.4", | ||
"next": "latest", | ||
"react": "^16.13.1", | ||
"react-dom": "^16.13.1" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
if (process.env.enableApiMocking) { | ||
require('../mocks') | ||
} | ||
|
||
export default function App({ Component, pageProps }) { | ||
return <Component {...pageProps} /> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { useState } from 'react' | ||
|
||
export default function Home({ book }) { | ||
const [reviews, setReviews] = useState(null) | ||
|
||
const handleGetReviews = () => { | ||
// Client-side request are mocked by `mocks/browser.js`. | ||
fetch('/reviews') | ||
.then((res) => res.json()) | ||
.then(setReviews) | ||
} | ||
|
||
return ( | ||
<div> | ||
<img src={book.imageUrl} alt={book.title} width="250" /> | ||
<h1>{book.title}</h1> | ||
<p>{book.description}</p> | ||
<button onClick={handleGetReviews}>Load reviews</button> | ||
{reviews && ( | ||
<ul> | ||
{reviews.map((review) => ( | ||
<li key={review.id}> | ||
<p>{review.text}</p> | ||
<p>{review.author}</p> | ||
</li> | ||
))} | ||
</ul> | ||
)} | ||
</div> | ||
) | ||
} | ||
|
||
export async function getServerSideProps() { | ||
// Server-side requests are mocked by `mocks/server.js`. | ||
const res = await fetch('https://my.backend/book') | ||
const book = await res.json() | ||
|
||
return { | ||
props: { | ||
book, | ||
}, | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
/** | ||
* Mock Service Worker. | ||
* @see https://github.com/mswjs/msw | ||
* - Please do NOT modify this file. | ||
* - Please do NOT serve this file on production. | ||
*/ | ||
/* eslint-disable */ | ||
/* tslint:disable */ | ||
|
||
const INTEGRITY_CHECKSUM = 'ca2c3cd7453d8c614e2c19db63ede1a1' | ||
const bypassHeaderName = 'x-msw-bypass' | ||
|
||
let clients = {} | ||
|
||
self.addEventListener('install', function () { | ||
return self.skipWaiting() | ||
}) | ||
|
||
self.addEventListener('activate', async function (event) { | ||
return self.clients.claim() | ||
}) | ||
|
||
self.addEventListener('message', async function (event) { | ||
const clientId = event.source.id | ||
const client = await event.currentTarget.clients.get(clientId) | ||
const allClients = await self.clients.matchAll() | ||
const allClientIds = allClients.map((client) => client.id) | ||
|
||
switch (event.data) { | ||
case 'INTEGRITY_CHECK_REQUEST': { | ||
sendToClient(client, { | ||
type: 'INTEGRITY_CHECK_RESPONSE', | ||
payload: INTEGRITY_CHECKSUM, | ||
}) | ||
break | ||
} | ||
|
||
case 'MOCK_ACTIVATE': { | ||
clients = ensureKeys(allClientIds, clients) | ||
clients[clientId] = true | ||
|
||
sendToClient(client, { | ||
type: 'MOCKING_ENABLED', | ||
payload: true, | ||
}) | ||
break | ||
} | ||
|
||
case 'MOCK_DEACTIVATE': { | ||
clients = ensureKeys(allClientIds, clients) | ||
clients[clientId] = false | ||
break | ||
} | ||
|
||
case 'CLIENT_CLOSED': { | ||
const remainingClients = allClients.filter((client) => { | ||
return client.id !== clientId | ||
}) | ||
|
||
// Unregister itself when there are no more clients | ||
if (remainingClients.length === 0) { | ||
self.registration.unregister() | ||
} | ||
|
||
break | ||
} | ||
} | ||
}) | ||
|
||
self.addEventListener('fetch', async function (event) { | ||
const { clientId, request } = event | ||
const requestClone = request.clone() | ||
const getOriginalResponse = () => fetch(requestClone) | ||
|
||
// Opening the DevTools triggers the "only-if-cached" request | ||
// that cannot be handled by the worker. Bypass such requests. | ||
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { | ||
return | ||
} | ||
|
||
event.respondWith( | ||
new Promise(async (resolve, reject) => { | ||
const client = await event.target.clients.get(clientId) | ||
|
||
if ( | ||
// Bypass mocking when no clients active | ||
!client || | ||
// Bypass mocking if the current client has mocking disabled | ||
!clients[clientId] || | ||
// Bypass mocking for navigation requests | ||
request.mode === 'navigate' | ||
) { | ||
return resolve(getOriginalResponse()) | ||
} | ||
|
||
// Bypass requests with the explicit bypass header | ||
if (requestClone.headers.get(bypassHeaderName) === 'true') { | ||
const modifiedHeaders = serializeHeaders(requestClone.headers) | ||
// Remove the bypass header to comply with the CORS preflight check | ||
delete modifiedHeaders[bypassHeaderName] | ||
|
||
const originalRequest = new Request(requestClone, { | ||
headers: new Headers(modifiedHeaders), | ||
}) | ||
|
||
return resolve(fetch(originalRequest)) | ||
} | ||
|
||
const reqHeaders = serializeHeaders(request.headers) | ||
const body = await request.text() | ||
|
||
const rawClientMessage = await sendToClient(client, { | ||
type: 'REQUEST', | ||
payload: { | ||
url: request.url, | ||
method: request.method, | ||
headers: reqHeaders, | ||
cache: request.cache, | ||
mode: request.mode, | ||
credentials: request.credentials, | ||
destination: request.destination, | ||
integrity: request.integrity, | ||
redirect: request.redirect, | ||
referrer: request.referrer, | ||
referrerPolicy: request.referrerPolicy, | ||
body, | ||
bodyUsed: request.bodyUsed, | ||
keepalive: request.keepalive, | ||
}, | ||
}) | ||
|
||
const clientMessage = rawClientMessage | ||
|
||
switch (clientMessage.type) { | ||
case 'MOCK_SUCCESS': { | ||
setTimeout( | ||
resolve.bind(this, createResponse(clientMessage)), | ||
clientMessage.payload.delay | ||
) | ||
break | ||
} | ||
|
||
case 'MOCK_NOT_FOUND': { | ||
return resolve(getOriginalResponse()) | ||
} | ||
|
||
case 'NETWORK_ERROR': { | ||
const { name, message } = clientMessage.payload | ||
const networkError = new Error(message) | ||
networkError.name = name | ||
|
||
// Rejecting a request Promise emulates a network error. | ||
return reject(networkError) | ||
} | ||
|
||
case 'INTERNAL_ERROR': { | ||
const parsedBody = JSON.parse(clientMessage.payload.body) | ||
|
||
console.error( | ||
`\ | ||
[MSW] Request handler function for "%s %s" has thrown the following exception: | ||
${parsedBody.errorType}: ${parsedBody.message} | ||
(see more detailed error stack trace in the mocked response body) | ||
This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error. | ||
If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\ | ||
`, | ||
request.method, | ||
request.url | ||
) | ||
|
||
return resolve(createResponse(clientMessage)) | ||
} | ||
} | ||
}).catch((error) => { | ||
console.error( | ||
'[MSW] Failed to mock a "%s" request to "%s": %s', | ||
request.method, | ||
request.url, | ||
error | ||
) | ||
}) | ||
) | ||
}) | ||
|
||
function serializeHeaders(headers) { | ||
const reqHeaders = {} | ||
headers.forEach((value, name) => { | ||
reqHeaders[name] = reqHeaders[name] | ||
? [].concat(reqHeaders[name]).concat(value) | ||
: value | ||
}) | ||
return reqHeaders | ||
} | ||
|
||
function sendToClient(client, message) { | ||
return new Promise((resolve, reject) => { | ||
const channel = new MessageChannel() | ||
|
||
channel.port1.onmessage = (event) => { | ||
if (event.data && event.data.error) { | ||
reject(event.data.error) | ||
} else { | ||
resolve(event.data) | ||
} | ||
} | ||
|
||
client.postMessage(JSON.stringify(message), [channel.port2]) | ||
}) | ||
} | ||
|
||
function createResponse(clientMessage) { | ||
return new Response(clientMessage.payload.body, { | ||
...clientMessage.payload, | ||
headers: clientMessage.payload.headers, | ||
}) | ||
} | ||
|
||
function ensureKeys(keys, obj) { | ||
return Object.keys(obj).reduce((acc, key) => { | ||
if (keys.includes(key)) { | ||
acc[key] = obj[key] | ||
} | ||
|
||
return acc | ||
}, {}) | ||
} |