npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@web/mocks

v1.2.0

Published

MSW integration for @web tooling

Downloads

316

Readme

@web/mocks

MSW integration layer for usage with @web/dev-server, @web/test-runner and @web/dev-server-storybook.

Defining mocks

feature-a/demo/mocks.js:

import { http } from '@web/mocks/http.js';
import mocksFromAnotherFeature from 'another-feature/demo/mocks.js';

/**
 * Define mock scenarios
 */
export default {
  /**
   * Return an object from the handler
   */
  default: [http.get('/api/foo', context => Response.json({ foo: 'bar' }))],
  /**
   * Return native `Response` object from the handler
   */
  error: [http.get('/api/foo', context => new Response('', { status: 400 }))],
  /**
   * Handle additional custom logic in the handler, based on url, searchparams, whatever
   */
  custom: [
    /**
     * Customize based on searchParams
     */
    http.get('/api/users', ({ request }) => {
      const searchParams = new URL(request.url).searchParams;

      if (searchParams.get('user') === '123') {
        return Response.json({ id: '123', name: 'frank' });
      }

      return Response.json({ id: '456', name: 'bob' });
    }),

    /**
     * Customize based on params
     */
    http.get('/api/users/:id', ({ params }) => {
      if (params.id === '123') {
        return new Response('', { status: 400 });
      }

      return Response.json({ id: '456', name: 'bob' });
    }),

    /**
     * Customize based on cookies
     */
    http.get('/api/abtest', ({ cookies }) => {
      return Response.json({ abtest: cookies.segment === 'business' });
    }),
  ],
  /**
   * Provide an async fn, a fn returning an object, a fn returning a Response, or just an object
   */
  returnValues: [
    http.get('/api/foo', async context => Response.json({ foo: 'bar' })),
    http.get(
      '/api/foo',
      async context => new Response(JSON.stringify({ foo: 'bar' }), { status: 200 }),
    ),
    http.get('/api/foo', context => Response.json({ foo: 'bar' })),
    http.get('/api/foo', context => new Response(JSON.stringify({ foo: 'bar' }), { status: 200 })),
  ],
  importedMocks: [
    mocksFromAnotherFeature.default,
    http.get('/api/foo', () => Response.json({ foo: 'bar' })),
  ],
};

Context

The context object that gets passed to the handler includes:

http.get('/api/foo', ({ request, cookies, params }) => {
  return Response.json({ foo: 'bar' });
});
  • request the native Request object
  • cookies an object based on the request cookies
  • params an object based on the request params

@web/dev-server/@web/dev-server-storybook/@web/storybook-builder

feature-a/web-dev-server.config.mjs:

+// for Storybook 7+ (@web/storybook-builder)
+// no need to do anything here
+// this file is not even needed for "@web/storybook-builder"

import { storybookPlugin } from '@web/dev-server-storybook';

+// for Storybook 6 (@web/dev-server-storybook)
+import { mockPlugin } from '@web/mocks/plugins.js';

export default {
  nodeResolve: true,
  plugins: [
+    // for Storybook 6 (@web/dev-server-storybook)
+    mockPlugin(),
    storybookPlugin({ type: 'web-components' })
  ],
};

You can also add the mockRollupPlugin to your .storybook/main.cjs config for when you're bundling your Storybook to deploy somewhere; your mocks will be deployed along with your Storybook, and will work in whatever environment you deploy them to.

feature-a/.storybook/main.cjs:

module.exports = {
  stories: ['../stories/**/*.stories.{js,md,mdx}'],
+  // for Storybook 7+ (@web/storybook-builder)
+  // no need to do anything here

+  // for Storybook 6 (@web/dev-server-storybook)
+  rollupConfig: async config => {
+    const { mockRollupPlugin } = await import('@web/mocks/plugins.js');
+    config.plugins.push(mockRollupPlugin());
+    return config;
+  },
};
mockRollupPlugin({
  interceptor: `
    const domain = window.location.hostname;
    const apiDomain = "api." + domain;

    const { fetch: originalFetch } = window;

    async function fetch(request) {
      request = new Request(request);

      const resolvedURL = new URL(request.url, window.location);
      if (resolvedURL.hostname === apiDomain) {
        // rewrite hostname without api.
        resolvedURL.hostname = domain;

        const init = {};
        for (const property in request) {
          init[property] = request[property];
        }

        request = new Request(resolvedURL.href, init);
        if (request.body != null) {
          request = new Request(request, { body: await request.arrayBuffer() });
        }
      }
      return originalFetch(request);
    }

    window.fetch = fetch;
  `,
});

This can be used to avoid CORS issues when deploying your Storybooks.

In the Storybook 7+ (@web/storybook-builder) you can achieve the same by using native Storybook API previewHead:

// .storybook/main.js
/** @type { import('@web/storybook-framework-web-components').StorybookConfig } */
const config = {
  framework: {
    name: '@web/storybook-framework-web-components',
  },
  // ...
  previewHead(head) {
    return `
      ${process.env.NODE_ENV === 'production' ? `<script>${interceptor}</script>` : ''}
      ${head}
    `;
  },
};
export default config;

And add the addon: feature-a/.storybook/main.cjs:

module.exports = {
  stories: ['../stories/**/*.stories.{js,md,mdx}'],
+  // for Storybook 7+ (@web/storybook-builder)
+  addons: ['@web/mocks/storybook-addon'],

+  // for Storybook 6 (@web/dev-server-storybook)
+  addons: ['@web/mocks/storybook/addon.js'],
  rollupConfig: async config => {
    const { mockRollupPlugin } = await import('@web/mocks/plugins.js');
    config.plugins.push(mockRollupPlugin());
    return config;
  },
};

feature-a/.storybook/preview.js:

+// for Storybook 7+ (@web/storybook-builder)
+// no need to do anything here

+// for Storybook 6 (@web/dev-server-storybook)
+ import { withMocks } from '@web/mocks/storybook/decorator.js';
+ export const decorators = [withMocks];

feature-a/stories/default.stories.js:

import { html } from 'lit';
import { http } from '@web/mocks/http.js';
import mocks from '../demo/mocks.js';

export const Default = () => html`<feature-a></feature-a>`;
Default.story = {
  parameters: {
    mocks: mocks.default,
    // or
    mocks: [
      mocks.default,
      otherMocks.error,
      http.get('/api/bar', () => Response.json({ bar: 'bar' })),
      http.post('/api/baz', () => new Response('', { status: 400 })),
    ],
    // or
    mocks: [
      http.get('/api/users/:id', ({ params }) => {
        if (params.id === '123') {
          return Response.json({ name: 'frank' });
        }
        return Response.json({ name: 'bob' });
      }),
    ],
  },
};

@web/test-runner

The registerMockRoutes function will ensure the service worker is installed, and the mockPlugin takes care of resolving the service worker file, so users don't have to keep this one-time generated service worker file in their own project roots.

feature-a/web-test-runner.config.mjs:

import { mockPlugin } from '@web/mocks/plugins.js';

export default {
  nodeResolve: true,
  files: ['test/**/*.test.js'],
  plugins: [mockPlugin()],
};

feature-a/test/my-test.test.js:

import { registerMockRoutes } from '@web/mocks/browser.js';
import { http } from '@web/mocks/http.js';
import mocks from '../demo/mocks.js';
import featureBmocks from 'feature-b/demo/mocks.js';

describe('feature-a', () => {
  it('works', async () => {
    registerMockRoutes(http.get('/api/foo', () => Response.json({ foo: 'foo' })));

    const response = await fetch('/api/foo').then(r => r.json());
    expect(response.foo).to.equal('foo');
  });

  it('works', () => {
    registerMockRoutes(
      // Current project's mocks
      mocks.default, // is an array, arrays get flattened in the integration layer

      // Third party project's mocks, that uses a different version of MSW internally
      featureBmocks.default,

      // Additional mocks
      http.get('/api/baz', context => Response.json({ baz: 'baz' })),
    );
  });
});

Mocking requests in node.js

You can also mock requests in node.js:

import { registerMockRoutes } from '@web/mocks/node.js';
import { http } from '@web/mocks/http.js';

registerMockRoutes(
  http.get('/api/foo', () => new Response(JSON.stringify({ foo: 'bar' }), { status: 200 })),
);

const r = await fetch('/api/foo').then(r => r.json());
console.assert(r.foo === 'bar');

Rationale

Why not use MSW directly?

Large applications may have many features, that themself may depend on other features internally. Consider the following example:

feature-a uses feature-b internally. feature-a wants to reuse the mocks of feature-b, but the versions of msw are different.

import mocks from '../demo/mocks.js';
import featureBmocks from 'feature-b/demo/mocks.js';

const Default = () => html`<feature-a></feature-a>`; // uses `feature-b` internally
Default.story = {
  parameters: {
    mocks: [
      mocks.default, // uses [email protected]
      featureBmocks.default, // ❌ uses [email protected], incompatible mocks -> [email protected] may expect a different service worker, or different API!
    ],
  },
};

[email protected] may have a different API or it's service worker may expect a different message, event, or data format. In order to ensure forward compatibility, we expose a "middleman" function:

import { http } from '@web/mocks/http.js';

http.get('/api/foo', () => Response.json({ foo: 'bar' }));

The middleware function simply returns an object that looks like:

{
  method: 'get',
  endpoint: '/api/foo',
  handler: () => Response.json({foo: 'bar'})
}

This way we can support multiple versions of msw inside of our integration layer by acting as a bridge of sorts; the function people define mocks with doesn't directly depend on msw itself, it just creates an object with the information we need to pass on to msw.

That way, feature-a's project controls the dependency on msw (via the msw integration layer package), while still being able to use mocks from other projects that may use a different version of msw themself internally.

In the wrapper, we standardize on native Request and Response objects; the handler function receives a Request object, and returns a Response object. For utility, we also pass cookies and params, since those are often used to conditionally return mocks. This means that the wrapper function only depends on standard, browser-native JS, and itself has no other dependencies, which is a good foundation to ensure forward compatibility.

Requests/Responses

The Request and Response objects used are standard JS Request and Response objects. You can read more about them on MDN.