Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Make sure MSW loader is resolved only when mocking is enabled #157

Merged
merged 5 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading