Skip to content

Commit

Permalink
Fix: Make sure MSW loader is resolved only when mocking is enabled (#157
Browse files Browse the repository at this point in the history
)

* upgrade storybook and vitest dependencies

* make sure MSW loader only resolves when mocking is enabled

* add more passthrough urls

* update msw file

* cleanup
  • Loading branch information
yannbf authored Jul 12, 2024
1 parent 1ed70e3 commit 7758e8f
Show file tree
Hide file tree
Showing 13 changed files with 1,909 additions and 2,329 deletions.
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,11 +257,11 @@ initialize({}, [

#### Using the addon in Node.js with Portable Stories

If you're using [portable stories](https://storybook.js.org/docs/writing-tests/stories-in-unit-tests), you need to make sure the MSW loaders are applied correctly.
If you're using [portable stories](https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest), you need to make sure the MSW loaders are applied correctly.

### Storybook 8
### Storybook 8.2 or higher

You do so by calling the `load` function of your story before rendering it:
If you [set up the project annotations](https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations) correctly, by calling the `play` function of your story, the MSW loaders will be applied automatically:

```ts
import { composeStories } from '@storybook/react'
Expand All @@ -270,13 +270,12 @@ import * as stories from './MyComponent.stories'
const { Success } = composeStories(stories)

test('<Success />', async() => {
// 👇 Crucial step, so that the MSW loaders are applied
await Success.load()
render(<Success />)
// The MSW loaders are applied automatically via the play function
await Success.play()
})
```

### Storybook 7
### Storybook < 8.2

You do so by calling the `applyRequestHandlers` helper before rendering your story:

Expand Down
8 changes: 5 additions & 3 deletions packages/docs/.storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { initialize, mswLoader } from 'msw-storybook-addon';

import '../src/styles.css';

initialize();

const preview: Preview = {
// beforeAll is available in Storybook 8.2. Else the call would happen outside of the preview object
beforeAll: async() => {
initialize();
},
loaders: mswLoader,
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
},
loaders: [mswLoader],
};

export default preview;
Expand Down
35 changes: 19 additions & 16 deletions packages/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,31 +40,34 @@
},
"devDependencies": {
"@rollup/plugin-replace": "^5.0.5",
"@storybook/addon-a11y": "^8.0.0",
"@storybook/addon-actions": "^8.0.0",
"@storybook/addon-essentials": "^8.0.0",
"@storybook/addon-links": "^8.0.0",
"@storybook/addon-mdx-gfm": "^8.0.0",
"@storybook/node-logger": "^8.0.0",
"@storybook/preset-create-react-app": "^8.0.0",
"@storybook/react": "^8.0.0",
"@storybook/react-vite": "^8.0.0",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.1",
"@storybook/addon-a11y": "^8.2.1",
"@storybook/addon-actions": "^8.2.1",
"@storybook/addon-essentials": "^8.2.1",
"@storybook/addon-links": "^8.2.1",
"@storybook/addon-mdx-gfm": "^8.2.1",
"@storybook/node-logger": "^8.2.1",
"@storybook/preset-create-react-app": "^8.2.1",
"@storybook/react": "^8.2.1",
"@storybook/react-vite": "^8.2.1",
"@testing-library/dom": "^10.3.1",
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^4.2.1",
"chromatic": "^11.0.3",
"jsdom": "^23.0.0",
"msw": "^2.0.9",
"storybook": "^8.0.0",
"msw": "^2.3.1",
"storybook": "^8.2.1",
"typescript": "^5.3.2",
"vite": "^5.2.11",
"vitest": "^1.6.0"
"vitest": "^2.0.2"
},
"msw": {
"workerDirectory": "public"
"workerDirectory": [
"public"
]
}
}
2 changes: 1 addition & 1 deletion packages/docs/public/mockServiceWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* - Please do NOT serve this file on production.
*/

const PACKAGE_VERSION = '2.2.14'
const PACKAGE_VERSION = '2.3.1'
const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
Expand Down
7 changes: 4 additions & 3 deletions packages/docs/src/demos/fetch/AddonOnNode.test.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
/**
* @jest-environment jsdom
* @vitest-environment jsdom
*/
import { render, screen } from '@testing-library/react'
import { composeStories, setProjectAnnotations } from '@storybook/react'
import { describe, afterAll, it, expect } from 'vitest'
import { describe, afterAll, it, expect, beforeAll } from 'vitest'

import { getWorker, applyRequestHandlers } from 'msw-storybook-addon'
import * as stories from './App.stories'
import projectAnnotations from '../../../.storybook/preview'

setProjectAnnotations(projectAnnotations)
const annotations = setProjectAnnotations(projectAnnotations)

const { MockedSuccess, MockedError } = composeStories(stories)

// Useful in scenarios where the addon runs on node, such as with portable stories
describe('Running msw-addon on node', () => {
beforeAll(annotations.beforeAll!)
afterAll(() => {
// @ts-expect-error TS(2339): Property 'close' does not exist on type 'SetupWork... Remove this comment to see the full error message
getWorker().close()
Expand Down
10 changes: 5 additions & 5 deletions packages/msw-addon/src/applyRequestHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import type { RequestHandler } from 'msw'
import { api } from '@build-time/initialize'
import type { Context } from './decorator.js'
import { deprecate } from './util.js';
import { deprecate } from './util.js'

const deprecateMessage = deprecate(`
const deprecateMessage = deprecate(`
[msw-storybook-addon] You are using parameters.msw as an Array instead of an Object with a property "handlers". This usage is deprecated and will be removed in the next release. Please use the Object syntax instead.
More info: https://github.com/mswjs/msw-storybook-addon/blob/main/MIGRATION.md#parametersmsw-array-notation-deprecated-in-favor-of-object-notation
`)

// P.S. this is used by Storybook 7 users as a way to help them migrate.
// P.S. this is publicly exported as it is used by Storybook 7 users as a way to help them migrate.
// This should be removed from the package exports in a future release.
export function applyRequestHandlers(
handlersListOrObject: Context['parameters']['msw']
): void {
api?.resetHandlers();
api?.resetHandlers()

if (handlersListOrObject == null) {
return
}
Expand Down
54 changes: 30 additions & 24 deletions packages/msw-addon/src/augmentInitializeOptions.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,60 @@
import { InitializeOptions } from "./initialize.js";
import { InitializeOptions } from './initialize.js'

const fileExtensionPattern = /\.(js|jsx|ts|tsx|mjs|woff|woff2|ttf|otf|eot)$/;
const fileExtensionPattern = /\.(js|jsx|ts|tsx|mjs|woff|woff2|ttf|otf|eot)$/
const filteredURLSubstrings = [
"sb-common-assets",
"node_modules",
"node-modules",
"hot-update.json",
"__webpack_hmr",
"sb-vite",
];
'sb-common-assets',
'node_modules',
'node-modules',
'hot-update.json',
'__webpack_hmr',
'iframe.html',
'sb-vite',
'@vite',
'@react-refresh',
'/virtual:',
'.stories.',
'.mdx',
]

const shouldFilterUrl = (url: string) => {
// files which are mostly noise from webpack/vite builders + font files
if (fileExtensionPattern.test(url)) {
return true;
return true
}

const isStorybookRequest = filteredURLSubstrings.some((substring) =>
url.includes(substring)
);
)

if (isStorybookRequest) {
return true;
return true
}

return false;
};
return false
}

export const augmentInitializeOptions = (options: InitializeOptions) => {
if (typeof options?.onUnhandledRequest === "string") {
return options;
if (typeof options?.onUnhandledRequest === 'string') {
return options
}

return {
...options,
// Filter requests that we know are not relevant to the user e.g. HMR, builder requests, statics assets, etc.
onUnhandledRequest: (...args) => {
const [{ url }, print] = args;
const [{ url }, print] = args
if (shouldFilterUrl(url)) {
return;
return
}

if (!options?.onUnhandledRequest) {
print.warning();
return;
print.warning()
return
}

if (typeof options?.onUnhandledRequest === "function") {
options.onUnhandledRequest(...args);
if (typeof options?.onUnhandledRequest === 'function') {
options.onUnhandledRequest(...args)
}
},
} as InitializeOptions;
};
} as InitializeOptions
}
15 changes: 13 additions & 2 deletions packages/msw-addon/src/initialize.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,29 @@ type SetupWorker = ReturnType<typeof setupWorker>

export let api: SetupWorker

type ContextfulWorker = SetupWorker & {
context: { isMockingEnabled: boolean; activationPromise?: any }
}

export type InitializeOptions = Parameters<SetupWorker['start']>[0]

export function initialize(
options?: InitializeOptions,
initialHandlers: RequestHandler[] = []
): SetupWorker {
const worker = setupWorker(...initialHandlers)
worker.start(augmentInitializeOptions(options))
const worker = setupWorker(...initialHandlers) as ContextfulWorker
worker.context.activationPromise = worker.start(
augmentInitializeOptions(options)
)
api = worker
return worker
}

export async function waitForMswReady() {
const msw = getWorker() as ContextfulWorker
await msw.context.activationPromise
}

export function getWorker(): SetupWorker {
if (api === undefined) {
throw new Error(
Expand Down
4 changes: 3 additions & 1 deletion packages/msw-addon/src/initialize.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ export declare function initialize(
initialHandlers?: RequestHandler[]
): SetupApi<LifeCycleEventsMap>

export declare function getWorker(): typeof api
export declare function waitForMswReady(): Promise<void>

export declare function getWorker(): typeof api
5 changes: 5 additions & 0 deletions packages/msw-addon/src/initialize.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export function initialize(
return server
}

export async function waitForMswReady() {
// in Node MSW is activated instantly upon registration. Still we need to check the presence of the worker
getWorker()
}

export function getWorker(): SetupServer {
if (api === undefined) {
throw new Error(
Expand Down
5 changes: 5 additions & 0 deletions packages/msw-addon/src/initialize.react-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export function initialize(
return server
}

export async function waitForMswReady() {
// in Node MSW is activated instantly upon registration. Still we need to check the presence of the worker
getWorker()
}

export function getWorker(): SetupServer {
if (api === undefined) {
throw new Error(
Expand Down
12 changes: 2 additions & 10 deletions packages/msw-addon/src/loader.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import { waitForMswReady } from '@build-time/initialize'
import type { Context } from './decorator.js'
import { applyRequestHandlers } from './applyRequestHandlers.js'

export const mswLoader = async (context: Context) => {
await waitForMswReady()
applyRequestHandlers(context.parameters.msw)

if (
typeof window !== 'undefined' &&
'navigator' in window &&
navigator.serviceWorker?.controller
) {
// No need to rely on the MSW Promise exactly
// since only 1 worker can control 1 scope at a time.
await navigator.serviceWorker.ready
}

return {}
}
Loading

0 comments on commit 7758e8f

Please sign in to comment.