Skip to content

Instantly share code, notes, and snippets.

@pixelmattersdev
Last active April 15, 2023 06:17
Show Gist options
  • Save pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b to your computer and use it in GitHub Desktop.
Save pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b to your computer and use it in GitHub Desktop.
How to develop an offline Front-End app with mock data
git clone git@github.com:Pixelmatters/setup-project-demo.git project-demo-msw
cd project-demo-msw
npm install
mkdir src/components
touch src/components/users.tsx
export const Users: React.FC = () => {
return (
<div>
<h1>Users</h1>
<ul>
<li>User 1</li>
<li>User 2</li>
<li>User 3</li>
</ul>
</div>
)
}
touch src/components/users.stories.tsx
import { ComponentStory, ComponentMeta } from '@storybook/react'
import { Users } from './users'
export default {
title: 'Components/Users',
component: Users,
} as ComponentMeta<typeof Users>
const Template: ComponentStory<typeof Users> = (args) => <Users {...args} />
export const Default = Template.bind({})
import { useEffect, useState } from 'react'
type User = {
id: number
name: string
}
export const Users: React.FC = () => {
const [users, setUsers] = useState<User[]>([])
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then((res) => res.json())
.then((result: User[]) => {
setUsers(result)
})
.catch(console.error)
}, [])
return (
<div>
<h1>Users</h1>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
)
}
npm install msw@^0.36.0 --save-dev
npx msw init public/ --save
mkdir src/mocks
touch src/mocks/handlers.ts
import { rest } from 'msw'
export const handlers = [
rest.get('https://jsonplaceholder.typicode.com/users', (req, res, ctx) => {
return res(
ctx.delay(),
ctx.status(200),
ctx.json([
{ id: 1, name: 'Leanne Graham' },
{ id: 2, name: 'Ervin Howell' },
{ id: 3, name: 'Clementine Bauch' },
{ id: 4, name: 'Patricia Lebsack' },
{ id: 5, name: 'Chelsey Dietrich' },
{ id: 6, name: 'Mrs. Dennis Schulist' },
{ id: 7, name: 'Kurtis Weissnat' },
{ id: 8, name: 'Nicholas Runolfsdottir V' },
{ id: 9, name: 'Glenna Reichert' },
{ id: 10, name: 'Clementina DuBuque' },
])
)
}),
]
npm install msw-storybook-addon --save-dev
import { initialize, mswDecorator } from 'msw-storybook-addon'
import { handlers } from '../src/mocks/handlers'
import '../src/index.css'
initialize({ onUnhandledRequest: 'bypass' });
export const decorators = [mswDecorator];
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
msw: { handlers },
}
module.exports = {
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials"
],
"core": {
"builder": "storybook-builder-vite"
},
"staticDirs": ["../public"], // this line was added
}
npm run build-storybook
npx http-server ./storybook-static/
node_modules
.DS_Store
dist
dist-ssr
*.local
storybook-static # this line as added
touch src/mocks/browser.ts
import { setupWorker } from 'msw'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import { App } from './App'
async function prepare() {
if (process.env.NODE_ENV === 'development') {
const { worker } = await import('./mocks/browser')
return worker.start({ onUnhandledRequest: 'bypass' })
}
return Promise.resolve()
}
prepare()
.then(() => {
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
})
.catch(console.error)
import logo from './logo.svg'
import { Users } from './components/users'
import './App.css'
export const App: React.FC = () => {
return (
<div className="app">
<header className="app-header">
<img src={logo} className="app-logo" alt="logo" />
<p>Hello Vite + React!</p>
<Users />
</header>
</div>
)
}
touch src/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)
const { server } = require('./src/mocks/server')
// Establish API mocking before all tests.
beforeAll(() => server.listen())
// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => server.resetHandlers())
// Clean up after the tests are finished.
afterAll(() => server.close())
module.exports = {
preset: 'ts-jest',
testMatch: ['**/src/**/?(*.)+(spec|test).[jt]s?(x)'],
moduleNameMapper: {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/__mocks__/fileMock.js',
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
testEnvironment: 'jest-environment-jsdom',
setupFilesAfterEnv: ['./jest.setup.js'], // this line was added
}
● renders hello message
ReferenceError: fetch is not defined
10 |
11 | useEffect(() => {
> 12 | fetch('https://jsonplaceholder.typicode.com/users')
| ^
13 | .then((res) => res.json())
14 | .then((result: User[]) => {
15 | setUsers(result)
at src/components/users.tsx:12:5
at invokePassiveEffectCreate (node_modules/react-dom/cjs/react-dom.development.js:23487:20)
at HTMLUnknownElement.callCallback (node_modules/react-dom/cjs/react-dom.development.js:3945:14)
at HTMLUnknownElement.callTheUserObjectsOperation (node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30)
at innerInvokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:338:25)
at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:274:3)
at HTMLUnknownElementImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:221:9)
at HTMLUnknownElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:94:17)
at HTMLUnknownElement.dispatchEvent (node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:231:34)
at Object.invokeGuardedCallbackDev (node_modules/react-dom/cjs/react-dom.development.js:3994:16)
at invokeGuardedCallback (node_modules/react-dom/cjs/react-dom.development.js:4056:31)
at flushPassiveEffectsImpl (node_modules/react-dom/cjs/react-dom.development.js:23574:9)
at unstable_runWithPriority (node_modules/scheduler/cjs/scheduler.development.js:468:12)
at runWithPriority$1 (node_modules/react-dom/cjs/react-dom.development.js:11276:10)
at flushPassiveEffects (node_modules/react-dom/cjs/react-dom.development.js:23447:14)
at Object.<anonymous>.flushWork (node_modules/react-dom/cjs/react-dom-test-utils.development.js:992:10)
at act (node_modules/react-dom/cjs/react-dom-test-utils.development.js:1107:9)
at render (node_modules/@testing-library/react/dist/pure.js:97:26)
at Object.<anonymous> (src/App.test.tsx:6:9)
Test Suites: 1 failed, 1 passed, 2 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 0 total
Time: 2.175 s, estimated 3 s
Ran all test suites.
npm install whatwg-fetch --save-dev
require('whatwg-fetch') // this line was added
const { server } = require('./src/mocks/server')
// Establish API mocking before all tests.
beforeAll(() => server.listen())
// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => server.resetHandlers())
// Clean up after the tests are finished.
afterAll(() => server.close())
> demo-project@0.0.0 test
> jest
PASS src/demo.test.ts
PASS src/App.test.tsx
Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.856 s, estimated 3 s
Ran all test suites.
import '@testing-library/jest-dom'
import { render, screen } from '@testing-library/react'
import { App } from './App'
it('renders hello message', () => {
render(<App />)
expect(screen.getByText('Hello Vite + React!')).toBeInTheDocument()
expect(
screen.getByRole('heading', { level: 1, name: 'Users' })
).toBeInTheDocument()
expect(screen.getByRole('list')).toBeInTheDocument()
expect(screen.getAllByRole('listitem')).toHaveLength(10)
})
Unable to find an accessible element with the role "listitem"
// ...
export const Users: React.FC = () => {
const [isLoading, setIsLoading] = useState(false) // this line was added
const [users, setUsers] = useState<User[]>([])
useEffect(() => {
setIsLoading(true) // this line was added
fetch('https://jsonplaceholder.typicode.com/users')
.then((res) => res.json())
.then((result: User[]) => {
setUsers(result)
})
.catch(console.error)
.finally(() => {
setIsLoading(false) // this line was added
})
}, [])
return (
<div>
<h1>Users</h1>
{/* we conditionally render the loading or success UI */}
{isLoading ? (
<div>Loading...</div>
) : (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</div>
)
}
import '@testing-library/jest-dom'
import {
render,
screen,
waitForElementToBeRemoved, // this line was added
} from '@testing-library/react'
import { App } from './App'
it('renders hello message', async () => { // this line was updated
render(<App />)
expect(screen.getByText('Hello Vite + React!')).toBeInTheDocument()
expect(
screen.getByRole('heading', { level: 1, name: 'Users' })
).toBeInTheDocument()
const loading = screen.getByText('Loading...') // this line was added
await waitForElementToBeRemoved(loading) // this line was added
expect(screen.getByRole('list')).toBeInTheDocument()
expect(screen.getAllByRole('listitem')).toHaveLength(10)
})
❯ npm run serve
> demo-project@0.0.0 serve
> vite preview
> Local: http://localhost:4173/
> Network: use `--host` to expose
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
preview: { port: 3000 }, // this line was added
server: { port: 3000 }, // this line was added
})
❯ npm run serve
> demo-project@0.0.0 serve
> vite preview
> Local: http://localhost:3000/
> Network: use `--host` to expose
npm install cypress-msw-interceptor --save-dev
import './commands'
import 'cypress-msw-interceptor' // this line was added
describe('App', () => {
it('shows the users list', () => {
cy.interceptRequest(
'GET',
'https://jsonplaceholder.typicode.com/users',
(req, res, ctx) => {
return res(
ctx.delay(),
ctx.status(200),
ctx.json([
{ id: 1, name: 'Mock User 1' },
{ id: 2, name: 'Mock User 2' },
{ id: 3, name: 'Mock User 3' },
]),
)
},
)
cy.visit('/')
cy.findByRole('heading').should('have.text', 'Users')
cy.findByText('Loading...').should('exist')
cy.findByRole('list').should('exist')
cy.findAllByRole('listitem').should('have.length', 3)
})
})
import { rest } from 'msw' // this line was added
import './commands'
import 'cypress-msw-interceptor'
// this block was added
declare global {
namespace Cypress {
interface Chainable {
interceptRequest(
method: string,
url: string,
responseResolver: Parameters<typeof rest.get>[1],
alias?: string,
): Chainable<any>
}
}
}
npm run cypress:open
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment