shunnNet / nuxt-msw

Nuxt module for adding msw
https://www.npmjs.com/package/@crazydos/nuxt-msw
MIT License
13 stars 1 forks source link
msw nuxt-module nuxt3

nuxt-msw

nuxt-msw integrates MSW (Mock Service Worker) into a Nuxt project, allowing you to use it for API mocking during development. Most of the code can be directly shared with test mocks.

Features

Migrate: 0.x -> 1.x users

Thank you to all the users of version 0.x. I believe you won’t need to put in too much effort to upgrade to version 1.x.

The differences between 0.x and 1.x are as follows:

If you don't want upgrade to 1.x now, you can find the 0.x doc here: 0.2.x doc

Setup

To install the module to your Nuxt application:

npm install @crazydos/nuxt-msw msw --save-dev
export default defineNuxtConfig({
  modules: ['@crazydos/nuxt-msw'],
  msw : {
    // Options, see below
  }
})

Usage

You need to set up the MSW worker and server separately. When either one is set up, that side will start running. The setup location is in the ~/msw directory (by default), and you configure it through the corresponding files.

worker

To set up the worker, you need follow the MSW documentation to create a worker file.

# For example
npx msw init public --save

Next, you need to create a worker.{ts|js|mjs|cjs} file in the ~/msw directory. The worker file will be run in the Nuxt client plugin, which in browser context. Means you can use browser api and Nuxt composable in this file.

// ~/msw/worker.ts
import { http, HttpResponse } from 'msw'

export default defineNuxtMswWorkerOption(() => {
  const handlers = [
    // Intercept "GET /api/user" requests...
    http.get('/api/user', () => {
      // ...and respond to them using this JSON response.
      return HttpResponse.json({
        message: "Hello Worker!",
      })
    }),
  ]
  // You can access any browser api
  // window.location.href

  return {
    handlers,
    workerOptions: {
      // ...you can pass options to worker.start()
      // onUnhandledRequest: 'bypass',
    },
    onWorkerStarted(worker, nuxtApp) {
      // Module will setup worker when nuxt run client plugin
      // Means this function will be called after plugin call worker.start()

      nuxtApp.hook('app:mounted', () => {
        // const route = useRoute()
        // console.log(worker.listHandlers())
      })
    },

  }
})

You can now try to fetch the data from the worker.

<script setup>

onMounted(async () => {
  const res = await $fetch("/api/user")
  console.log(res) // { message: "Hello Worker!" }
})
</script>

server

The way to set up the server is similar to the worker. You need to create a server.{ts|js|mjs|cjs} file in the ~/msw directory.

The server file will be run in Node.js Nitro context. Because it is before NuxtApp created, you can not access NuxtApp and composable which access it. But you can access msw server and request (h3Event).

One more important thing is that, for your mock and nitro server handler work properly, you need to set baseURL in the server option. The baseURL must be same as your server listening address.

And, when mocking the server side request, you need include the baseURL in your handler's path.

// ~/msw/server.ts
import { http, HttpResponse } from 'msw'

export default defineNuxtMswServerOption(() => {
  // assume your server listening at http://localhost:3000
  const baseURL = "http://localhost:3000" 

  // composables that not related to NuxtApp can be used here, like: useRuntimeConfig

  const handlers = [
    // Intercept "GET http://localhost:3000/user" requests...
    http.get(baseURL + '/api/user', () => {
      // ...and respond to them using this JSON response.
      return HttpResponse.json({
        message: "Hello msw server!"
      })
    }),
  ]
  return {
    baseURL, // baseURL is required 
    handlers,
    serverOptions: {
      onUnhandledRequest: 'bypass',
    },

    onRequest(mswServer, h3Event) {
      // This funtion will be call when Nitro server "request" hook
      console.log('Hello from onRequest')
      mswServer.use(/*...*/)
    },

  }
})

After setting up the server, you can now try to fetch the data from the server.

<template>
  <h1>{{ data?.message }}</h1> 
  <!-- Hello msw server! -->
</template>
<script setup>
const { data, error } = await useFetch('/api/user')
</script>

The h3Event has some basic information about the request, such as event.path. If you need more, you can explicitly install h3, and use it to get more.

import { getQuery } from 'h3'

// ...
{
  // ....
  onRequest(mswServer, h3Event) {
    const query = useQuery(h3Event)
    // do something with query
  }
}

Module options

The option usage could be found at following.

[!NOTE]
It is important to understand that these options is build time config. So if you use environment variable like .env to decide these options, you need setup .env at nuxi build too.

export default defineNuxtConfig({
  modules: ['@crazydos/nuxt-msw'],
  msw: {
    /**
     * Whether to enable the module. Default to `true`
     */
    enable: true,
     /**
      * Path to the nuxt-msw runtime folder that include worker and server files.
      * default: `~/msw`
      */ 
    folderPath: "~/msw"

    /**
     * Should include nuxt layers msw settings or not
     * default: true
     */
    includeLayer?: boolean

    /**
     *
     * Enable unit test mode. default: false
     *
     * In unit test mode, the module run <folderPath>/unit.{ts,js,mjs,cjs} file which run in nodejs environment.
     *
     * Which means msw server will be used in this mode.
     */
    testUtils?: boolean
  },
})

How to Use in Tests

When performing unit tests with Nuxt, you might use @nuxt/test-utils with vitest.

vitest

If you are only using Vitest without the @nuxt/test-utils environment, you will need to manually set up MSW.

Vitest has an example how to use msw in unit test.

[!NOTE] In this scenario, if you have handlers set up in the Nuxt layer, you will need to export them for use.

@nuxt/test-utils

Let's talk about unit test part first.

Unit Tests

According to the Nuxt official documentation, if you need to test within the Nuxt context (to ensure composables and other features function correctly), you must set up the Nuxt environment.

To ensure that MSW works correctly in this situation, nuxt-msw provides a helper function for environment setup and configuring the MSW server.

First, you need enable testUtils option for nuxt-msw module. Suggest enable in vitest.config.ts.

// vitest.config.ts
import { defineVitestConfig } from '@nuxt/test-utils/config'

export default defineVitestConfig({
  // any custom Vitest config you require
  test: {
    // .....
    environmentOptions: {
      nuxt: {
        domEnvionment: 'happy-dom',
        host: 3000,
        overrides: {
          msw: {
            testUtils: true,
          },
        },
      },
    },
  },
})

Then, you can use setupNuxtMswServer to setup MSW server in unit test. (Note: can't run in setup files currently. So you need to run it in every test file.)

The component may look like this:

<script setup lang="ts">
const { data } = await useFetch(
  '/api/user', 
  { baseURL: 'http://localhost:3000' }
)
</script>

<template>
  <div id="name">{{ data.name }}</div>
</template>
// @vitest-environment nuxt
import { it, expect, afterEach } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import UserName from './UserName.vue'

import { setupNuxtMswServer } from '@crazydos/nuxt-msw/test-utils'
import { http, HttpResponse } from "msw"

const mswServer = await setupNuxtMswServer({
  handlers: [
    http.get(
      'http://localhost:3000/api/user', 
      () => HttpResponse.json({ name: 'msw server unit' })
    )
  ],
  serverOptions: {
    // onUnhandledRequest: 'bypass',
  },
})
afterEach(() => {
  // you can do anything with mswServer
  mswServer.resetHandlers()
})

it('displays message', async () => {
  // replace handler if needed
  // mswServer.use(
  //   http.get('/api/user', () => {
  //     return HttpResponse.json({
  //       name: 'msw server unit replaced',
  //     })
  //   })
  // )
  const component = await mountSuspended(UserName)

  expect(component.find('#name').text()).toBe('msw server unit')
})

Then you can run the test with vitest:

npx vitest
Note: baseURL in unit test

If you are using $fetch or useFetch with no baseURL set, and use path with no domain like /some/path. You can mock like following:

// for example
await useFetch('/api/user') 

// mock with baseURL
const baseURL = 'http://localhost:3000'
await setupNuxtMswServer({
  baseURL,
  handlers: [
    http.get(baseURL + '/api/user', () => {
      return HttpResponse.json({ name: 'msw server unit' })
    })
  ]
})

[!NOTE] baseURL here is just a valid domain, it is not necessary to match nuxt server address. But if you run a real server and want msw fallback to there, you need to set baseURL to the real server address.

E2E Tests

@nuxt/test-utils uses Playwright as the E2E test driver, and it actually runs the Nuxt server and browser in a separate process. This means we can't directly modify mock in test.

To modify mocks in this situation, you need to employ workarounds, such as changing the mocks through dynamic mock.

The way nuxt-msw runs in this situation is just like normal Nuxt application. So you can just use the same way to setup MSW server and worker.

// For example: dynamic mock
// Normal test case
test('normal mock', async ({ page, goto }) => {
  await goto('/', { waitUntil: 'hydration' })
  await expect(page.getByText('John')).toBeVisible()
})

// Alternative test case
test('test dynamic mock', async ({ page, goto }) => {
  // The server response still the same
  await goto('/?alternative=true', { waitUntil: 'hydration' })

  // Next, we click a button that make a request from client side
  await page.getByTestId('get-data-button').click()

  // We get a different response from same api endpoints
  await expect(page.getByText('Jane')).toBeVisible()
})

Note: $fetch and fetch in @nuxt/test-utils/e2e

@nuxt/test-utils/e2e provide $fetch and fetch to make request to nuxt server in e2e test. nuxt-msw will do nothing with these two functions becuase they are outside the Nuxt application. So use them as normal.

import { $fetch, fetch } from '@nuxt/test-utils/e2e'

const html = await $fetch('/')

Nuxt Layer

(Although I think the simplest way to share handlers is just share the handlers.) There may be some cases your teams want to share msw settings by Nuxt layer. nuxt-msw also support this.

nuxt-msw will merge the worker and server configurations at each level. Depending on the configuration options, they will either be merged or overwritten. Below is the behavior of each option:

// worker and server options
{
  handlers: [] // merge. the higher level take precedence

  workerOptions: {} // merge. the higher level take precedence
  serverOptions: {} // merge. the higher level take precedence

  baseURL: "" // overwrite. the higher level take precedence

  onWorkerStarted: () => {} // merge. the lower level execute first
  onRequest: () => {} // merge. the lower level execute first 
}

Here is how nuxt.config will be resolved:

export default defineNuxtConfig({
  msw: {
    includeLayer // overwrite. the higher level take precedence
    enable // overwrite. the higher level take precedence
    testUtils // overwrite. the higher level take precedence 
    folderPath // every layer follow its own folderPath setting
  }
})

folderPath is the only option that is not merged. Each layer will follow its own folderPath setting. For example, if you set folderPath to ./msw but set ~/msw at the layer user level, nuxt-msw will find msw files in following folders:

nuxt layer and unit test

If you need share msw settings in layers for unit test, you can add a file named unit.{ts|js|mjs|cjs} in the layer folder (app level also support this file). The merge strategy is same as worker and server file.

unit.{ts|js|mjs|cjs} file will be run in Vitest environment, so you can access in this file. Acctually, a part of work of setupNuxtMswServer in unit test is import this file and merge the options.

setupNuxtMswServer accept the same options as defineNuxtMswTestOptions return. The options of setupNuxtMswServer will be merged with the options of unit.{ts|js|mjs|cjs} file. setupNuxtMswServer will take highest precedence.

import { http, HttpResponse } from 'msw'

export default defineNuxtMswTestOptions(() => {
  const handlers = [
    http.get('/api/user', () => {
      // ...and respond to them using this JSON response.
      return HttpResponse.json({
        id: 'Mock01',
        name: 'msw server unit',
      })
    }),
  ]
  return {
    baseURL: 'http://localhost:3000',
    handlers,
    serverOptions: {
      // onUnhandledRequest: 'bypass',
    },
  }
})
// xxx.spec.ts
import { setupNuxtMswServer } from '@crazydos/nuxt-msw/test-utils'
import { http, HttpResponse } from "msw"

const mswServer = await setupNuxtMswServer(
  /** 
   * You can still pass options to overwrite the options in unit.ts
  */
)

Contribution

Local development ```bash # Install dependencies pnpm install # Generate type stubs pnpm dev:prepare # Develop with the playground pnpm dev # Build the playground pnpm dev:build # Run ESLint pnpm lint ```

Learn more about authoring nuxt module