Skip to content

Commit 64dca6a

Browse files
authored
Merge pull request facebook#1 from hi-ogawa/react-server-dom-vite-mini-fixture
React server dom vite mini fixture
2 parents bb66c49 + 5731f7b commit 64dca6a

28 files changed

+2039
-31
lines changed

fixtures/flight-vite-mini/.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.DS_Store
2+
dist
3+
node_modules
4+
/test-results/
5+
/playwright-report/
6+
/blob-report/
7+
/playwright/.cache/
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { type Page, expect, test } from "@playwright/test";
2+
import { createEditor } from "./helper";
3+
4+
test("client reference", async ({ page }) => {
5+
await page.goto("/");
6+
await page.getByText("[hydrated: 1]").click();
7+
await page.getByText("Client counter: 0").click();
8+
await page
9+
.getByTestId("client-counter")
10+
.getByRole("button", { name: "+" })
11+
.click();
12+
await page.getByText("Client counter: 1").click();
13+
await page.reload();
14+
await page.getByText("Client counter: 0").click();
15+
});
16+
17+
test("server reference in server @js", async ({ page }) => {
18+
await testServerAction(page);
19+
});
20+
21+
test.describe(() => {
22+
test.use({ javaScriptEnabled: false });
23+
test("server reference in server @nojs", async ({ page }) => {
24+
await testServerAction(page);
25+
});
26+
});
27+
28+
async function testServerAction(page: Page) {
29+
await page.goto("/");
30+
await page.getByText("Server counter: 0").click();
31+
await page
32+
.getByTestId("server-counter")
33+
.getByRole("button", { name: "+" })
34+
.click();
35+
await page.getByText("Server counter: 1").click();
36+
await page.goto("/");
37+
await page.getByText("Server counter: 1").click();
38+
await page
39+
.getByTestId("server-counter")
40+
.getByRole("button", { name: "-" })
41+
.click();
42+
await page.getByText("Server counter: 0").click();
43+
}
44+
45+
test("server reference in client @js", async ({ page }) => {
46+
await testServerAction2(page, { js: true });
47+
});
48+
49+
test.describe(() => {
50+
test.use({ javaScriptEnabled: false });
51+
test("server reference in client @nojs", async ({ page }) => {
52+
await testServerAction2(page, { js: false });
53+
});
54+
});
55+
56+
async function testServerAction2(page: Page, options: { js: boolean }) {
57+
await page.goto("/");
58+
if (options.js) {
59+
await page.getByText("[hydrated: 1]").click();
60+
}
61+
await page.locator('input[name="x"]').fill("2");
62+
await page.locator('input[name="y"]').fill("3");
63+
await page.locator('input[name="y"]').press("Enter");
64+
await expect(page.getByTestId("calculator-answer")).toContainText("5");
65+
await page.locator('input[name="x"]').fill("2");
66+
await page.locator('input[name="y"]').fill("three");
67+
await page.locator('input[name="y"]').press("Enter");
68+
await expect(page.getByTestId("calculator-answer")).toContainText(
69+
"(invalid input)",
70+
);
71+
if (options.js) {
72+
await expect(page.locator('input[name="x"]')).toHaveValue("2");
73+
await expect(page.locator('input[name="y"]')).toHaveValue("three");
74+
} else {
75+
await expect(page.locator('input[name="x"]')).toHaveValue("");
76+
await expect(page.locator('input[name="y"]')).toHaveValue("");
77+
}
78+
}
79+
80+
test("client hmr @dev", async ({ page }) => {
81+
await page.goto("/");
82+
await page.getByText("[hydrated: 1]").click();
83+
// client +1
84+
await page.getByText("Client counter: 0").click();
85+
await page
86+
.getByTestId("client-counter")
87+
.getByRole("button", { name: "+" })
88+
.click();
89+
await page.getByText("Client counter: 1").click();
90+
// edit client
91+
using file = createEditor("src/app/client.tsx");
92+
file.edit((s) => s.replace("Client counter", "Client [EDIT] counter"));
93+
await page.getByText("Client [EDIT] counter: 1").click();
94+
});
95+
96+
test("server hmr @dev", async ({ page }) => {
97+
await page.goto("/");
98+
await page.getByText("[hydrated: 1]").click();
99+
100+
// server +1
101+
await page.getByText("Server counter: 0").click();
102+
await page
103+
.getByTestId("server-counter")
104+
.getByRole("button", { name: "+" })
105+
.click();
106+
await page.getByText("Server counter: 1").click();
107+
108+
// client +1
109+
await page.getByText("Client counter: 0").click();
110+
await page
111+
.getByTestId("client-counter")
112+
.getByRole("button", { name: "+" })
113+
.click();
114+
await page.getByText("Client counter: 1").click();
115+
116+
// edit server
117+
using file = createEditor("src/app/index.tsx");
118+
file.edit((s) => s.replace("Server counter", "Server [EDIT] counter"));
119+
await page.getByText("Server [EDIT] counter: 1").click();
120+
await page.getByText("Client counter: 1").click();
121+
122+
// server -1
123+
await page
124+
.getByTestId("server-counter")
125+
.getByRole("button", { name: "-" })
126+
.click();
127+
await page.getByText("Server [EDIT] counter: 0").click();
128+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import fs from "node:fs";
2+
3+
export function createEditor(filepath: string) {
4+
let init = fs.readFileSync(filepath, "utf-8");
5+
let data = init;
6+
return {
7+
edit(editFn: (data: string) => string) {
8+
data = editFn(data);
9+
fs.writeFileSync(filepath, data);
10+
},
11+
[Symbol.dispose]() {
12+
fs.writeFileSync(filepath, init);
13+
},
14+
};
15+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"private": true,
3+
"type": "module",
4+
"scripts": {
5+
"build-deps": "rm -rf node_modules/{react,react-dom/react-server-dom-vite} && cd ../.. && RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react/index,react/jsx,react.react-server,react-jsx-runtime.react-server,react-jsx-dev-runtime.react-server,react-dom/index,react-dom/client,react-dom/server,react-dom.react-server,react-dom-server.node,react-dom-server-legacy.node,scheduler,react-server-dom-vite/ --type=NODE_DEV,NODE_PROD && cp -r ./build/node_modules/* ./fixtures/flight-vite-mini/node_modules/",
6+
"dev": "vite dev",
7+
"build": "vite build --app",
8+
"preview": "vite preview",
9+
"format": "npx biome format --write",
10+
"test-e2e": "playwright test",
11+
"test-e2e-preview": "E2E_PREVIEW=1 playwright test"
12+
},
13+
"dependencies": {
14+
"react-server-dom-vite": "npm:@jacob-ebey/react-server-dom-vite@19.0.0-experimental.14",
15+
"react": "^19.0.0",
16+
"react-dom": "^19.0.0"
17+
},
18+
"devDependencies": {
19+
"@biomejs/biome": "^1.9.4",
20+
"@playwright/test": "^1.49.0",
21+
"@types/node": "^22.0.0",
22+
"@types/react": "^19.0.1",
23+
"@types/react-dom": "^19.0.1",
24+
"@vitejs/plugin-react": "^4.3.4",
25+
"vite": "^6.0.3"
26+
}
27+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { defineConfig, devices } from "@playwright/test";
2+
3+
const port = Number(process.env.E2E_PORT || 6174);
4+
const isPreview = Boolean(process.env.E2E_PREVIEW);
5+
const command = isPreview
6+
? `yarn preview --port ${port} --strict-port`
7+
: `yarn dev --port ${port} --strict-port`;
8+
9+
export default defineConfig({
10+
testDir: "e2e",
11+
use: {
12+
trace: "on-first-retry",
13+
},
14+
projects: [
15+
{
16+
name: "chromium",
17+
use: {
18+
...devices["Desktop Chrome"],
19+
},
20+
},
21+
],
22+
webServer: {
23+
command,
24+
port,
25+
},
26+
grepInvert: isPreview ? /@dev/ : /@build/,
27+
forbidOnly: !!process.env["CI"],
28+
retries: process.env["CI"] ? 2 : 0,
29+
reporter: "list",
30+
});
24.3 KB
Binary file not shown.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"use server";
2+
3+
export async function add(_prev: unknown, formData: FormData) {
4+
let x = formData.get("x");
5+
let y = formData.get("y");
6+
if (typeof x === "string" && typeof y === "string") {
7+
let x2 = parseFloat(x);
8+
let y2 = parseFloat(y);
9+
if (!Number.isNaN(x2) && !Number.isNaN(y2)) {
10+
return x2 + y2;
11+
}
12+
}
13+
return "(invalid input)";
14+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"use server";
2+
3+
let counter = 0;
4+
5+
export function getCounter() {
6+
return counter;
7+
}
8+
9+
export async function changeCounter(formData: FormData) {
10+
const change = Number(formData.get("change"));
11+
counter += change;
12+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"use client";
2+
3+
import React from "react";
4+
import { add } from "./action-by-client";
5+
6+
export function Counter() {
7+
const [count, setCount] = React.useState(0);
8+
return (
9+
<div data-testid="client-counter" style={{ padding: "0.5rem" }}>
10+
<div>Client counter: {count}</div>
11+
<div>
12+
<button onClick={() => setCount((c) => c - 1)}>-</button>
13+
<button onClick={() => setCount((c) => c + 1)}>+</button>
14+
</div>
15+
</div>
16+
);
17+
}
18+
19+
export function Hydrated() {
20+
return <pre>[hydrated: {Number(useHydrated())}]</pre>;
21+
}
22+
23+
function useHydrated() {
24+
return React.useSyncExternalStore(
25+
React.useCallback(() => () => {}, []),
26+
() => true,
27+
() => false,
28+
);
29+
}
30+
31+
export function Calculator() {
32+
const [returnValue, formAction, _isPending] = React.useActionState(add, null);
33+
const [x, setX] = React.useState("");
34+
const [y, setY] = React.useState("");
35+
36+
return (
37+
<form
38+
action={formAction}
39+
style={{ padding: "0.5rem" }}
40+
data-testid="calculator"
41+
>
42+
<div>Calculator</div>
43+
<div style={{ display: "flex", gap: "0.3rem" }}>
44+
<input
45+
name="x"
46+
style={{ width: "2rem" }}
47+
value={x}
48+
onChange={(e) => setX(e.target.value)}
49+
/>
50+
+
51+
<input
52+
name="y"
53+
style={{ width: "2rem" }}
54+
value={y}
55+
onChange={(e) => setY(e.target.value)}
56+
/>
57+
=<span data-testid="calculator-answer">{returnValue ?? "?"}</span>
58+
</div>
59+
<button hidden></button>
60+
</form>
61+
);
62+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { changeCounter, getCounter } from "./action";
2+
import { Calculator, Counter, Hydrated } from "./client";
3+
4+
export async function IndexPage() {
5+
return (
6+
<div>
7+
<div>server random: {Math.random().toString(36).slice(2)}</div>
8+
<Hydrated />
9+
<Counter />
10+
<form
11+
action={changeCounter}
12+
data-testid="server-counter"
13+
style={{ padding: "0.5rem" }}
14+
>
15+
<div>Server counter: {getCounter()}</div>
16+
<div>
17+
<button name="change" value="-1">
18+
-
19+
</button>
20+
<button name="change" value="+1">
21+
+
22+
</button>
23+
</div>
24+
</form>
25+
<Calculator />
26+
</div>
27+
);
28+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export function Layout(props: React.PropsWithChildren) {
2+
return (
3+
<html>
4+
<head>
5+
<meta charSet="UTF-8" />
6+
<title>react-server</title>
7+
<meta
8+
name="viewport"
9+
content="width=device-width, height=device-height, initial-scale=1.0"
10+
/>
11+
</head>
12+
<body>
13+
<ul>
14+
<li>
15+
<a href="/">Home</a>
16+
</li>
17+
<li>
18+
<a href="/other">Other</a>
19+
</li>
20+
</ul>
21+
{props.children}
22+
</body>
23+
</html>
24+
);
25+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function OtherPage() {
2+
return <div>Other Page</div>;
3+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type React from "react";
2+
import { IndexPage } from ".";
3+
import { Layout } from "./layout";
4+
import OtherPage from "./other";
5+
6+
const routes: Record<string, React.ReactNode> = {
7+
"/": <IndexPage />,
8+
"/other": <OtherPage />,
9+
};
10+
11+
export function Router(props: { url: URL }) {
12+
const page = routes[props.url.pathname] ?? <h4>Not found</h4>;
13+
return <Layout>{page}</Layout>;
14+
}

0 commit comments

Comments
 (0)