Skip to content

Commit

Permalink
use msw instead of the dead endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
kentcdodds committed Aug 30, 2020
1 parent d80e077 commit ea3146e
Show file tree
Hide file tree
Showing 19 changed files with 974 additions and 472 deletions.
870 changes: 477 additions & 393 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@
"npm": ">=6"
},
"dependencies": {
"@kentcdodds/react-workshop-app": "^2.14.1",
"@kentcdodds/react-workshop-app": "^2.15.0",
"@testing-library/react": "^10.4.9",
"@testing-library/user-event": "^12.1.1",
"@testing-library/user-event": "^12.1.3",
"chalk": "^4.1.0",
"codegen.macro": "^4.0.0",
"msw": "^0.20.5",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-error-boundary": "^2.3.1",
Expand All @@ -26,7 +27,7 @@
"devDependencies": {
"husky": "^4.2.5",
"npm-run-all": "^4.1.5",
"prettier": "^2.0.5",
"prettier": "^2.1.1",
"react-scripts": "^4.0.0-next.77"
},
"scripts": {
Expand Down
Binary file added public/img/pokemon/bulbasaur.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/pokemon/charizard.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/pokemon/ditto.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/pokemon/mew.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/pokemon/mewtwo.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/pokemon/pikachu.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 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
}, {})
}
65 changes: 8 additions & 57 deletions src/__tests__/06.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,81 +4,32 @@ import userEvent from '@testing-library/user-event'
import App from '../final/06'
// import App from '../exercise/06'

beforeAll(() => {
window.fetch.mockImplementation(() =>
Promise.resolve({json: () => Promise.resolve({data: {pokemon: {}}})}),
)
})

function buildPokemon(overrides) {
return {
name: 'jeffry',
number: '777',
image: '/some/image.png',
attacks: {
special: [
{name: 'Super kick', type: 'Karate', damage: '122'},
{name: 'Pound it', type: 'Cool', damage: '323'},
],
},
...overrides,
}
}
beforeEach(() => jest.spyOn(window, 'fetch'))
afterEach(() => window.fetch.mockReset())

test('displays the pokemon', async () => {
const fakePokemon = buildPokemon()
window.fetch.mockImplementationOnce(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({data: {pokemon: fakePokemon}}),
}),
)
render(<App />)
const input = screen.getByLabelText(/pokemon/i)
const submit = screen.getByText(/^submit$/i)

// verify that an initial request is made when mounted
userEvent.type(input, fakePokemon.name)
userEvent.type(input, 'pikachu')
userEvent.click(submit)

await screen.findByRole('heading', {name: new RegExp(fakePokemon.name, 'i')})

expect(window.fetch).toHaveBeenCalledTimes(1)
expect(window.fetch).toHaveBeenCalledWith('https://graphql-pokemon.now.sh', {
method: 'POST',
headers: {'content-type': 'application/json;charset=UTF-8'},
// if this assertion fails, make sure that the pokemon name is being passed
body: expect.stringMatching(new RegExp(fakePokemon.name, 'i')),
})
window.fetch.mockClear()
await screen.findByRole('heading', {name: /pikachu/i})

// verify that a request is made when props change
const fakePokemon2 = buildPokemon({name: 'fred'})
window.fetch.mockImplementationOnce(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({data: {pokemon: fakePokemon2}}),
}),
)
userEvent.clear(input)
userEvent.type(input, fakePokemon2.name)
userEvent.type(input, 'ditto')
userEvent.click(submit)

await screen.findByRole('heading', {name: new RegExp(fakePokemon.name, 'i')})

expect(window.fetch).toHaveBeenCalledTimes(1)
expect(window.fetch).toHaveBeenCalledWith('https://graphql-pokemon.now.sh', {
method: 'POST',
headers: {'content-type': 'application/json;charset=UTF-8'},
// if this assertion fails, make sure that the pokemon name is being passed
body: expect.stringMatching(new RegExp(fakePokemon2.name, 'i')),
})
window.fetch.mockClear()
await screen.findByRole('heading', {name: /pikachu/i})

// verify that when props remain the same a request is not made
window.fetch.mockClear()
userEvent.click(submit)

await screen.findByRole('heading', {name: new RegExp(fakePokemon2.name, 'i')})
await screen.findByRole('heading', {name: /ditto/i})

expect(
window.fetch,
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import './styles.css'
import codegen from 'codegen.macro'
import './test/server'

codegen`module.exports = require('@kentcdodds/react-workshop-app/codegen')`
20 changes: 2 additions & 18 deletions src/pokemon.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,15 @@
import React from 'react'
import {ErrorBoundary} from 'react-error-boundary'

window.FETCH_TIME = undefined
window.MIN_FETCH_TIME = 500
window.FETCH_TIME_RANDOM = false

function sleep(t = window.FETCH_TIME) {
t = window.FETCH_TIME ?? t
if (window.FETCH_TIME_RANDOM) {
t = Math.random() * t + window.MIN_FETCH_TIME
}
if (process.env.NODE_ENV === 'test') {
t = 0
}
return new Promise(resolve => setTimeout(resolve, t))
}

const formatDate = date =>
`${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')} ${String(
date.getSeconds(),
).padStart(2, '0')}.${String(date.getMilliseconds()).padStart(3, '0')}`

// the delay argument is for faking things out a bit
function fetchPokemon(name, delay = 1500) {
const endTime = Date.now() + delay
const pokemonQuery = `
query ($name: String) {
query PokemonInfo($name: String) {
pokemon(name: $name) {
id
number
Expand All @@ -48,14 +32,14 @@ function fetchPokemon(name, delay = 1500) {
method: 'POST',
headers: {
'content-type': 'application/json;charset=UTF-8',
delay: delay,
},
body: JSON.stringify({
query: pokemonQuery,
variables: {name: name.toLowerCase()},
}),
})
.then(async response => {
await sleep(endTime - Date.now())
const {data} = await response.json()
if (response.ok) {
const pokemon = data?.pokemon
Expand Down
Loading

0 comments on commit ea3146e

Please sign in to comment.