Skip to content

Commit

Permalink
make sure MSW loader only resolves when mocking is enabled
Browse files Browse the repository at this point in the history
  • Loading branch information
yannbf committed Jul 12, 2024
1 parent 8cd3038 commit edc76c3
Show file tree
Hide file tree
Showing 10 changed files with 84 additions and 55 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
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
51 changes: 27 additions & 24 deletions packages/msw-addon/src/augmentInitializeOptions.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,57 @@
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',
'sb-vite',
'/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
}
24 changes: 22 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,38 @@ 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

// scenario: MSW is registered and enabled
if (!!msw.context?.isMockingEnabled) {
return
}

// scenario: MSW is registered but not enabled yet
if (msw.context.activationPromise) {
return 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 {}
}

0 comments on commit edc76c3

Please sign in to comment.