Skip to content

Commit bbd8400

Browse files
Merge pull request #77 from storybookjs/copilot/fix-dynamic-mocking-issue
Fix next/dynamic mock to properly handle ssr: false option
2 parents 1755ab3 + c6666a2 commit bbd8400

File tree

3 files changed

+58
-4
lines changed

3 files changed

+58
-4
lines changed

example/src/app/components/DynamicImport/DynamicImport.stories.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ const DynamicComponentNoSSR = dynamic(() => import("./DynamicImport"), {
77
ssr: false,
88
});
99

10+
// Test case that matches the user's issue pattern: using .then((mod) => mod.Component) with ssr: false
11+
const namedExportLoader = () =>
12+
import("./NamedExport").then((mod) => mod.NamedComponent);
13+
const DynamicNamedComponentNoSSR = dynamic(namedExportLoader, { ssr: false });
14+
15+
// Test without ssr: false to check if issue is SSR-specific
16+
const DynamicNamedComponent = dynamic(() =>
17+
import("./NamedExport").then((mod) => mod.NamedComponent),
18+
);
19+
1020
function Component() {
1121
return <DynamicComponent />;
1222
}
@@ -31,4 +41,35 @@ export const Default: Story = {
3141

3242
export const NoSSR: Story = {
3343
render: () => <DynamicComponentNoSSR />,
44+
play: async ({ canvas }) => {
45+
await waitFor(() =>
46+
expect(
47+
canvas.getByText("I am a dynamically loaded component"),
48+
).toBeDefined(),
49+
);
50+
},
51+
};
52+
53+
// Test without ssr: false
54+
export const NamedExport: Story = {
55+
render: () => <DynamicNamedComponent />,
56+
play: async ({ canvas }) => {
57+
await waitFor(() =>
58+
expect(
59+
canvas.getByText("I am a named export dynamically loaded component"),
60+
).toBeDefined(),
61+
);
62+
},
63+
};
64+
65+
// This test case matches the user's issue pattern
66+
export const NamedExportNoSSR: Story = {
67+
render: () => <DynamicNamedComponentNoSSR />,
68+
play: async ({ canvas }) => {
69+
await waitFor(() =>
70+
expect(
71+
canvas.getByText("I am a named export dynamically loaded component"),
72+
).toBeDefined(),
73+
);
74+
},
3475
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import React from "react";
2+
3+
export function NamedComponent() {
4+
return <div>I am a named export dynamically loaded component</div>;
5+
}

src/plugins/next-mocks/alias/dynamic/index.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,6 @@ export default function dynamic<P = Record<string, unknown>>(
7676
options?: DynamicOptions<P>,
7777
): React.ComponentType<P> {
7878
const loadableFn = Loadable as LoadableFn<P>;
79-
if (options?.ssr === false) {
80-
// biome-ignore lint/performance/noDelete: <explanation>
81-
delete options.ssr;
82-
}
8379

8480
let loadableOptions: LoadableOptions<P> = {
8581
// A loading component is not required, so we default it
@@ -136,5 +132,17 @@ export default function dynamic<P = Record<string, unknown>>(
136132
delete loadableOptions.loadableGenerated;
137133
}
138134

135+
// support for disabling server side rendering, eg: dynamic(() => import('../hello-world'), {ssr: false}).
136+
// In browser environments (Storybook/Vitest), we simply remove webpack and modules options
137+
// and proceed with the loadable component since we're always on the client-side.
138+
if (typeof loadableOptions.ssr === "boolean" && !loadableOptions.ssr) {
139+
// biome-ignore lint/performance/noDelete: <explanation>
140+
delete loadableOptions.ssr;
141+
// biome-ignore lint/performance/noDelete: <explanation>
142+
delete loadableOptions.webpack;
143+
// biome-ignore lint/performance/noDelete: <explanation>
144+
delete loadableOptions.modules;
145+
}
146+
139147
return loadableFn({ ...loadableOptions, loader: loader as Loader<P> });
140148
}

0 commit comments

Comments
 (0)