Fix MSW and storybook setup (#2976)

* Fix MSW and storybook setup

* Fix

* Fix

* Fixes

* Fix

* Fix

* Fix
This commit is contained in:
Charles Bochet
2023-12-13 14:37:55 +01:00
committed by GitHub
parent 34b5bfc34f
commit 9182efc57a
6 changed files with 260 additions and 280 deletions

View File

@ -1,12 +1,9 @@
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.7/iframeResizer.contentWindow.min.js"></script>
<!-- <script>
window.global = window;
</script> -->
<style type="text/css">
body {

View File

@ -1,7 +1,7 @@
import { ThemeProvider } from '@emotion/react';
import { Preview, ReactRenderer } from '@storybook/react';
import { withThemeFromJSXProvider } from "@storybook/addon-themes";
import { initialize, mswLoader } from 'msw-storybook-addon';
import { initialize, mswDecorator } from 'msw-storybook-addon';
import { RootDecorator } from '../src/testing/decorators/RootDecorator';
import { mockedUserJWT } from '../src/testing/mock-data/jwt';
@ -9,7 +9,20 @@ import { mockedUserJWT } from '../src/testing/mock-data/jwt';
import { lightTheme, darkTheme } from '../src/modules/ui/theme/constants/theme';
import 'react-loading-skeleton/dist/skeleton.css';
initialize({ onUnhandledRequest: 'bypass' });
initialize({
onUnhandledRequest: async (request: Request) => {
const fileExtensionsToIgnore = /\.(ts|tsx|js|jsx|svg|css|png)(\?v=[a-zA-Z0-9]+)?/;
if (fileExtensionsToIgnore.test(request.url)) {
return;
}
const requestBody = await request.json();
console.warn(`Unhandled ${request.method} request to ${request.url}
with payload ${JSON.stringify(requestBody)}\n
This request should be mocked with MSW`);
},
});
const preview: Preview = {
decorators: [
@ -22,6 +35,7 @@ const preview: Preview = {
Provider: ThemeProvider,
}),
RootDecorator,
mswDecorator,
],
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
@ -39,7 +53,6 @@ const preview: Preview = {
cookie: {
tokenPair: `{%22accessToken%22:{%22token%22:%22${mockedUserJWT}%22%2C%22expiresAt%22:%222023-07-18T15:06:40.704Z%22%2C%22__typename%22:%22AuthToken%22}%2C%22refreshToken%22:{%22token%22:%22${mockedUserJWT}%22%2C%22expiresAt%22:%222023-10-15T15:06:41.558Z%22%2C%22__typename%22:%22AuthToken%22}%2C%22__typename%22:%22AuthTokenPair%22}`,
},
loaders: [mswLoader],
},
};

View File

@ -145,8 +145,8 @@
"http-server": "^14.1.1",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"msw": "1.3.2",
"msw-storybook-addon": "^1.10.0",
"msw": "^2.0.11",
"msw-storybook-addon": "2.0.0--canary.122.b3ed3b1.0",
"prettier": "^3.1.0",
"storybook": "^7.6.3",
"storybook-addon-cookie": "^3.1.0",

View File

@ -2,13 +2,14 @@
/* tslint:disable */
/**
* Mock Service Worker (1.3.2).
* Mock Service Worker (2.0.11).
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/
const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70'
const INTEGRITY_CHECKSUM = 'c5f7f8e188b673ea4e677df7ea3c5a39'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
self.addEventListener('install', function () {
@ -86,12 +87,6 @@ self.addEventListener('message', async function (event) {
self.addEventListener('fetch', function (event) {
const { request } = event
const accept = request.headers.get('accept') || ''
// Bypass server-sent events.
if (accept.includes('text/event-stream')) {
return
}
// Bypass navigation requests.
if (request.mode === 'navigate') {
@ -112,29 +107,8 @@ self.addEventListener('fetch', function (event) {
}
// Generate unique request ID.
const requestId = Math.random().toString(16).slice(2)
event.respondWith(
handleRequest(event, requestId).catch((error) => {
if (error.name === 'NetworkError') {
console.warn(
'[MSW] Successfully emulated a network error for the "%s %s" request.',
request.method,
request.url,
)
return
}
// At this point, any exception indicates an issue with the original request/response.
console.error(
`\
[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
request.method,
request.url,
`${error.name}: ${error.message}`,
)
}),
)
const requestId = crypto.randomUUID()
event.respondWith(handleRequest(event, requestId))
})
async function handleRequest(event, requestId) {
@ -146,21 +120,24 @@ async function handleRequest(event, requestId) {
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
;(async function () {
const clonedResponse = response.clone()
sendToClient(client, {
type: 'RESPONSE',
payload: {
requestId,
type: clonedResponse.type,
ok: clonedResponse.ok,
status: clonedResponse.status,
statusText: clonedResponse.statusText,
body:
clonedResponse.body === null ? null : await clonedResponse.text(),
headers: Object.fromEntries(clonedResponse.headers.entries()),
redirected: clonedResponse.redirected,
const responseClone = response.clone()
sendToClient(
client,
{
type: 'RESPONSE',
payload: {
requestId,
isMockedResponse: IS_MOCKED_RESPONSE in response,
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
body: responseClone.body,
headers: Object.fromEntries(responseClone.headers.entries()),
},
},
})
[responseClone.body],
)
})()
}
@ -196,20 +173,20 @@ async function resolveMainClient(event) {
async function getResponse(event, client, requestId) {
const { request } = event
const clonedRequest = request.clone()
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const requestClone = request.clone()
function passthrough() {
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const headers = Object.fromEntries(clonedRequest.headers.entries())
const headers = Object.fromEntries(requestClone.headers.entries())
// Remove MSW-specific request headers so the bypassed requests
// comply with the server's CORS preflight check.
// Operate with the headers as an object because request "Headers"
// are immutable.
delete headers['x-msw-bypass']
// Remove internal MSW request header so the passthrough request
// complies with any potential CORS preflight checks on the server.
// Some servers forbid unknown request headers.
delete headers['x-msw-intention']
return fetch(clonedRequest, { headers })
return fetch(requestClone, { headers })
}
// Bypass mocking when the client is not active.
@ -227,31 +204,36 @@ async function getResponse(event, client, requestId) {
// Bypass requests with the explicit bypass header.
// Such requests can be issued by "ctx.fetch()".
if (request.headers.get('x-msw-bypass') === 'true') {
const mswIntention = request.headers.get('x-msw-intention')
if (['bypass', 'passthrough'].includes(mswIntention)) {
return passthrough()
}
// Notify the client that a request has been intercepted.
const clientMessage = await sendToClient(client, {
type: 'REQUEST',
payload: {
id: requestId,
url: request.url,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
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: await request.text(),
bodyUsed: request.bodyUsed,
keepalive: request.keepalive,
const requestBuffer = await request.arrayBuffer()
const clientMessage = await sendToClient(
client,
{
type: 'REQUEST',
payload: {
id: requestId,
url: request.url,
mode: request.mode,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: requestBuffer,
keepalive: request.keepalive,
},
},
})
[requestBuffer],
)
switch (clientMessage.type) {
case 'MOCK_RESPONSE': {
@ -261,21 +243,12 @@ async function getResponse(event, client, requestId) {
case 'MOCK_NOT_FOUND': {
return passthrough()
}
case 'NETWORK_ERROR': {
const { name, message } = clientMessage.data
const networkError = new Error(message)
networkError.name = name
// Rejecting a "respondWith" promise emulates a network error.
throw networkError
}
}
return passthrough()
}
function sendToClient(client, message) {
function sendToClient(client, message, transferrables = []) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
@ -287,17 +260,28 @@ function sendToClient(client, message) {
resolve(event.data)
}
client.postMessage(message, [channel.port2])
})
}
function sleep(timeMs) {
return new Promise((resolve) => {
setTimeout(resolve, timeMs)
client.postMessage(
message,
[channel.port2].concat(transferrables.filter(Boolean)),
)
})
}
async function respondWithMock(response) {
await sleep(response.delay)
return new Response(response.body, response)
// Setting response status code to 0 is a no-op.
// However, when responding with a "Response.error()", the produced Response
// instance will have status code set to 0. Since it's not possible to create
// a Response instance with status code 0, handle that use-case separately.
if (response.status === 0) {
return Response.error()
}
const mockedResponse = new Response(response.body, response)
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
value: true,
enumerable: true,
})
return mockedResponse
}

View File

@ -1,16 +1,14 @@
import { getOperationName } from '@apollo/client/utilities';
import { graphql } from 'msw';
import { graphql, HttpResponse } from 'msw';
import { CREATE_EVENT } from '@/analytics/graphql/queries/createEvent';
import { GET_CLIENT_CONFIG } from '@/client-config/graphql/queries/getClientConfig';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { mockedActivities } from '~/testing/mock-data/activities';
import { mockedCompaniesData } from './mock-data/companies';
import { mockedObjectMetadataItems } from './mock-data/metadata';
import { mockedPeopleData } from './mock-data/people';
import { mockedUsersData } from './mock-data/users';
import { mockedViewFieldsData } from './mock-data/view-fields';
import { mockedViewsData } from './mock-data/views';
@ -20,53 +18,52 @@ const metadataGraphql = graphql.link(
export const graphqlMocks = {
handlers: [
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', (req, res, ctx) => {
return res(
ctx.data({
currentUser: mockedUsersData[0],
}),
);
}),
graphql.mutation(getOperationName(CREATE_EVENT) ?? '', (req, res, ctx) => {
return res(
ctx.data({
// graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
// return HttpResponse.json({
// data: {
// currentUser: mockedUsersData[0],
// },
// });
// }),
graphql.mutation(getOperationName(CREATE_EVENT) ?? '', () => {
return HttpResponse.json({
data: {
createEvent: { success: 1, __typename: 'Event' },
}),
);
},
});
}),
graphql.query(
getOperationName(GET_CLIENT_CONFIG) ?? '',
(req, res, ctx) => {
return res(
ctx.data({
clientConfig: {
signInPrefilled: true,
dataModelSettingsEnabled: true,
developersSettingsEnabled: true,
debugMode: false,
authProviders: { google: true, password: true, magicLink: false },
telemetry: { enabled: false, anonymizationEnabled: true },
support: {
supportDriver: 'front',
supportFrontChatId: null,
},
graphql.query(getOperationName(GET_CLIENT_CONFIG) ?? '', () => {
return HttpResponse.json({
data: {
clientConfig: {
signInPrefilled: true,
dataModelSettingsEnabled: true,
developersSettingsEnabled: true,
debugMode: false,
authProviders: { google: true, password: true, magicLink: false },
telemetry: { enabled: false, anonymizationEnabled: true },
support: {
supportDriver: 'front',
supportFrontChatId: null,
},
}),
);
},
),
},
},
});
}),
metadataGraphql.query(
getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? '',
(req, res, ctx) => {
return res(ctx.data({ objects: mockedObjectMetadataItems }));
() => {
return HttpResponse.json({
data: { objects: mockedObjectMetadataItems },
});
},
),
graphql.query('FindManyViews', (req, res, ctx) => {
const objectMetadataId = req.variables.filter.objectMetadataId.eq;
const viewType = req.variables.filter.type.eq;
graphql.query('FindManyViews', ({ variables }) => {
const objectMetadataId = variables.filter.objectMetadataId.eq;
const viewType = variables.filter.type.eq;
return res(
ctx.data({
return HttpResponse.json({
data: {
views: {
edges: mockedViewsData
.filter(
@ -85,14 +82,14 @@ export const graphqlMocks = {
endCursor: null,
},
},
}),
);
},
});
}),
graphql.query('FindManyViewFields', (req, res, ctx) => {
const viewId = req.variables.filter.view.eq;
graphql.query('FindManyViewFields', ({ variables }) => {
const viewId = variables.filter.view.eq;
return res(
ctx.data({
return HttpResponse.json({
data: {
viewFields: {
edges: mockedViewFieldsData
.filter((viewField) => viewField.viewId === viewId)
@ -107,12 +104,12 @@ export const graphqlMocks = {
endCursor: null,
},
},
}),
);
},
});
}),
graphql.query('FindManyCompanies', (req, res, ctx) => {
return res(
ctx.data({
graphql.query('FindManyCompanies', () => {
return HttpResponse.json({
data: {
companies: {
edges: mockedCompaniesData.map((company) => ({
node: company,
@ -125,12 +122,12 @@ export const graphqlMocks = {
endCursor: null,
},
},
}),
);
},
});
}),
graphql.query('FindManyPeople', (req, res, ctx) => {
return res(
ctx.data({
graphql.query('FindManyPeople', () => {
return HttpResponse.json({
data: {
people: {
edges: mockedPeopleData.map((person) => ({
node: person,
@ -143,12 +140,12 @@ export const graphqlMocks = {
endCursor: null,
},
},
}),
);
},
});
}),
graphql.query('FindManyActivities', (req, res, ctx) => {
return res(
ctx.data({
graphql.query('FindManyActivities', () => {
return HttpResponse.json({
data: {
activities: {
edges: mockedActivities.map((activities) => ({
node: activities,
@ -161,8 +158,8 @@ export const graphqlMocks = {
endCursor: null,
},
},
}),
);
},
});
}),
],
};