mswjs / examples

Examples of Mock Service Worker usage with various frameworks and libraries.
683 stars 211 forks source link

add next.js (app directory) example #101

Open kettanaito opened 9 months ago

kettanaito commented 9 months ago

Adds a Next.js 14 (App directory ) + MSW usage example.

Todos

kettanaito commented 9 months ago

Server-side integration

I got the server-side MSW integration working in Next.js by using the instrumentation hook:

export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { server } = await import('./mocks/node')
    server.listen()
  }
}

This allows MSW to intercept server-side requests Next.js makes.

Downsides

  1. Next seems to evaluate the instrumentation hook once. The import graph it creates will not update if you change mocks/handlers.ts because nothing but the instrumentation hook depends on that import. This means stale mocks until you re-run Next.js/force it re-evaluate the instrumentation hook.
SalahAdDin commented 9 months ago

I'm our company we are using this example as a reference.

mizamelo commented 9 months ago

@SalahAdDin

I'm our company we are using this example as a reference.

is it working? I've tried to use, but it's showing these messages below:

Internal error: TypeError: fetch failed

`[MSW] Warning: intercepted a request without a matching request handler:

• POST https://telemetry.nextjs.org/api/v1/record`

SalahAdDin commented 9 months ago

@SalahAdDin

I'm our company we are using this example as a reference.

Is it working? I've tried to use it, but it's showing these messages below:

Internal error: TypeError: fetch failed

`[MSW] Warning: intercepted a request without a matching request handler:

• POST https://telemetry.nextjs.org/api/v1/record`

Not checked it yet, We just set it up.

SalahAdDin commented 8 months ago

@kettanaito I don't know why but MSW is not intercepting page request. The mock is enabled but does not catch any fetch.

pandeymangg commented 6 months ago

how does playwright work with this? My test makes the actual api call instead of the mocked call

kettanaito commented 6 months ago

@pandeymangg, there should be nothing specific to Playwright here. You enable MSW in your Next.js app, then navigate to it in a Playwright test and perform the actions you need.

mw999 commented 5 months ago

Hey @kettanaito what is the status of this example? 😄

Got some of this working in my own app, but curious to see the libraries official recommended approach. Thanks 🙏

PS. love the library, it rivals trpc and react query as my favourite open source projects.

kettanaito commented 5 months ago

@pandeymangg, the Playwright integration is the same as in this example:

  1. Enable MSW client-side.
  2. Run your app.
  3. Navigate to it in Playwright.

There has never been anything specific to Cypress/Playwright that MSW required.

@mw999, that is a high praise, thank you! No updates on this example, I didn't have time to look into it and, frankly, very little I can do here. If Next.js doesn't pick up changes from the instrumentation hook in HRM, I cannot fix that. I won't be recommending half-baked experiences. For now, you cannot officially use MSW in Next.js.

mrmckeb commented 4 months ago

This is great work, thanks @kettanaito. Have you looked into the work that has been done here? https://github.com/vercel/next.js/tree/canary/packages/next/src/experimental/testmode/playwright

There may be an opportunity to work more closely with the Next.js team on this functionality, especially for use cases like Playwright.

bertoldoklinger commented 4 months ago

One question, with this template, how i can toggle msw with playwright to run only on test env?

kettanaito commented 4 months ago

@mrmckeb, thank you. Yes, I've heard about the test mode in Next.js. I don't believe it's the right place to integrate MSW. I will look at it again in the future but it's unlikely I will be recommending it.

@bertoldoklinger, you can toggle it by introducing an environment variable. Start the worker conditionally based on the value of that variable.

// your/app.jsx
if (process.env.SOME_VARIABLE === 'some-value') {
  // ...
  worker.start()
}
crisfcodes commented 4 months ago

I'm using this config with vitest, but If I try to run a simple test:

test('should pass', function () {
  expect(true).toBe(true);
});

I get:

Error: No known conditions for "./browser" specifier in "msw" package

Some suggestion to avoid this issue?

kettanaito commented 3 months ago

@crisfcodes, you need to configure your Vitest not to load browser modules in Node.js. Afaik, it doesn't do that by default. Perhaps you've enabled that explicitly?

crisfcodes commented 3 months ago

I'm using jsdom env in Vitest, if that is what you mean with enabled, also I'm wrapping the app with the MockProvider but the worker.start executed conditionally. what I did as workaround is to ignore the file that is throwing the error(browser.ts) with Vitest exclusions.

Also I'm having another issues like:

  1. Infinite browser loading states after manual reload and hot reload/tab switching, it blocks the browser so it's hard to debug, any recommendation for debugging?
  2. Cannot read properties of undefined (reading 'url')](https://github.com/mswjs/msw/issues/2053) from browser/index.js file... I patched the package adding ? to request?.url and that fixed the issue.

MockProvider reference:

import { PropsWithChildren, useEffect, useState } from 'react';
import { env } from '@/constants/env.mjs';

export const MockProvider = ({ children }: PropsWithChildren) => {
  const [mockingEnabled, enableMocking] = useState(false);
  const isProduction = env.NEXT_PUBLIC_ENV === 'prod';
  const isAppReady = mockingEnabled || isProduction;

  useEffect(() => {
    const enableApiMocking = async () => {
      const isLocalDevelopment = env.NEXT_PUBLIC_ENV === 'local';
      const shouldEnableMocking = typeof window !== 'undefined' && isLocalDevelopment;

      if (shouldEnableMocking) {
        const { worker } = await import('../tests/mocks/browser');
        await worker.start({
          serviceWorker: {
            url: `${env.NEXT_PUBLIC_BASE_PATH}/mockServiceWorker.js`,
          },
        });
        enableMocking(true);
      }
    };

    enableApiMocking();
  }, []);

  if (!isAppReady) {
    return null;
  }

  return <>{children}</>;
};

Sorry to use this comment for pointing to other issues, I'm just posting what I've got using this code

asapo commented 3 months ago

Does anyone have any example about middleware?

mikecfisher commented 3 months ago

FYI the current example doesn't seem to work when using Turbopack on Next 14 or RC.0 of Next 15. However it works great with the exact version in the package.json here.

dwjohnston commented 3 months ago

Hi I know that this PR is still in progress - I happen to be investigating how to using MSW with NextJS - it appears that this example as it currently is has the same problem that I've run into - MSW mocks seem to work for the first time the endpoint is called, but for subsequent calls the MSW behaviour is lost.

(I get this error message in subsequent calls):

SERVER LISTEN
is fetch patched? YES
 GET / 200 in 3124ms
is fetch patched? undefined
 ⨯ TypeError: fetch failed
    at async getUser (./app/page.tsx:15:22)
    at async Home (./app/page.tsx:20:18)
digest: "2377642097"
Cause: Error: getaddrinfo ENOTFOUND api.example.com
    at GetAddrInfoReqWrap.onlookup [as oncomplete] (node:dns:108:26)
    at GetAddrInfoReqWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
  errno: -3008,
  code: 'ENOTFOUND',
  syscall: 'getaddrinfo',
  hostname: 'api.example.com'
}
 GET / 200 in 67ms

Is this maybe something to do with how NextJS is munging fetch for its caching?

Update: short answer is YES.

If we replace our getUser with a call using axios

async function getUser() {
  console.log('is fetch patched?', Reflect.get(fetch, '__FOO'))

  const result = await axios.get('https://api.example.com/user');
  const user = result.data as User; 
  return user
}

Then our mocking behaviour works fine.

Is it possible to completely opt out of Next's fetch caching?

PhilipAngelinNE commented 2 months ago

Server-side integration

I got the server-side MSW integration working in Next.js by using the instrumentation hook:

export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { server } = await import('./mocks/node')
    server.listen()
  }
}

This allows MSW to intercept server-side requests Next.js makes.

Downsides

  1. Next seems to evaluate the instrumentation hook once. The import graph it creates will not update if you change mocks/handlers.ts because nothing but the instrumentation hook depends on that import. This means stale mocks until you re-run Next.js/force it re-evaluate the instrumentation hook.

To add to this:

In my testing, for some strange reason a guard clause doesn't work. That is:

// Fails
export const register = async (): Promise<void> => {
  if (process.env.NEXT_RUNTIME !== 'nodejs') {
    return;
  }
  const { server } = await import('./mocks/node');
  server.listen();
};
// Succeeds
export const register = async (): Promise<void> => {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { server } = await import('./mocks/node');
    server.listen();
  }
};
kettanaito commented 2 months ago

@PhilipAngelinNE, I bet that guard doesn't work because expressions like process.env.NEXT_RUNTIME !== 'nodejs' are evaluated at build-time, and if its false, the entire if statement will be removed. That's a wild guess.

kettanaito commented 2 months ago

Update

I confirm that https://github.com/vercel/next.js/pull/68193 fixes the remaining server-side issues, and renders a great experience! 🎉 Huge thanks to @feedthejim for his help with this one.

Another pending issue is this: https://github.com/vercel/next.js/issues/69098. Hope to get some feedback on it and help how I can. Looking good.

kettanaito commented 2 months ago

Update

I believe I've tracked down the root cause of the remaining browser HMR issue. See my investigation here: https://github.com/vercel/next.js/issues/69098#issuecomment-2317652004.

While I'm illustrating how to "fix" that issue in the comment, note that it is a workaround, and it is not recommended. Ultimately, it mustn't mater where you import your handlers. It's up to the framework's bundler to provide the correct update path for modules during HMR.

Pinging the Next.js team for more assistance now that the root cause is more clear.

jogilvyt commented 2 months ago

I've been using this as a guide to getting MSW set up with Playwright in Next - thank you so much!

I'm just wondering if it's possible to override the mocked responses inside specific Playwright tests using server.use(). I've exposed the server as a fixture which can be consumed in the Playwright tests, but the handlers I create never seem to be called.

I'm wondering if the server instance being used in the Playwright tests could potentially be different to the one initialised in the root layout.tsx file, but I'm not sure how to check this. Any ideas would be very welcome!

kettanaito commented 2 months ago

@jogilvyt, glad to hear that. Playwright runs in 2 environments: your tests run in Node.js, your app runs in the browser. You want to use setupWorker in the browser. The setupServer you introduce as a fixture in your tests never affects the browser process, only the Node.js (test) process.

What you are describing will be possible with https://github.com/mswjs/msw/pull/1617 once it ships. Stay tuned. You are also welcome to support that effort financially if your team relies on this feature. Thanks.

jogilvyt commented 2 months ago

Thanks @kettanaito - that makes sense, and yes setupRemoteServer sounds like exactly what I'm looking for. I'll keep an eye on the PR ❤️

Yan-pg commented 1 month ago

HI, I'm using next 14 and I can't integrate with mws on server Module not found: Can't resolve '_http_common', someone had this same error?

r34son commented 1 month ago

HI, I'm using next 14 and I can't integrate with mws on server Module not found: Can't resolve '_http_common', someone had this same error?

Look at https://github.com/vercel/next.js/issues/70262

pstachula-dev commented 1 month ago

Does anyone have any example about middleware?

MSW 2.0 is not working with middlewares I have an error:

Invariant Violation: Failed to create a WebStorageCookieStore: `localStorage` is not available in this environment. This is likely an issue with MSW. Please report it on GitHub: https://github.com/mswjs/msw/issues
    at <unknown> (webpack-internal:///(instrument)/./node_modules/outvariant/lib/index.mjs:75)
    at eval (webpack-internal:///(instrument)/./node_modules/msw/lib/core/utils/cookieStore.mjs:147:108)
kettanaito commented 1 month ago

@pstachula-dev, that looks like a rather old version of MSW. npm i msw@latest, and let me know.

pstachula-dev commented 1 month ago

@kettanaito It was MSW 2.4.3 on the latest version 2.4.9 I have different error:

Module not found: Can't resolve '_http_common'

https://nextjs.org/docs/messages/module-not-found
kettanaito commented 1 month ago

@pstachula-dev, for that you have to wait for Next.js to release that bugfix. It's already been merged (see https://github.com/vercel/next.js/issues/70262).

pstachula-dev commented 1 month ago

@kettanaito Update... Test repo: https://github.com/pstachula-dev/msw-nextjs-error MSW: 2.4.9 Nextjs: 15.0.0-canary.166

Module not found: Can't resolve '_http_common' - this errors is gone 🆗

But now I have new problems:

 Compiled /_not-found in 1218ms (920 modules)
 ⨯ node_modules/outvariant/lib/index.mjs (69:1) @ <unknown>
 ⨯ Failed to create a WebStorageCookieStore: `localStorage` is not available in this environment. This is likely an issue with MSW. Please report it on GitHub: https://github.com/mswjs/msw/issues
  67 | var invariant = (predicate, message, ...positionals) => {
  68 |   if (!predicate) {
> 69 |     throw new InvariantError(message, ...positionals);
     | ^
  70 |   }
  71 | };
  72 | invariant.as = (ErrorConstructor, predicate, message, ...positionals) => {
kettanaito commented 1 month ago

@pstachula-dev, can you please provide a reproduction repo for this?

The error means you are running browser code in a non-browser environment. Properly describing what you are doing, how, and what you expect as a result will help tremendously.

pstachula-dev commented 1 month ago

@kettanaito I have already posted repository above :)

Test repo: https://github.com/pstachula-dev/msw-nextjs-error

felipepalazzo commented 1 month ago

@pstachula-dev I upgraded to v15.0.0-canary.171, and I don't see any errors now

pstachula-dev commented 1 month ago

@felipepalazzo The situation is quite dynamic, this version is from 17h ago 😆

Hmm I have still same problems: with canary.171. Node 20.17.0

 ⨯ node_modules/outvariant/lib/index.mjs (69:1) @ <unknown>
 ⨯ Failed to create a WebStorageCookieStore: `localStorage` is not available in this environment. This is likely an issue with MSW. Please report it on GitHub: https://github.com/mswjs/msw/issues
  67 | var invariant = (predicate, message, ...positionals) => {
  68 |   if (!predicate) {
> 69 |     throw new InvariantError(message, ...positionals);
     | ^
  70 |   }
  71 | };
  72 | invariant.as = (ErrorConstructor, predicate, message, ...positionals) => {
sebws commented 2 weeks ago

As a workaround, to fix HMR not disposing of the registered worker, we can tell the HMR code how to dispose of it ourselves.

In browser.ts, append to the end of the file

module.hot?.dispose(() => { worker.stop(); })

You may need a ts-expect-error.

What this is doing is adding the function () => { worker.stop(); } to a list of functions called when the module is disposed of (in this case since it's being replaced). This makes it so client-side HMR is working as expected (at least with minimal testing so far).

Originally mentioned here: https://github.com/vercel/next.js/issues/69098

kettanaito commented 2 weeks ago

@sebws, still curious how does calling worker.stop() help?

All worker.stop() does it tells the worker script that this client has to be ignored. It doesn't clean up the handlers, doesn't affect the worker's state otherwise. I think what worker.stop() achieves in your suggest is ignores the previous, persisted worker instance from the last refresh so it doesn't conflict with the newly created instance after a new refresh (HMR). Thus providing once again that it's a workaround.

sebws commented 2 weeks ago

@kettanaito I'll have to double check. I remember the worker no longer being present in the memory of the page.

There's a chance I misunderstood if it was working correctly, so I was hoping you might give it a shot.

sebws commented 2 weeks ago

Yep, as far as I can see without it, there'd be extra SetupWorkerApis, when taking a heap snapshot in the memory tab of chrome devtools.

With the workaround, there's just the one (a new one each reload). My guess would be that if worker.stop() doesn't kill the worker, whatever it does do, removes the retainers of the worker that otherwise keep it in memory.