2023-03-12

How to integrate MSW queries mocking with @web/test-runner and StoryBook

In short, follow the comments in Pool Request

TLDR


The serious WebComponents and UI components development SDLC needs the TDD ability to create UI, visually test with incremental changes, apply the unit tests for functional parts, CI/CD integration for visual and functional regression. 

Those tasks are perfectly covered by 

  • StoryBook as component based UI development media. It allows to do the development per component detached from main application/library. In addition to development of single use case, it gives ability to change the component parameters, and its behavior in various external conditions like high contrast/dark/light display mode, blurred vision, etc. Which is a must during development of accessible UI components and components with parameter driven variations.  
  • @web/test-runner - the browser based unit test runner. The perfect combo of CLI and in-browser development. 
Both, a visual development in StoryBook, and functional unit testing need the data mocking which is provided by
  • MockServiceWorker - MSW helper for interrupting the back-end HTTP calls and simulating the response behavior with headers, data and errors.   
All sounds as a perfect combo till the moment trying to make a plumbing in real project. It happen that many nuances are not covered by docs and some aspects of MSW deployment strategies become impossible to resolve. Till now. 

MSW packaging includes CJS( node kind of modules ) and IILF (ES5 no-module) formats. In StoryBook it works out of the box as it is based on WebPack build which resolves CJS and bundles-in the MSW code. 
Unfortunately it become an issue in non-compile based environments including @web/test-runner. There is no es6 module which can be imported directly. 

The top-level await become a savior. IILF formatted script can be loaded by SCRIPT tag and its variables re-exported by es6 module. msw.js module source
const msw = await new Promise((resolve,reject)=>
{
    ((d, s)=>
    {   s = d.createElement('script')
        s.setAttribute('src','node_modules/msw/lib/iife/index.js');
        s.onload = ()=> resolve( window.MockServiceWorker );
        d.head.append(s)
    })(document)
});

export const    { GraphQLHandler
                , MockedRequest
                , RESTMethods
                , RequestHandler
                , RestHandler
                , SetupApi
                , SetupWorkerApi
                , cleanUrl
                , compose
                , context
                , createResponseComposition
                , defaultContext
                , defaultResponse
                , graphql
                , graphqlContext
                , handleRequest
                , matchRequestUrl
                , response
                , rest
                , restContext
                , setupWorker
                } = msw;
This module is used by unit test config and in mock handlers. To make it compatible with usual use by StoryBook compatible code, add the import maps support in web-test-runner.config.mjs
plugins:[
        importMapsPlugin({
      inject: {
        importMap: {
          imports: {
            'msw': '/src/mocks/msw.js',
          },
        },
      },
    }),
    ]
Which would make the import in StoryBook and in unit test alike. handlers.js:
import { rest } from 'msw'

import pokemonsMock from "../../stories/pokemons.mock";

export const handlers =
[   rest.get("*/api/v2/pokemon", (req, res, ctx) =>
    {
        return res(ctx.json(pokemonsMock));
    }),
    rest.get("*/noreturn", (req, res, ctx) =>
            {   console.log(req.url, 'trapped')
                return new Promise((resolve)=>{ setTimeout(()=>
                {   console.log(req.url, 'resolving')
                    resolve(res(ctx.json(pokemonsMock)))
                }, 10000)}); // 1 second to be able to catch the initial state before the full data returned;
            }),
]
The service worker needs a bit of tuning to be reused by StoryBook and unit tests. First comes initialization on root level:

npx msw init "./" --save

Then in .storybook/main.js
module.exports = {
    staticDirs: [
        {from: '../mockServiceWorker.js', to: '/mockServiceWorker.js'} // MSW support
    ],

}
and in .storybook/preview.js
import { initialize, mswDecorator } from "msw-storybook-addon";

import {handlers} from "../src/mocks/handlers";

export const parameters = {
    msw: {handlers},
};

// Initialize MSW
initialize();

// Provide the MSW addon decorator globally
export const decorators = [mswDecorator];
The unit test MSW setup is a bit trickier. Besides configuration of the import maps to simulated JS msw module above, in each test you would need to import browser.js
// src/mocks/browser.js
import { setupWorker } from './msw.js'
import { handlers } from './handlers.js'

// This configures a Service Worker with the given request handlers.
export const worker = setupWorker(...handlers)
await worker.start({ serviceWorker: { url: '/mockServiceWorker.js' } });
Hopefully that is sufficient. If not, take a look into notes on PR or bug me.

Happy coding!