Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Question]: How to mount NextRouter in tests when using framework @storybook/nextjs #22538

Closed
Tracked by #25875
Mattgic opened this issue May 12, 2023 · 14 comments
Closed
Tracked by #25875

Comments

@Mattgic
Copy link

Mattgic commented May 12, 2023

Describe the bug

When using @storybook/nextjs framework and composeStories from @storybook/react to import my stories in my tests, I have this error saying "NextRouter was not mounted" on components that need Next router.

I made a reproduction repo here : https://github.com/Mattgic/repro-storybook-nextjs-tests

Before updating to Storybook 7, I was using storybook-addon-next-router. Everything worked fine, from my stories to my tests. The addon configuration included adding a custom global provider & decorator in my preview.ts. As the whole preview configuration is used in the setProjectAnnotations call (formerly setGlobalConfig) for my jest tests, my stories in my tests include this decorator and everything works fine.

@storybook/nextjs documentation doesn't talk about importing stories in tests. Maybe there is a custom decorator I'm missing.
What I don't get about the new framework option is that it seems to only be a build config. But if this next router "decorator" is only added on Storybook build, I can't have it in my tests too.

To Reproduce

https://github.com/Mattgic/repro-storybook-nextjs-tests

System

No response

Additional context

No response

@valentinpalkovic
Copy link
Contributor

@Mattgic

Thank you for opening up this issue.

Indeed, you could import and use the custom decorator, which is automatically applied to each Next.js story also in your tests:

import { composeStories } from '@storybook/react'
import { decorators } from '@storybook/nextjs/preview.js'
import { render, screen } from '@testing-library/react'

import * as stories from './MyComp.stories'

const { Default } = composeStories(stories, { decorators })

test('My test', () => {
  render(<Default />)
  screen.getByText(/Path : \/toto/i)
})

You will encounter a Typescript error, though, that the type declaration for the module @storybook/nextjs/preview.js cannot be found. To fix this, open up your tsconfig.json file and change the moduleResolution to node16 or nodenext.

Would you mind opening up a PR to add the necessary instructions in the Readme of the @storybook/nextjs package? :)

@valentinpalkovic valentinpalkovic changed the title [Bug]: NextRouter was not mounted in tests when using framework @storybook/nextjs [Question]: How to mount NextRouter in tests when using framework @storybook/nextjs May 15, 2023
@PupoSDC
Copy link

PupoSDC commented Aug 6, 2023

Hello,

I've been trying to get this to work with vite, and no joy:

 FAIL  src/questions/question-editor/question-editor.test.tsx [ src/questions/question-editor/question-editor.test.tsx ]
Error: Cannot find module '/Users/pedro/dev/chair-flight-monorepo/node_modules/.pnpm/@[email protected]_@[email protected]_@[email protected]_@[email protected]_esbuild_y7peqbgta5yxnw4uvcg2d6o5eu/node_modules/next/config' imported from /Users/pedro/dev/chair-flight-monorepo/node_modules/.pnpm/@storybook+nextjs@7.2.1_@swc+core@1.3.74_@types+react-dom@18.2.7_@types+react@18.2.18_esbuild_y7peqbgta5yxnw4uvcg2d6o5eu/node_modules/@storybook/nextjs/dist/preview.mjs
Did you mean to import next@13.4.12_@babel+core@7.22.9_react-dom@18.2.0_react@18.2.0/node_modules/next/config.js?

the problematic node_modules/@storybook/nextjs/dist/preview.mjs

import { ImageContext } from './chunk-2SSJK7AW.mjs';
import { __require } from './chunk-FFRTCGB4.mjs';
import { setConfig } from 'next/config';
import * as React2 from 'react';
import React2__default, { useMemo } from 'react';
import { RouterContext } from 'next/dist/shared/lib/router-context';
import { HeadManagerContext } from 'next/dist/shared/lib/head-manager-context';
import initHeadManager from 'next/dist/client/head-manager';

setConfig(process.env.__NEXT_RUNTIME_CONFIG);var I...
import { composeStories } from '@storybook/react';
import * as stories from './question-editor.stories';
import { decorators } from '@storybook/nextjs/preview.js'
import { render, screen } from '@testing-library/react';
import { default as userEvent } from '@testing-library/user-event';

const { BasePage } = composeStories(stories, { decorators });

describe('QuestionEditor', () => {
    it('is possible to create a new variant', async () => {
        render(<BasePage />);
        const numberOfCards = screen.getAllByTestId('variant-card').length;
        const addButton = screen.getByRole('button', { name: /new variant/i });
        await userEvent.click(addButton,);
        const newNumberOfCards = screen.getAllByTestId('variant-card').length;
        expect(newNumberOfCards).toBe(numberOfCards + 1);
    });
});

I've kind of ran out of ideas when trying to configure vite. Stuff like inline deps, don't work.... any ideas?

@solace
Copy link

solace commented Nov 3, 2023

When I apply the decorator manually as described, the tests return:

Error: Cannot find module '/.../node_modules/next/config' imported from /.../node_modules/@storybook/nextjs/dist/preview.mjs
Did you mean to import next/config.js?

Is there additional config required? Thanks!

PS. node_modules/next/config.js is definitely there.

@elussich-globant
Copy link

Hello everyone, I recently faced a similar issue and fixed it by leveraging the MemoryRouterProvider module from next-router-mock library: https://github.com/scottrippey/next-router-mock#storybook-example.

In my particular case, I added this provider as a wrapper to my component, for a particular Story, but I guess it could be included as a global decorator, at the preview.js file level, whether the case.

// simplified, for the sake of brevity; not actual code
import React from 'react';
import { Meta } from '@storybook/react';
import { MemoryRouterProvider } from 'next-router-mock/MemoryRouterProvider/next-13';

export const WithBackButton = {
  render: (): JSX.Element => (
    <MemoryRouterProvider>
      <MyComponent url={'/home'} />
    </MemoryRouterProvider>
  ),
};

Let me know if you need any further details about my context and the solution. Cheers!

@Mattgic
Copy link
Author

Mattgic commented Nov 24, 2023

I wish we didn't need a separate package to make this work. @storybook/nextjs framework should expose decorators to include in the tests.
In the last beta version 7.6.0-beta.2, importing decorators from '@storybook/nextjs/preview.js' doesn't work anymore. pb occurs since v7.6.0-alpha.7 exactly)

After investigation, problem comes from this PR #24834

@Mattgic
Copy link
Author

Mattgic commented Dec 1, 2023

The problem is that decorators in dist/preview are not exposed.
I found a workaround, with jest module mapper.
In jest.config.js :

  moduleNameMapper: {
     ...
    '@storybook/nextjs/dist/preview': '<rootDir>/node_modules/@storybook/nextjs/dist/preview',
  },

In my jest setup :

// @ts-expect-error This import is resolved through jest module mapper
import { decorators as nextFrameworkDecorators } from '@storybook/nextjs/dist/preview'
import { setProjectAnnotations } from '@storybook/react'

import globalStorybookConfig, { decorators as globalStorybookConfigDecorators } from '../.storybook/preview'

const newConfig = {
  ...globalStorybookConfig,
  // Add decorator from @storybook/nextjs framework
  // https://github.com/storybookjs/storybook/issues/22538
  decorators: [...globalStorybookConfigDecorators, ...nextFrameworkDecorators],
}
setProjectAnnotations(newConfig)

@yannbf
Copy link
Member

yannbf commented Mar 5, 2024

Hey everyone! Thanks a lot for elaborating the issues. For the time being, please use the workarounds suggested.

In Storybook 8.1, we will be releasing portable-stories (composeStories) API in the @storybook/nextjs package, which will contain all the necessary internals to get things to work correctly. Storybook 8.1 will be released shortly after Storybook 8.0 is released (which is currently in RC stage). Thank you so much for your patience!! 🙏

@yannbf yannbf closed this as completed Mar 5, 2024
@Mattgic
Copy link
Author

Mattgic commented Jul 11, 2024

I can't to make this work with Storybook 8.1+.

What I understand is that importing the nextjs decorators manually from @storybook/nextjs is no longer necessary when importing composeStories from @storybook/nextjs, and indeed when I do that I don't have the "NextRouter was not mounted" error, but I have another one :

Error: Uncaught [SB_FRAMEWORK_NEXTJS_0002 (NextjsRouterMocksNotAvailable): Tried to access router mocks from "next/router" but they were not created yet. You might be running code in an unsupported environment.]

Should I open a new issue ?

@xveganxxxedgex
Copy link

xveganxxxedgex commented Jul 24, 2024

I'm hitting this as well with storybook 8.2.5 and @storybook/nextjs 8.2.4. If I have the assertions directly in the play func for stories, looping through them in my test file does pass the tests. But if I try to add additional test coverage by reusing existing stories and modifying args and composing them individually, I get the same error as @Mattgic posted above. Similarly, doing await composedStories.NotExistingCustomer.play(); within the individual test case does get it to render without the warning, but I'm not sure how to override parameters for that test case before causing the story to render.

FWIW even in the case where the tests do pass in the composeStories loop, I do still get the error outlined in this other issue about An update to Component inside a test was not wrapped in act(...)

edit: I was able to get around this for now by composing individually stories separately instead of leveraging the ones from composeStories:

it("should render as readonly", async () => {
  const ComposedStory = composeStory(
    {
      ...stories.PackageIsSelected,
        args: {
          ...composedStories.PackageIsSelected.args,
          deal: { ...mockDeal, writable: false },
        },
        play: async ({ canvasElement }) => {
          const canvas = within(canvasElement);
          expect(...)
        },
      },
      stories.default
    );
  await ComposedStory.play();
});

@valentinpalkovic
Copy link
Contributor

cc @kasperpeulen, @yannbf

@vekinox
Copy link

vekinox commented Sep 9, 2024

Using Storybook 8.2.9, Jest 29.7.0

I was able to get rid of the error:

Error: Uncaught [SB_FRAMEWORK_NEXTJS_0002 (NextjsRouterMocksNotAvailable): Tried to access router mocks from "next/router" but they were not created yet. You might be running code in an unsupported environment.]

Building on the ideas by @Mattgic, I added @storybook/nextjs/dist/export-mocks/router to jest.config.js

const nextJest = require('next/jest');
 
/** @type {import('jest').Config} */
const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: './',
});
 
// Add any custom config to be passed to Jest
const config = {
  coverageProvider: 'v8',
  testEnvironment: 'jsdom',
  // Add more setup options before each test is run
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  moduleNameMapper: {
    // Make preview and export-mocks available
   '@storybook/nextjs/dist/preview': '<rootDir>/node_modules/@storybook/nextjs/dist/preview',
   '@storybook/nextjs/dist/export-mocks/router': '<rootDir>/node_modules/@storybook/nextjs/dist/export-mocks/router',
  },
}
 
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(config);

By importing and calling createRouter in jest.setup.js or directly in test files, the error went away.

// stories.test.js

import '@testing-library/jest-dom';

import { createRouter, getRouter } from '@storybook/nextjs/dist/export-mocks/router';
import { RouterContext } from 'next/dist/shared/lib/router-context.shared-runtime';

import { setProjectAnnotations } from '@storybook/nextjs';

import { decorators as nextFrameworkDecorators } from '@storybook/nextjs/dist/preview';
import globalStorybookConfig, { decorators as globalStorybookConfigDecorators } from '../.storybook/preview';

const newConfig = {
  ...globalStorybookConfig,
  // Add decorator from @storybook/nextjs framework
  // https://github.com/storybookjs/storybook/issues/22538
  decorators: [...globalStorybookConfigDecorators, ...nextFrameworkDecorators],
}

setProjectAnnotations(newConfig)

// Create a mock router using createRouter from @storybook/nextjs/dist/export-mocks/router
createRouter({ globals: { locale: 'en' }, routeParams: {}}); 

// (Optional) Get the mock router using getRouter from @storybook/nextjs/dist/export-mocks/router
const mockRouter = getRouter(); // Get the mock router

@leochiu-a
Copy link

leochiu-a commented Sep 25, 2024

I encountered the same issue with storybook 8.3.2 and @storybook/nextjs 8.3.2. I have the app router set up in preview.tsx.

The following errors occurred:

Error: Uncaught [SB_FRAMEWORK_NEXTJS_0002 (NextjsRouterMocksNotAvailable): Tried to access router mocks from "next/router" but they were not created yet. You might be running code in an unsupported environment.]

Error: Uncaught [SB_FRAMEWORK_NEXTJS_0002 (NextjsRouterMocksNotAvailable): Tried to access router mocks from "next/navigation" but they were not created yet. You might be running code in an unsupported environment.]

To resolve this, simply add createRouter() and createNavigation() to jest.setup.js:

import { createRouter } from '@storybook/nextjs/router.mock';
import { createNavigation } from '@storybook/nextjs/navigation.mock';

createRouter();
createNavigation();

@vin-mfvhn
Copy link

vin-mfvhn commented Sep 26, 2024

I have followed the workaround. But it's simply throw the error

TypeError: Cannot read properties of undefined (reading 'forEach')

while calling the createRouter function.

FYI: I'm using the latest storybook version 8.3.3 for all packages.

@bazter
Copy link

bazter commented Oct 16, 2024

Storybook 8.3.5, Jest 29.5.0

For app router environment, I was able to get away with this:

// jest.setup.ts

import '@testing-library/jest-dom';
import { setProjectAnnotations } from '@storybook/nextjs';
import { createNavigation } from '@storybook/nextjs/navigation.mock'; // <------------- THIS

// import global project annotations form the storybook config
import * as previewAnnotations from '../../apps/fe-storybook/.storybook/preview.js';
const annotations = setProjectAnnotations([previewAnnotations]);

// Supports beforeAll hook from Storybook
beforeAll(annotations.beforeAll);

createNavigation({}); // <------------- THIS

No moduleNameMapper needed this way.

Component example:

// example-component.tsx

import { useState, FC } from 'react';
import { useParams, usePathname } from 'next/navigation.js';

interface ExampleComponentProps {
  initialCount: number;
}

export const ExampleComponent: FC<ExampleComponentProps> = ({ initialCount }) => {
  const [count, setCount] = useState(initialCount || 0);
  const pathname = usePathname(); // <------------------ using next/navigation here

  const handleIncrement = () => {
    setCount((prevCount) => prevCount + 1);
  };

  return (
    <div>
      <h1>Example Component</h1>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>Increment</button>
      <p>Current route: {pathname}</p> // <---------------- printing current pathname
    </div>
  );
};

Story example:

// example-component.stories.tsx

export const ExampleStory: Story = {
  args: {
    initialCount: 5,
  },
  parameters: {
    nextjs: {
      appDirectory: true, // <-------- This is here just to make the example simpler, I have this globally in the .storybook/preview.ts
      navigation: {
        pathname: '/my-route', // <------ Mocking current route here
      },
    },
  }
};

Test example:

// example-component.test.tsx

import { render, screen } from '@testing-library/react';
import { composeStories } from '@storybook/nextjs';

import * as stories from '../../stories/example/example-component.stories.js';

const { ExampleStory } = composeStories(stories);

it('should render the component with initial state', async () => {
  render(<ExampleStory />);
  const initialCountText = screen.getByText('Count: 5');
  expect(initialCountText).toBeInTheDocument();
});

it('should show mocked URL path', async () => {
  render(<ExampleStory />);
  const routeText = await screen.findByText(/current route: \/my-route/i); // <----- Testing printed pathname
  expect(routeText).not.toBeNull();
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests