mswjs / msw-storybook-addon

Mock API requests in Storybook with Mock Service Worker.
https://msw-sb.vercel.app
MIT License
408 stars 39 forks source link

Story scoped handlers are not working in another files on version 2.0.1 or newer #154

Closed adrianpiw closed 5 months ago

adrianpiw commented 5 months ago

The problem:

I have globally defined handlers in preview file and they work as expected, then I have a story file, that has it's own interceptors, it's basically the same api endpoint as in globals but for particular story I want to have different response. This is working perfectly fine in storybook itself, but when I import the story to test file, it ignores the story specific msw handlers, and global ones are used instead.

When I'm importing a Story component in different file, I'm expecting to have this particular stories applied too.

Downgrading package to 2.0.0 resolves this issue and it's having unexpected behaviour on version 2.0.1 and 2.0.2

In below example, during the test being executed, I'd expect a value of api response to be "story" which means, it's interceptor comes form the story itself, instead it's equal to "handlers" which means, the story interceptor is not used, it's the global one from handlers.

Stack:

Packages:

    "@storybook/addon-essentials": "^8.1.9",
    "@storybook/addon-interactions": "^8.1.9",
    "@storybook/addon-links": "^8.1.9",
    "@storybook/addon-onboarding": "^8.1.9",
    "@storybook/blocks": "^8.1.9",
    "@storybook/react": "^8.1.9",
    "@storybook/react-vite": "^8.1.9",
    "@storybook/test": "^8.1.9",
    "@testing-library/dom": "^10.1.0",
    "@testing-library/jest-dom": "^6.4.6",
    "@testing-library/react": "^16.0.0",
    "@types/react": "^18.3.3",
    "@types/react-dom": "^18.3.0",
    "@typescript-eslint/eslint-plugin": "^7.2.0",
    "@typescript-eslint/parser": "^7.2.0",
    "@vitejs/plugin-react": "^4.2.1",
    "jsdom": "^24.1.0",
    "msw": "^2.3.1",
    "msw-storybook-addon": "^2.0.0",
    "storybook": "^8.1.9",
    "typescript": "^5.2.2",
    "vite": "^5.2.0",
    "vitest": "^1.6.0"
// handlers.ts
import { http, HttpResponse } from 'msw'

export const handlers = [
    http.get('/api/button', () => {
      return HttpResponse.json({
          text: 'handlers'
      })
    }),
  ]

Storybook previews file

// preview.tsx
import { handlers } from '../src/handlers';

initialize({});

const preview: Preview = {
  decorators: [
    (Story) => {
      const queryClient = new QueryClient()
        return (
          <QueryClientProvider client={queryClient}>
            <Story />
          </QueryClientProvider>
        )
      }
  ],
  parameters: {
    msw: {
      handlers: handlers
    }
  },
  loaders: [mswLoader],
};

Story file

// Button.stories.ts
import type { Meta, StoryObj } from '@storybook/react';
import { http, HttpResponse } from 'msw'

import Button from '../Button';

const meta: Meta<typeof Button> = {
  component: Button,
};

export default meta;
type Story = StoryObj<typeof Button>;

export const Default: Story = {
  parameters: {
    msw: {
      handlers: [
        http.get('/api/button', () => {
          return HttpResponse.json({
            text: 'story'
          })
        })
      ]
    }
  }
};

Button test file

// Button.test.tsx
import React from 'react'
import { composeStory } from "@storybook/react";
import { render, screen } from '@testing-library/react';
import { describe, expect, test } from "vitest";

import Meta, { Default as ButtonStory } from './stories/Button.stories';

const Story = composeStory(ButtonStory, Meta);

describe('button', () => {
    test('button should have text story', async () => {
        await Story.load();

        render(<Story />);
        const btn = await screen.findByTestId('btn');
        expect(btn.textContent).toBe("story");
    });
  });```

vitest file

// vitest.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/setupFile.ts'
  }
})

setup file

// setupFile.ts
import '@testing-library/dom';
import * as globalStorybookConfig from '../.storybook/preview';
import { setProjectAnnotations } from '@storybook/react';
import { afterEach, beforeAll, afterAll } from 'vitest';

import { server } from './server';

setProjectAnnotations(globalStorybookConfig);

beforeAll(() => {
    server.listen();
});

afterEach(() => {
    server.resetHandlers();
});

afterAll(() => {
    server.close();
});

server setup file

server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'

export const server = setupServer(...handlers)
yannbf commented 5 months ago

Hey there @maleficv, the issue you're facing is not related to the addon, but to the fact that you're setting up MSW yourself in the server.ts file:

server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'

export const server = setupServer(...handlers)

and you're telling that server to start listening during your tests.

beforeAll(() => {
    server.listen();
});

that is overwriting the behavior of the MSW instance from the addon.

If you don't use that at all, things should just work. You can get access to the MSW instance used by the addon via the getWorker function: import { getWorker } from 'msw-storybook-addon'

adrianpiw commented 5 months ago

Hey there @maleficv, the issue you're facing is not related to the addon, but to the fact that you're setting up MSW yourself in the server.ts file:

server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'

export const server = setupServer(...handlers)

and you're telling that server to start listening during your tests.

beforeAll(() => {
    server.listen();
});

that is overwriting the behavior of the MSW instance from the addon.

If you don't use that at all, things should just work. You can get access to the MSW instance used by the addon via the getWorker function: import { getWorker } from 'msw-storybook-addon'

I see, so the addon was spinning it's own instance of worker and I duplicated that, replacing the addon one manually.

That fixes an issue, thank you a lot