1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176/**
* Next.js Compatibility Tests: next/dynamic
*
* Ported from: https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/dynamic/dynamic.test.ts
*
* Tests next/dynamic behavior in the App Router:
* - React.lazy in client components (SSR rendering)
* - dynamic() in server components (SSR rendering)
* - dynamic() in client components (SSR rendering)
* - dynamic() server component importing client component
* - dynamic() with ssr: false (content NOT present in SSR HTML)
* - dynamic() with named exports (via .then(mod => mod.X))
*
* Fixture pages live in:
* - fixtures/app-basic/app/nextjs-compat/dynamic/ (main page + components)
* - fixtures/app-basic/app/nextjs-compat/dynamic/named-export/ (named export sub-page)
* - fixtures/app-basic/app/nextjs-compat/dynamic/ssr-false-only/ (isolated ssr:false test)
*/
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import type { ViteDevServer } from "vite";
import { APP_FIXTURE_DIR, startFixtureServer, fetchHtml } from "../helpers.js";
describe("Next.js compat: next/dynamic", () => {
let server: ViteDevServer;
let baseUrl: string;
beforeAll(async () => {
({ server, baseUrl } = await startFixtureServer(APP_FIXTURE_DIR, {
appRouter: true,
}));
// Warm up
await fetch(`${baseUrl}/`).catch(() => {});
}, 60_000);
afterAll(async () => {
await server?.close();
});
// โโ Main /dynamic page โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Next.js: 'should handle next/dynamic in SSR correctly'
// Source: https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/dynamic/dynamic.test.ts#L17-L27
//
// The main /dynamic page composes:
// 1. LazyClientComponent (React.lazy) -> renders "next-dynamic lazy"
// 2. NextDynamicServerComponent (dynamic() in server) -> renders "next-dynamic dynamic on server"
// 3. NextDynamicClientComponent (dynamic() in client) -> renders "next-dynamic dynamic on client"
// Also includes DynamicNoSSR (ssr:false) -> should NOT be in SSR HTML
// 4. NextDynamicServerImportClientComponent -> renders "next-dynamic server import client"
it("SSR: should contain React.lazy loaded content", async () => {
const { html } = await fetchHtml(baseUrl, "/nextjs-compat/dynamic");
expect(html).toContain("next-dynamic lazy");
});
it("SSR: should contain dynamic() server component content", async () => {
const { html } = await fetchHtml(baseUrl, "/nextjs-compat/dynamic");
expect(html).toContain("next-dynamic dynamic on server");
});
it("SSR: should contain dynamic() client component content", async () => {
const { html } = await fetchHtml(baseUrl, "/nextjs-compat/dynamic");
expect(html).toContain("next-dynamic dynamic on client");
expect(html).toContain('id="css-text-dynamic-client"');
});
// Regression test for issue #75: dynamic() client components must render
// their own imported component, not another client component's content.
it("SSR: dynamic() client should not render LazyClientComponent content (#75)", async () => {
const { html } = await fetchHtml(baseUrl, "/nextjs-compat/dynamic");
// class="hi" appears exactly once (from LazyClientComponent), not duplicated
// by NextDynamicClientComponent rendering the wrong module
const hiMatches = (html.match(/class="hi"/g) || []).length;
expect(hiMatches).toBe(1);
});
it("SSR: should contain dynamic() server-imported client content", async () => {
const { html } = await fetchHtml(baseUrl, "/nextjs-compat/dynamic");
expect(html).toContain("next-dynamic server import client");
});
it("SSR: should NOT contain ssr:false client content in HTML", async () => {
const { html } = await fetchHtml(baseUrl, "/nextjs-compat/dynamic");
expect(html).not.toContain("next-dynamic dynamic no ssr on client");
});
// โโ Named export โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Next.js: 'should support dynamic import with accessing named exports from client component'
// Source: https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/dynamic/dynamic.test.ts#L97-L100
//
// Uses dynamic(() => import('./client').then(mod => ({ default: mod.Button })))
it("SSR: named export via dynamic() should render button content", async () => {
const { html } = await fetchHtml(
baseUrl,
"/nextjs-compat/dynamic/named-export",
);
expect(html).toContain("this is a client button");
});
// โโ SSR false only page โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Next.js: 'should not render client component imported through ssr: false in client components'
// Source: https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/dynamic/dynamic.test.ts#L79-L96
it("SSR: ssr:false page should contain static content", async () => {
const { html } = await fetchHtml(
baseUrl,
"/nextjs-compat/dynamic/ssr-false-only",
);
expect(html).toContain("This is static content");
});
it("SSR: ssr:false page should NOT contain dynamic content", async () => {
const { html } = await fetchHtml(
baseUrl,
"/nextjs-compat/dynamic/ssr-false-only",
);
expect(html).not.toContain("next-dynamic dynamic no ssr on client");
});
// โโ Browser-only tests (documented, not ported) โโโโโโโโโโโโโโ
//
// SKIP: 'should handle next/dynamic in hydration correctly'
// Source: dynamic.test.ts#L29-L36
// WHY: Requires Playwright. After hydration, ssr:false component appears
// via client-side rendering. The #css-text-dynamic-no-ssr-client element
// should show "next-dynamic dynamic no ssr on client:suffix".
// TO PORT: tests/e2e/app-router/nextjs-compat/dynamic.spec.ts
//
// SKIP: 'should handle ssr: false in pages when appDir is enabled'
// Source: dynamic.test.ts#L13-L21
// WHY: Tests Pages Router /legacy/no-ssr. We're testing App Router patterns only.
// N/A for this suite.
//
// SKIP: 'should generate correct client manifest for dynamic chunks'
// Source: dynamic.test.ts#L38-L41
// WHY: Tests chunk loading via a specific /chunk-loading/server page.
// Would need a dedicated fixture. Low priority for compat.
// N/A โ build manifest structure differs in vinext.
//
// SKIP: 'should render loading by default if loading is specified and loader is slow'
// Source: dynamic.test.ts#L43-L50
// WHY: Dev-only behavior โ slow loader shows loading component. In production
// the component resolves. The test patches a file at runtime (dev-only).
// N/A for HTTP-level SSR testing.
//
// SKIP: 'should not render loading by default'
// Source: dynamic.test.ts#L52-L55
// WHY: Tests that dynamic component without loading option doesn't show "loading" text.
// Could be tested but needs a dedicated fixture. Low value.
//
// SKIP: 'should ignore next/dynamic in routes'
// Source: dynamic.test.ts#L57-L60
// WHY: Tests route handler (API route) behavior. Covered in Chunk 5 (app-routes).
//
// SKIP: 'should ignore next/dynamic in sitemap'
// Source: dynamic.test.ts#L62-L65
// WHY: Tests sitemap.xml generation. Vinext sitemap support is a separate feature.
// N/A for this suite.
//
// SKIP: 'should not render client component imported through ssr: false in client components in edge runtime'
// Source: dynamic.test.ts#L68-L96
// WHY: Tests edge runtime variant + build manifest inspection. Both need Playwright
// and prod build. N/A for dev SSR tests.
//
// SKIP: 'should support dynamic import with TLA in client components'
// Source: dynamic.test.ts#L102-L116
// WHY: Tests top-level await in client component dynamic imports.
// Partially testable at SSR level, but the key assertion (no-ssr text empty
// on server, present after hydration) needs Playwright.
// TO PORT: tests/e2e/app-router/nextjs-compat/dynamic.spec.ts
});