Skip to content

Commit 1511adc

Browse files
committed
Add dedicated RSC HMR + HDR test
1 parent 13c1f7c commit 1511adc

File tree

1 file changed

+383
-0
lines changed

1 file changed

+383
-0
lines changed
Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
import fs from "node:fs/promises";
2+
import path from "node:path";
3+
import { expect } from "@playwright/test";
4+
5+
import type { Files, TemplateName } from "./helpers/vite.js";
6+
import {
7+
test,
8+
createEditor,
9+
viteConfig,
10+
reactRouterConfig,
11+
} from "./helpers/vite.js";
12+
13+
const templateName = "rsc-vite-framework" as const satisfies TemplateName;
14+
15+
test.describe("Vite HMR & HDR (RSC)", () => {
16+
test("vite dev", async ({ page, dev }) => {
17+
let files: Files = async ({ port }) => ({
18+
"vite.config.js": await viteConfig.basic({ port, templateName }),
19+
"react-router.config.ts": reactRouterConfig({
20+
viteEnvironmentApi: templateName.includes("rsc"),
21+
}),
22+
"app/routes/hmr/route.tsx": `
23+
// imports
24+
import { Mounted } from "./route.client";
25+
26+
// loader
27+
28+
export function ServerComponent() {
29+
return (
30+
<div id="index">
31+
<h2 data-title>Index</h2>
32+
<input />
33+
<Mounted />
34+
<p data-hmr>HMR updated: 0</p>
35+
{/* elements */}
36+
</div>
37+
);
38+
}
39+
`,
40+
"app/routes/hmr/route.client.tsx": `
41+
"use client";
42+
43+
import { useState, useEffect } from "react";
44+
45+
export function Mounted() {
46+
const [mounted, setMounted] = useState(false);
47+
useEffect(() => {
48+
setMounted(true);
49+
}, []);
50+
51+
return <p data-mounted>Mounted: {mounted ? "yes" : "no"}</p>;
52+
}
53+
`,
54+
});
55+
let { cwd, port } = await dev(files, templateName);
56+
let edit = createEditor(cwd);
57+
58+
// setup: initial render
59+
await page.goto(`http://localhost:${port}/hmr`, {
60+
waitUntil: "networkidle",
61+
});
62+
await expect(page.locator("#index [data-title]")).toHaveText("Index");
63+
64+
// setup: hydration
65+
await expect(page.locator("#index [data-mounted]")).toHaveText(
66+
"Mounted: yes",
67+
);
68+
69+
// setup: browser state
70+
let hmrStatus = page.locator("#index [data-hmr]");
71+
72+
await expect(hmrStatus).toHaveText("HMR updated: 0");
73+
let input = page.locator("#index input");
74+
await expect(input).toBeVisible();
75+
await input.type("stateful");
76+
expect(page.errors).toEqual([]);
77+
78+
// route: HMR
79+
await edit("app/routes/hmr/route.tsx", (contents) =>
80+
contents.replace("HMR updated: 0", "HMR updated: 1"),
81+
);
82+
await page.waitForLoadState("networkidle");
83+
84+
await expect(hmrStatus).toHaveText("HMR updated: 1");
85+
await expect(input).toHaveValue("stateful");
86+
expect(page.errors).toEqual([]);
87+
88+
// route: add loader
89+
await edit("app/routes/hmr/route.tsx", (contents) =>
90+
contents
91+
.replace(
92+
"// loader",
93+
`// loader\nexport const loader = () => ({ message: "HDR updated: 0" });`,
94+
)
95+
.replace(
96+
"export function ServerComponent() {",
97+
`export function ServerComponent({ loaderData }: { loaderData: { message: string } }) {`,
98+
)
99+
.replace(
100+
"{/* elements */}",
101+
`{/* elements */}\n<p data-hdr>{loaderData.message}</p>`,
102+
),
103+
);
104+
await page.waitForLoadState("networkidle");
105+
let hdrStatus = page.locator("#index [data-hdr]");
106+
await expect(hdrStatus).toHaveText("HDR updated: 0");
107+
await expect(input).toHaveValue("stateful");
108+
expect(page.errors).toEqual([]);
109+
110+
// route: HDR
111+
await edit("app/routes/hmr/route.tsx", (contents) =>
112+
contents.replace("HDR updated: 0", "HDR updated: 1"),
113+
);
114+
await page.waitForLoadState("networkidle");
115+
await expect(hdrStatus).toHaveText("HDR updated: 1");
116+
await expect(input).toHaveValue("stateful");
117+
expect(page.errors).toEqual([]);
118+
119+
// route: HMR + HDR
120+
await edit("app/routes/hmr/route.tsx", (contents) =>
121+
contents
122+
.replace("HMR updated: 1", "HMR updated: 2")
123+
.replace("HDR updated: 1", "HDR updated: 2"),
124+
);
125+
await page.waitForLoadState("networkidle");
126+
await expect(hmrStatus).toHaveText("HMR updated: 2");
127+
await expect(hdrStatus).toHaveText("HDR updated: 2");
128+
await expect(input).toHaveValue("stateful");
129+
expect(page.errors).toEqual([]);
130+
131+
// create new non-route imported server component
132+
await fs.writeFile(
133+
path.join(cwd, "app/imported-server-component.tsx"),
134+
`
135+
import { ImportedServerComponentClientMounted } from "./imported-server-component-client";
136+
137+
export function ImportedServerComponent() {
138+
return (
139+
<div>
140+
<p data-imported-server-component>Imported Server Component HMR: 0</p>
141+
<ImportedServerComponentClientMounted />
142+
</div>
143+
);
144+
}
145+
`,
146+
"utf8",
147+
);
148+
await fs.writeFile(
149+
path.join(cwd, "app/imported-server-component-client.tsx"),
150+
`
151+
"use client";
152+
153+
import { useState, useEffect } from "react";
154+
155+
export function ImportedServerComponentClientMounted() {
156+
const [mounted, setMounted] = useState(false);
157+
useEffect(() => {
158+
setMounted(true);
159+
}, []);
160+
161+
return (
162+
<p data-imported-server-component-client-mounted>
163+
Imported Server Component Client Mounted: {mounted ? "yes" : "no"}
164+
</p>
165+
);
166+
}
167+
`,
168+
"utf8",
169+
);
170+
await edit("app/routes/hmr/route.tsx", (contents) =>
171+
contents
172+
.replace(
173+
"// imports",
174+
`// imports\nimport { ImportedServerComponent } from "../../imported-server-component";`,
175+
)
176+
.replace(
177+
"{/* elements */}",
178+
"{/* elements */}\n<ImportedServerComponent />",
179+
),
180+
);
181+
await page.waitForLoadState("networkidle");
182+
let serverComponent = page.locator(
183+
"#index [data-imported-server-component]",
184+
);
185+
let importedServerComponentClientMounted = page.locator(
186+
"#index [data-imported-server-component-client-mounted]",
187+
);
188+
await expect(serverComponent).toBeVisible();
189+
await expect(serverComponent).toHaveText(
190+
"Imported Server Component HMR: 0",
191+
);
192+
await expect(importedServerComponentClientMounted).toBeVisible();
193+
await expect(importedServerComponentClientMounted).toHaveText(
194+
"Imported Server Component Client Mounted: yes",
195+
);
196+
await expect(input).toHaveValue("stateful");
197+
expect(page.errors).toEqual([]);
198+
199+
// non-route imported server component: HMR
200+
await edit("app/imported-server-component.tsx", (contents) =>
201+
contents.replace(
202+
"Imported Server Component HMR: 0",
203+
"Imported Server Component HMR: 1",
204+
),
205+
);
206+
await page.waitForLoadState("networkidle");
207+
await expect(serverComponent).toHaveText(
208+
"Imported Server Component HMR: 1",
209+
);
210+
await expect(input).toHaveValue("stateful");
211+
expect(page.errors).toEqual([]);
212+
213+
// create new non-route imported client component
214+
await fs.writeFile(
215+
path.join(cwd, "app/imported-client-component.tsx"),
216+
`
217+
"use client";
218+
219+
import { useState } from "react";
220+
221+
export function ImportedClientComponent() {
222+
const [count, setCount] = useState(0);
223+
return (
224+
<div>
225+
<p data-imported-client-component>Imported Client Component HMR: 0</p>
226+
<button data-imported-client-component-button onClick={() => setCount(count + 1)}>
227+
Count: {count}
228+
</button>
229+
</div>
230+
);
231+
}
232+
`,
233+
"utf8",
234+
);
235+
await edit("app/routes/hmr/route.tsx", (contents) =>
236+
contents
237+
.replace(
238+
"// imports",
239+
`// imports\nimport { ImportedClientComponent } from "../../imported-client-component";`,
240+
)
241+
.replace(
242+
"{/* elements */}",
243+
"{/* elements */}\n<ImportedClientComponent />",
244+
),
245+
);
246+
await page.waitForLoadState("networkidle");
247+
let clientComponent = page.locator(
248+
"#index [data-imported-client-component]",
249+
);
250+
let clientButton = page.locator(
251+
"#index [data-imported-client-component-button]",
252+
);
253+
await expect(clientComponent).toBeVisible();
254+
await expect(clientComponent).toHaveText(
255+
"Imported Client Component HMR: 0",
256+
);
257+
await expect(clientButton).toHaveText("Count: 0");
258+
await expect(input).toHaveValue("stateful");
259+
expect(page.errors).toEqual([]);
260+
261+
// non-route imported client component: HMR
262+
await edit("app/imported-client-component.tsx", (contents) =>
263+
contents.replace(
264+
"Imported Client Component HMR: 0",
265+
"Imported Client Component HMR: 1",
266+
),
267+
);
268+
await page.waitForLoadState("networkidle");
269+
await expect(clientComponent).toHaveText(
270+
"Imported Client Component HMR: 1",
271+
);
272+
await expect(clientButton).toHaveText("Count: 0");
273+
await expect(input).toHaveValue("stateful");
274+
expect(page.errors).toEqual([]);
275+
276+
// non-route imported client component: state preservation
277+
await clientButton.click();
278+
await expect(clientButton).toHaveText("Count: 1");
279+
await edit("app/imported-client-component.tsx", (contents) =>
280+
contents.replace(
281+
"Imported Client Component HMR: 1",
282+
"Imported Client Component HMR: 2",
283+
),
284+
);
285+
await page.waitForLoadState("networkidle");
286+
await expect(clientComponent).toHaveText(
287+
"Imported Client Component HMR: 2",
288+
);
289+
await expect(clientButton).toHaveText("Count: 1");
290+
await expect(input).toHaveValue("stateful");
291+
expect(page.errors).toEqual([]);
292+
293+
// create new non-route server module
294+
await fs.writeFile(
295+
path.join(cwd, "app/indirect-hdr-dep.ts"),
296+
`export const indirect = "indirect 0"`,
297+
"utf8",
298+
);
299+
await fs.writeFile(
300+
path.join(cwd, "app/direct-hdr-dep.ts"),
301+
`
302+
import { indirect } from "./indirect-hdr-dep"
303+
export const direct = "direct 0 & " + indirect
304+
`,
305+
"utf8",
306+
);
307+
await edit("app/routes/hmr/route.tsx", (contents) =>
308+
contents
309+
.replace(
310+
"// imports",
311+
`// imports\nimport { direct } from "../../direct-hdr-dep"`,
312+
)
313+
.replace(
314+
`{ message: "HDR updated: 2" }`,
315+
`{ message: "HDR updated: " + direct }`,
316+
)
317+
.replace(`HDR updated: 2`, `HDR updated: direct 0 & indirect 0`),
318+
);
319+
await page.waitForLoadState("networkidle");
320+
await expect(hdrStatus).toHaveText("HDR updated: direct 0 & indirect 0");
321+
await expect(input).toHaveValue("stateful");
322+
expect(page.errors).toEqual([]);
323+
324+
// non-route: HDR for direct dependency
325+
await edit("app/direct-hdr-dep.ts", (contents) =>
326+
contents.replace("direct 0 &", "direct 1 &"),
327+
);
328+
await page.waitForLoadState("networkidle");
329+
await expect(hdrStatus).toHaveText("HDR updated: direct 1 & indirect 0");
330+
await expect(input).toHaveValue("stateful");
331+
expect(page.errors).toEqual([]);
332+
333+
// non-route: HDR for indirect dependency
334+
await edit("app/indirect-hdr-dep.ts", (contents) =>
335+
contents.replace("indirect 0", "indirect 1"),
336+
);
337+
await page.waitForLoadState("networkidle");
338+
await expect(hdrStatus).toHaveText("HDR updated: direct 1 & indirect 1");
339+
await expect(input).toHaveValue("stateful");
340+
expect(page.errors).toEqual([]);
341+
342+
// everything everywhere all at once
343+
await Promise.all([
344+
edit("app/routes/hmr/route.tsx", (contents) =>
345+
contents
346+
.replace("HMR updated: 2", "HMR updated: 3")
347+
.replace("HDR updated: ", "HDR updated: route & "),
348+
),
349+
edit("app/imported-server-component.tsx", (contents) =>
350+
contents.replace(
351+
"Imported Server Component HMR: 1",
352+
"Imported Server Component HMR: 2",
353+
),
354+
),
355+
edit("app/imported-client-component.tsx", (contents) =>
356+
contents.replace(
357+
"Imported Client Component HMR: 2",
358+
"Imported Client Component HMR: 3",
359+
),
360+
),
361+
edit("app/direct-hdr-dep.ts", (contents) =>
362+
contents.replace("direct 1 &", "direct 2 &"),
363+
),
364+
edit("app/indirect-hdr-dep.ts", (contents) =>
365+
contents.replace("indirect 1", "indirect 2"),
366+
),
367+
]);
368+
await page.waitForLoadState("networkidle");
369+
await expect(hmrStatus).toHaveText("HMR updated: 3");
370+
await expect(serverComponent).toHaveText(
371+
"Imported Server Component HMR: 2",
372+
);
373+
await expect(clientComponent).toHaveText(
374+
"Imported Client Component HMR: 3",
375+
);
376+
await expect(clientButton).toHaveText("Count: 1");
377+
await expect(hdrStatus).toHaveText(
378+
"HDR updated: route & direct 2 & indirect 2",
379+
);
380+
await expect(input).toHaveValue("stateful");
381+
expect(page.errors).toEqual([]);
382+
});
383+
});

0 commit comments

Comments
 (0)