Skip to content

Commit

Permalink
Add MSW usage example
Browse files Browse the repository at this point in the history
  • Loading branch information
kettanaito committed Aug 11, 2020
1 parent 0fc344e commit 8dae5a7
Show file tree
Hide file tree
Showing 11 changed files with 383 additions and 0 deletions.
40 changes: 40 additions & 0 deletions examples/with-msw/README.md
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
```
4 changes: 4 additions & 0 deletions examples/with-msw/mocks/browser.js
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)
26 changes: 26 additions & 0 deletions examples/with-msw/mocks/handlers.js
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!',
},
])
)
}),
]
7 changes: 7 additions & 0 deletions examples/with-msw/mocks/index.js
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()
}
4 changes: 4 additions & 0 deletions examples/with-msw/mocks/server.js
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)
8 changes: 8 additions & 0 deletions examples/with-msw/next.config.js
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',
},
}
}
16 changes: 16 additions & 0 deletions examples/with-msw/package.json
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"
}
}
7 changes: 7 additions & 0 deletions examples/with-msw/pages/_app.js
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} />
}
43 changes: 43 additions & 0 deletions examples/with-msw/pages/index.js
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,
},
}
}
Binary file added examples/with-msw/public/book-cover.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
228 changes: 228 additions & 0 deletions examples/with-msw/public/mockServiceWorker.js
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
}, {})
}

0 comments on commit 8dae5a7

Please sign in to comment.