tangled
alpha
login
or
join now
danabra.mov
/
rscexplorer
37
fork
atom
A tool for people curious about the React Server Components protocol
rscexplorer.dev/
rsc
react
37
fork
atom
overview
issues
pulls
pipelines
add ci and unflake tests
danabra.mov
2 months ago
645c6477
fc7e4e64
+384
-108
16 changed files
expand all
collapse all
unified
split
.github
workflows
ci.yml
package.json
src
client
samples.js
tests
async.spec.js
bound.spec.js
clientref.spec.js
counter.spec.js
errors.spec.js
form.spec.js
globalSetup.js
hello.spec.js
helpers.js
kitchensink.spec.js
pagination.spec.js
refresh.spec.js
vitest.config.js
+23
.github/workflows/ci.yml
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
name: CI
2
+
3
+
on:
4
+
push:
5
+
branches: [main]
6
+
pull_request:
7
+
branches: [main]
8
+
9
+
jobs:
10
+
test:
11
+
runs-on: ubuntu-latest
12
+
steps:
13
+
- uses: actions/checkout@v4
14
+
- uses: actions/setup-node@v4
15
+
with:
16
+
node-version: 22
17
+
cache: npm
18
+
- run: npm install
19
+
- run: npm run lint
20
+
- run: npm run format:check
21
+
- run: npx playwright install --with-deps chromium
22
+
- run: npm test
23
+
- run: npm run build:versions
+1
-1
package.json
···
10
"build:versions": "node scripts/build-version.js --all",
11
"dev": "vite",
12
"preview": "vite preview",
13
-
"deploy": "npm install && npm test && npm run build:versions && wrangler pages deploy dist --project-name=rscexplorer",
14
"test": "vitest run --reporter=verbose",
15
"lint": "eslint .",
16
"format": "prettier --write .",
···
10
"build:versions": "node scripts/build-version.js --all",
11
"dev": "vite",
12
"preview": "vite preview",
13
+
"deploy": "npm install && npm run lint && npm test && npm run build:versions && wrangler pages deploy dist --project-name=rscexplorer",
14
"test": "vitest run --reporter=verbose",
15
"lint": "eslint .",
16
"format": "prettier --write .",
+1
-1
src/client/samples.js
···
248
borderRadius: 8,
249
textAlign: 'center'
250
}}>
251
-
<p style={{ fontFamily: 'monospace', fontSize: 32, margin: 0 }}>{seconds}s</p>
252
</div>
253
)
254
}
···
248
borderRadius: 8,
249
textAlign: 'center'
250
}}>
251
+
<p style={{ fontFamily: 'monospace', fontSize: 32, margin: 0 }}>Timer: {seconds}s</p>
252
</div>
253
)
254
}
+12
-5
tests/async.spec.js
···
1
import { test, expect, beforeAll, afterAll, afterEach } from "vitest";
2
-
import { chromium } from "playwright";
3
-
import { createHelpers } from "./helpers.js";
4
5
let browser, page, h;
6
7
beforeAll(async () => {
8
-
browser = await chromium.launch();
9
page = await browser.newPage();
10
h = createHelpers(page);
11
});
···
32
</Suspense>
33
</div>"
34
`);
35
-
expect(await h.preview()).toMatchInlineSnapshot(`"Async Component Loading..."`);
0
0
0
0
36
37
// Tree changes when async data resolves
38
expect(await h.stepAll()).toMatchInlineSnapshot(`
···
45
</Suspense>
46
</div>"
47
`);
48
-
expect(await h.preview()).toMatchInlineSnapshot(`"Async Component Loading..."`);
0
0
0
0
49
});
···
1
import { test, expect, beforeAll, afterAll, afterEach } from "vitest";
2
+
import { createHelpers, launchBrowser } from "./helpers.js";
0
3
4
let browser, page, h;
5
6
beforeAll(async () => {
7
+
browser = await launchBrowser();
8
page = await browser.newPage();
9
h = createHelpers(page);
10
});
···
31
</Suspense>
32
</div>"
33
`);
34
+
expect(await h.preview("Loading")).toMatchInlineSnapshot(`
35
+
"Async Component
36
+
37
+
Loading..."
38
+
`);
39
40
// Tree changes when async data resolves
41
expect(await h.stepAll()).toMatchInlineSnapshot(`
···
48
</Suspense>
49
</div>"
50
`);
51
+
expect(await h.preview("Data loaded")).toMatchInlineSnapshot(`
52
+
"Async Component
53
+
54
+
Data loaded!"
55
+
`);
56
});
+62
-15
tests/bound.spec.js
···
1
import { test, expect, beforeAll, afterAll, afterEach } from "vitest";
2
-
import { chromium } from "playwright";
3
-
import { createHelpers } from "./helpers.js";
4
5
let browser, page, h;
6
7
beforeAll(async () => {
8
-
browser = await chromium.launch();
9
page = await browser.newPage();
10
h = createHelpers(page);
11
});
···
31
<Greeter action={[Function: bound greet]} />
32
</div>"
33
`);
34
-
expect(await h.preview()).toMatchInlineSnapshot(
35
-
`"Bound Actions Same action, different bound greetings: Greet Greet Greet"`,
0
0
0
0
0
0
0
0
36
);
37
});
38
···
49
<Greeter action={[Function: bound greet]} />
50
</div>"
51
`);
52
-
expect(await h.preview()).toMatchInlineSnapshot(
53
-
`"Bound Actions Same action, different bound greetings: Greet Greet Greet"`,
0
0
0
0
0
0
0
0
54
);
55
56
// Fill all three inputs and submit all three forms
···
67
68
// First action pending
69
expect(await h.stepAll()).toMatchInlineSnapshot(`"Pending"`);
70
-
expect(await h.preview()).toMatchInlineSnapshot(
71
-
`"Bound Actions Same action, different bound greetings: Greet Greet Greet"`,
0
0
0
0
0
0
0
0
72
);
73
74
// First action resolves - Hello greeting (second still pending)
75
expect(await h.stepAll()).toMatchInlineSnapshot(`"Pending"`);
76
-
expect(await h.preview()).toMatchInlineSnapshot(
77
-
`"Bound Actions Same action, different bound greetings: GreetHello, Alice! Greet Greet"`,
0
0
0
0
0
0
0
0
78
);
79
80
// Second action resolves - Howdy greeting (third still pending)
81
expect(await h.stepAll()).toMatchInlineSnapshot(`"Pending"`);
82
-
expect(await h.preview()).toMatchInlineSnapshot(
83
-
`"Bound Actions Same action, different bound greetings: GreetHello, Alice! GreetHowdy, Bob! Greet"`,
0
0
0
0
0
0
0
0
84
);
85
86
// Third action resolves - Hey greeting
87
expect(await h.stepAll()).toMatchInlineSnapshot(`""Hey, Charlie!""`);
88
-
expect(await h.preview()).toMatchInlineSnapshot(
89
-
`"Bound Actions Same action, different bound greetings: GreetHello, Alice! GreetHowdy, Bob! GreetHey, Charlie!"`,
0
0
0
0
0
0
0
0
90
);
91
});
···
1
import { test, expect, beforeAll, afterAll, afterEach } from "vitest";
2
+
import { createHelpers, launchBrowser } from "./helpers.js";
0
3
4
let browser, page, h;
5
6
beforeAll(async () => {
7
+
browser = await launchBrowser();
8
page = await browser.newPage();
9
h = createHelpers(page);
10
});
···
30
<Greeter action={[Function: bound greet]} />
31
</div>"
32
`);
33
+
expect(await h.preview("bound greetings")).toMatchInlineSnapshot(
34
+
`
35
+
"Bound Actions
36
+
37
+
Same action, different bound greetings:
38
+
39
+
Greet
40
+
Greet
41
+
Greet"
42
+
`,
43
);
44
});
45
···
56
<Greeter action={[Function: bound greet]} />
57
</div>"
58
`);
59
+
expect(await h.preview("bound greetings")).toMatchInlineSnapshot(
60
+
`
61
+
"Bound Actions
62
+
63
+
Same action, different bound greetings:
64
+
65
+
Greet
66
+
Greet
67
+
Greet"
68
+
`,
69
);
70
71
// Fill all three inputs and submit all three forms
···
82
83
// First action pending
84
expect(await h.stepAll()).toMatchInlineSnapshot(`"Pending"`);
85
+
expect(await h.preview("bound greetings")).toMatchInlineSnapshot(
86
+
`
87
+
"Bound Actions
88
+
89
+
Same action, different bound greetings:
90
+
91
+
Greet
92
+
Greet
93
+
Greet"
94
+
`,
95
);
96
97
// First action resolves - Hello greeting (second still pending)
98
expect(await h.stepAll()).toMatchInlineSnapshot(`"Pending"`);
99
+
expect(await h.preview("Hello, Alice")).toMatchInlineSnapshot(
100
+
`
101
+
"Bound Actions
102
+
103
+
Same action, different bound greetings:
104
+
105
+
GreetHello, Alice!
106
+
Greet
107
+
Greet"
108
+
`,
109
);
110
111
// Second action resolves - Howdy greeting (third still pending)
112
expect(await h.stepAll()).toMatchInlineSnapshot(`"Pending"`);
113
+
expect(await h.preview("Howdy, Bob")).toMatchInlineSnapshot(
114
+
`
115
+
"Bound Actions
116
+
117
+
Same action, different bound greetings:
118
+
119
+
GreetHello, Alice!
120
+
GreetHowdy, Bob!
121
+
Greet"
122
+
`,
123
);
124
125
// Third action resolves - Hey greeting
126
expect(await h.stepAll()).toMatchInlineSnapshot(`""Hey, Charlie!""`);
127
+
expect(await h.preview("Hey, Charlie")).toMatchInlineSnapshot(
128
+
`
129
+
"Bound Actions
130
+
131
+
Same action, different bound greetings:
132
+
133
+
GreetHello, Alice!
134
+
GreetHowdy, Bob!
135
+
GreetHey, Charlie!"
136
+
`,
137
);
138
});
+9
-4
tests/clientref.spec.js
···
1
import { test, expect, beforeAll, afterAll, afterEach } from "vitest";
2
-
import { chromium } from "playwright";
3
-
import { createHelpers } from "./helpers.js";
4
5
let browser, page, h;
6
7
beforeAll(async () => {
8
-
browser = await chromium.launch();
9
page = await browser.newPage();
10
h = createHelpers(page);
11
});
···
37
</div>
38
</div>"
39
`);
40
-
expect(await h.preview()).toMatchInlineSnapshot(`"Client Reference Dark theme Light theme"`);
0
0
0
0
0
0
41
});
···
1
import { test, expect, beforeAll, afterAll, afterEach } from "vitest";
2
+
import { createHelpers, launchBrowser } from "./helpers.js";
0
3
4
let browser, page, h;
5
6
beforeAll(async () => {
7
+
browser = await launchBrowser();
8
page = await browser.newPage();
9
h = createHelpers(page);
10
});
···
36
</div>
37
</div>"
38
`);
39
+
expect(await h.preview("Dark theme")).toMatchInlineSnapshot(
40
+
`
41
+
"Client Reference
42
+
Dark theme
43
+
Light theme"
44
+
`,
45
+
);
46
});
+18
-5
tests/counter.spec.js
···
1
import { test, expect, beforeAll, afterAll, afterEach } from "vitest";
2
-
import { chromium } from "playwright";
3
-
import { createHelpers } from "./helpers.js";
4
5
let browser, page, h;
6
7
beforeAll(async () => {
8
-
browser = await chromium.launch();
9
page = await browser.newPage();
10
h = createHelpers(page);
11
});
···
27
<Counter initialCount={0} />
28
</div>"
29
`);
30
-
expect(await h.preview()).toMatchInlineSnapshot(`"Counter Count: 0 − +"`);
0
0
0
0
0
0
0
31
32
// Client interactivity works
33
await h.frame().locator(".preview-container button").last().click();
34
-
expect(await h.preview()).toContain("Count: 1");
0
0
0
0
0
0
0
35
});
···
1
import { test, expect, beforeAll, afterAll, afterEach } from "vitest";
2
+
import { createHelpers, launchBrowser } from "./helpers.js";
0
3
4
let browser, page, h;
5
6
beforeAll(async () => {
7
+
browser = await launchBrowser();
8
page = await browser.newPage();
9
h = createHelpers(page);
10
});
···
26
<Counter initialCount={0} />
27
</div>"
28
`);
29
+
expect(await h.preview("Count: 0")).toMatchInlineSnapshot(`
30
+
"Counter
31
+
32
+
Count: 0
33
+
34
+
−
35
+
+"
36
+
`);
37
38
// Client interactivity works
39
await h.frame().locator(".preview-container button").last().click();
40
+
expect(await h.preview("Count: 1")).toMatchInlineSnapshot(`
41
+
"Counter
42
+
43
+
Count: 1
44
+
45
+
−
46
+
+"
47
+
`);
48
});
+15
-6
tests/errors.spec.js
···
1
import { test, expect, beforeAll, afterAll, afterEach } from "vitest";
2
-
import { chromium } from "playwright";
3
-
import { createHelpers } from "./helpers.js";
4
5
let browser, page, h;
6
7
beforeAll(async () => {
8
-
browser = await chromium.launch();
9
page = await browser.newPage();
10
h = createHelpers(page);
11
});
···
44
</ErrorBoundary>
45
</div>"
46
`);
47
-
expect(await h.preview()).toContain("Error Handling");
0
0
0
0
48
49
// After async resolves with error, error boundary catches it
50
expect(await h.stepAll()).toMatchInlineSnapshot(`
···
69
</ErrorBoundary>
70
</div>"
71
`);
72
-
expect(await h.preview()).toContain("Failed to load user");
73
-
expect(await h.preview()).toContain("Please try again later");
0
0
0
0
0
0
74
});
···
1
import { test, expect, beforeAll, afterAll, afterEach } from "vitest";
2
+
import { createHelpers, launchBrowser } from "./helpers.js";
0
3
4
let browser, page, h;
5
6
beforeAll(async () => {
7
+
browser = await launchBrowser();
8
page = await browser.newPage();
9
h = createHelpers(page);
10
});
···
43
</ErrorBoundary>
44
</div>"
45
`);
46
+
expect(await h.preview("Loading user")).toMatchInlineSnapshot(`
47
+
"Error Handling
48
+
49
+
Loading user..."
50
+
`);
51
52
// After async resolves with error, error boundary catches it
53
expect(await h.stepAll()).toMatchInlineSnapshot(`
···
72
</ErrorBoundary>
73
</div>"
74
`);
75
+
expect(await h.preview("Failed to load user")).toMatchInlineSnapshot(
76
+
`
77
+
"Error Handling
78
+
Failed to load user
79
+
80
+
Please try again later."
81
+
`,
82
+
);
83
});
+18
-6
tests/form.spec.js
···
1
import { test, expect, beforeAll, afterAll, afterEach } from "vitest";
2
-
import { chromium } from "playwright";
3
-
import { createHelpers } from "./helpers.js";
4
5
let browser, page, h;
6
7
beforeAll(async () => {
8
-
browser = await chromium.launch();
9
page = await browser.newPage();
10
h = createHelpers(page);
11
});
···
27
<Form greetAction={[Function: greet]} />
28
</div>"
29
`);
30
-
expect(await h.preview()).toMatchInlineSnapshot(`"Form Action Greet"`);
0
0
0
31
32
// Submit form
33
await h.frame().locator('.preview-container input[name="name"]').fill("World");
34
await h.frame().locator(".preview-container button").click();
35
-
expect(await h.preview()).toMatchInlineSnapshot(`"Form Action Sending..."`);
0
0
0
36
37
// Action response
38
expect(await h.stepAll()).toMatchInlineSnapshot(`"{ message: "Hello, World!", error: null }"`);
39
-
expect(await h.preview()).toMatchInlineSnapshot(`"Form Action Greet Hello, World!"`);
0
0
0
0
0
0
0
40
});
···
1
import { test, expect, beforeAll, afterAll, afterEach } from "vitest";
2
+
import { createHelpers, launchBrowser } from "./helpers.js";
0
3
4
let browser, page, h;
5
6
beforeAll(async () => {
7
+
browser = await launchBrowser();
8
page = await browser.newPage();
9
h = createHelpers(page);
10
});
···
26
<Form greetAction={[Function: greet]} />
27
</div>"
28
`);
29
+
expect(await h.preview("Greet")).toMatchInlineSnapshot(`
30
+
"Form Action
31
+
Greet"
32
+
`);
33
34
// Submit form
35
await h.frame().locator('.preview-container input[name="name"]').fill("World");
36
await h.frame().locator(".preview-container button").click();
37
+
expect(await h.preview("Sending")).toMatchInlineSnapshot(`
38
+
"Form Action
39
+
Sending..."
40
+
`);
41
42
// Action response
43
expect(await h.stepAll()).toMatchInlineSnapshot(`"{ message: "Hello, World!", error: null }"`);
44
+
expect(await h.preview("Hello, World")).toMatchInlineSnapshot(
45
+
`
46
+
"Form Action
47
+
Greet
48
+
49
+
Hello, World!"
50
+
`,
51
+
);
52
});
+29
-27
tests/globalSetup.js
···
1
-
import { spawn, execSync } from "child_process";
2
-
3
-
let devServer;
4
-
5
-
export async function setup() {
6
-
await new Promise((resolve, reject) => {
7
-
devServer = spawn("npm", ["run", "dev", "--", "--port", "5599"], {
8
-
stdio: ["ignore", "pipe", "pipe"],
9
-
detached: false,
10
-
});
11
12
-
devServer.stdout.on("data", (data) => {
13
-
if (data.toString().includes("Local:")) {
14
-
resolve();
15
-
}
16
-
});
17
18
-
devServer.stderr.on("data", (data) => {
19
-
console.error(data.toString());
20
-
});
0
0
0
0
0
0
0
0
0
0
21
22
-
devServer.on("error", reject);
0
0
0
0
23
24
-
// Timeout after 30s
25
-
setTimeout(() => reject(new Error("Dev server failed to start")), 30000);
0
0
0
26
});
0
0
27
}
28
29
export async function teardown() {
30
-
if (devServer) {
31
-
devServer.kill("SIGTERM");
32
-
// Also kill any process on the port
33
-
try {
34
-
execSync("lsof -ti:5599 | xargs kill -9 2>/dev/null || true");
35
-
} catch {}
36
}
37
}
···
1
+
import { spawn } from "child_process";
0
0
0
0
0
0
0
0
0
2
3
+
let server;
0
0
0
0
4
5
+
async function waitForServer(url, timeout = 30000) {
6
+
const start = Date.now();
7
+
while (Date.now() - start < timeout) {
8
+
try {
9
+
const res = await fetch(url);
10
+
if (res.ok) return;
11
+
} catch {
12
+
// Server not ready yet
13
+
}
14
+
await new Promise((r) => setTimeout(r, 100));
15
+
}
16
+
throw new Error("Test server start timeout");
17
+
}
18
19
+
export async function setup() {
20
+
server = spawn("npx", ["vite", "--port", "5599", "--strictPort"], {
21
+
stdio: "inherit",
22
+
shell: true,
23
+
});
24
25
+
server.on("close", (code) => {
26
+
if (code === 1) {
27
+
console.error("\n\x1b[31mTest server failed to start (port 5599 in use?)\x1b[0m\n");
28
+
process.exit(1);
29
+
}
30
});
31
+
32
+
await waitForServer("http://localhost:5599");
33
}
34
35
export async function teardown() {
36
+
if (server) {
37
+
server.kill();
0
0
0
0
38
}
39
}
+3
-4
tests/hello.spec.js
···
1
import { test, expect, beforeAll, afterAll, afterEach } from "vitest";
2
-
import { chromium } from "playwright";
3
-
import { createHelpers } from "./helpers.js";
4
5
let browser, page, h;
6
7
beforeAll(async () => {
8
-
browser = await chromium.launch();
9
page = await browser.newPage();
10
h = createHelpers(page);
11
});
···
22
await h.load("hello");
23
24
expect(await h.stepAll()).toMatchInlineSnapshot(`"<h1>Hello World</h1>"`);
25
-
expect(await h.preview()).toMatchInlineSnapshot(`"Hello World"`);
26
});
···
1
import { test, expect, beforeAll, afterAll, afterEach } from "vitest";
2
+
import { createHelpers, launchBrowser } from "./helpers.js";
0
3
4
let browser, page, h;
5
6
beforeAll(async () => {
7
+
browser = await launchBrowser();
8
page = await browser.newPage();
9
h = createHelpers(page);
10
});
···
21
await h.load("hello");
22
23
expect(await h.stepAll()).toMatchInlineSnapshot(`"<h1>Hello World</h1>"`);
24
+
expect(await h.preview("Hello World")).toMatchInlineSnapshot(`"Hello World"`);
25
});
+12
-2
tests/helpers.js
···
1
import { expect } from "vitest";
0
0
0
0
0
0
2
3
let prevRowTexts = [];
4
let prevStatuses = [];
···
25
}
26
27
async function getPreviewText() {
28
-
return (await frameRef.locator(".preview-container").innerText()).trim().replace(/\s+/g, " ");
29
}
30
31
async function doStep() {
···
130
return await tree();
131
}
132
133
-
async function preview() {
0
0
0
0
134
const current = await getPreviewText();
135
if (current !== prevPreview) {
136
prevPreview = current;
···
1
import { expect } from "vitest";
2
+
import { chromium } from "playwright";
3
+
4
+
export async function launchBrowser() {
5
+
const executablePath = process.env.CHROMIUM_PATH;
6
+
return chromium.launch(executablePath ? { executablePath } : undefined);
7
+
}
8
9
let prevRowTexts = [];
10
let prevStatuses = [];
···
31
}
32
33
async function getPreviewText() {
34
+
return (await frameRef.locator(".preview-container").innerText()).trim();
35
}
36
37
async function doStep() {
···
136
return await tree();
137
}
138
139
+
async function preview(waitFor) {
140
+
if (waitFor) {
141
+
// Wait for preview to contain the marker
142
+
await expect.poll(() => getPreviewText(), { timeout: 10000 }).toContain(waitFor);
143
+
}
144
const current = await getPreviewText();
145
if (current !== prevPreview) {
146
prevPreview = current;
+62
-6
tests/kitchensink.spec.js
···
1
import { test, expect, beforeAll, afterAll, afterEach } from "vitest";
2
-
import { chromium } from "playwright";
3
-
import { createHelpers } from "./helpers.js";
4
5
let browser, page, h;
6
7
beforeAll(async () => {
8
-
browser = await chromium.launch();
9
page = await browser.newPage();
10
h = createHelpers(page);
11
});
···
36
</Suspense>
37
</div>"
38
`);
39
-
expect(await h.preview()).toMatchInlineSnapshot(`"Kitchen Sink Loading..."`);
0
0
0
0
40
41
// Step to resolve async content (with delayed promise still pending)
42
expect(await h.stepAll()).toMatchInlineSnapshot(`
···
133
</Suspense>
134
</div>"
135
`);
136
-
expect(await h.preview()).toMatchInlineSnapshot(`"Kitchen Sink Loading..."`);
0
0
0
0
137
138
// Step to resolve delayed promise
139
expect(await h.stepAll()).toMatchInlineSnapshot(`
···
230
</Suspense>
231
</div>"
232
`);
233
-
expect(await h.preview()).toMatchInlineSnapshot(`"Kitchen Sink Loading..."`);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
234
});
···
1
import { test, expect, beforeAll, afterAll, afterEach } from "vitest";
2
+
import { createHelpers, launchBrowser } from "./helpers.js";
0
3
4
let browser, page, h;
5
6
beforeAll(async () => {
7
+
browser = await launchBrowser();
8
page = await browser.newPage();
9
h = createHelpers(page);
10
});
···
35
</Suspense>
36
</div>"
37
`);
38
+
expect(await h.preview("Loading...")).toMatchInlineSnapshot(`
39
+
"Kitchen Sink
40
+
41
+
Loading..."
42
+
`);
43
44
// Step to resolve async content (with delayed promise still pending)
45
expect(await h.stepAll()).toMatchInlineSnapshot(`
···
136
</Suspense>
137
</div>"
138
`);
139
+
expect(await h.preview("Loading...")).toMatchInlineSnapshot(`
140
+
"Kitchen Sink
141
+
142
+
Loading..."
143
+
`);
144
145
// Step to resolve delayed promise
146
expect(await h.stepAll()).toMatchInlineSnapshot(`
···
237
</Suspense>
238
</div>"
239
`);
240
+
expect(await h.preview("hello world")).toMatchInlineSnapshot(
241
+
`
242
+
"Kitchen Sink
243
+
primitives
244
+
null: null
245
+
true: true
246
+
false: false
247
+
int: 42
248
+
float: 3.14159
249
+
string: hello world
250
+
empty:
251
+
dollar: $special
252
+
unicode: Hello 世界 🌍
253
+
special
254
+
negZero: 0
255
+
inf: Infinity
256
+
negInf: -Infinity
257
+
nan: NaN
258
+
types
259
+
date: 2024-01-15T12:00:00.000Z
260
+
bigint: 12345678901234567890n
261
+
symbol: Symbol(mySymbol)
262
+
collections
263
+
map: Map(2)
264
+
set: Set(3)
265
+
formData: FormData
266
+
blob: Blob(5)
267
+
arrays
268
+
simple: [3 items]
269
+
sparse: [4 items]
270
+
nested: [2 items]
271
+
objects
272
+
simple: {...}
273
+
nested: {...}
274
+
elements
275
+
div: {...}
276
+
fragment: [2 items]
277
+
suspense: {...}
278
+
promises
279
+
resolved: {...}
280
+
delayed: {...}
281
+
iterators
282
+
sync: {...}
283
+
refs
284
+
dup: {...}
285
+
cyclic: {...}
286
+
action
287
+
Call serverAction"
288
+
`,
289
+
);
290
});
+105
-13
tests/pagination.spec.js
···
1
import { test, expect, beforeAll, afterAll, afterEach } from "vitest";
2
-
import { chromium } from "playwright";
3
-
import { createHelpers } from "./helpers.js";
4
5
let browser, page, h;
6
7
beforeAll(async () => {
8
-
browser = await chromium.launch();
9
page = await browser.newPage();
10
h = createHelpers(page);
11
});
···
32
</Suspense>
33
</div>"
34
`);
35
-
expect(await h.preview()).toMatchInlineSnapshot(`"Pagination Loading recipes..."`);
0
0
0
0
0
0
36
37
// Then resolves to Paginator with initial items
38
expect(await h.stepAll()).toMatchInlineSnapshot(`
···
76
</Suspense>
77
</div>"
78
`);
79
-
expect(await h.preview()).toMatchInlineSnapshot(`"Pagination Loading recipes..."`);
0
0
0
0
0
0
0
0
0
0
0
0
0
80
81
// First Load More
82
await h.frame().locator(".preview-container button").click();
83
-
expect(await h.preview()).toMatchInlineSnapshot(
84
-
`"Pagination Pasta Carbonara 25 min · Medium Grilled Cheese 10 min · Easy Loading..."`,
0
0
0
0
0
0
0
0
0
0
0
85
);
86
87
// Action returns new items
···
123
hasMore: true
124
}"
125
`);
126
-
expect(await h.preview()).toMatchInlineSnapshot(
127
-
`"Pagination Pasta Carbonara 25 min · Medium Grilled Cheese 10 min · Easy Chicken Stir Fry 20 min · Easy Beef Tacos 30 min · Medium Load More"`,
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
128
);
129
130
// Second Load More
131
await h.frame().locator(".preview-container button").click();
132
-
expect(await h.preview()).toMatchInlineSnapshot(
133
-
`"Pagination Pasta Carbonara 25 min · Medium Grilled Cheese 10 min · Easy Chicken Stir Fry 20 min · Easy Beef Tacos 30 min · Medium Loading..."`,
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
134
);
135
136
// Final items, hasMore: false
···
172
hasMore: false
173
}"
174
`);
175
-
expect(await h.preview()).toMatchInlineSnapshot(
176
-
`"Pagination Pasta Carbonara 25 min · Medium Grilled Cheese 10 min · Easy Chicken Stir Fry 20 min · Easy Beef Tacos 30 min · Medium Caesar Salad 15 min · Easy Mushroom Risotto 45 min · Hard"`,
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
177
);
178
179
// No more items - button should be gone
···
1
import { test, expect, beforeAll, afterAll, afterEach } from "vitest";
2
+
import { createHelpers, launchBrowser } from "./helpers.js";
0
3
4
let browser, page, h;
5
6
beforeAll(async () => {
7
+
browser = await launchBrowser();
8
page = await browser.newPage();
9
h = createHelpers(page);
10
});
···
31
</Suspense>
32
</div>"
33
`);
34
+
expect(await h.preview("Loading recipes")).toMatchInlineSnapshot(
35
+
`
36
+
"Pagination
37
+
38
+
Loading recipes..."
39
+
`,
40
+
);
41
42
// Then resolves to Paginator with initial items
43
expect(await h.stepAll()).toMatchInlineSnapshot(`
···
81
</Suspense>
82
</div>"
83
`);
84
+
expect(await h.preview("Grilled Cheese")).toMatchInlineSnapshot(
85
+
`
86
+
"Pagination
87
+
Pasta Carbonara
88
+
89
+
25 min · Medium
90
+
91
+
Grilled Cheese
92
+
93
+
10 min · Easy
94
+
95
+
Load More"
96
+
`,
97
+
);
98
99
// First Load More
100
await h.frame().locator(".preview-container button").click();
101
+
expect(await h.preview("Loading...")).toMatchInlineSnapshot(
102
+
`
103
+
"Pagination
104
+
Pasta Carbonara
105
+
106
+
25 min · Medium
107
+
108
+
Grilled Cheese
109
+
110
+
10 min · Easy
111
+
112
+
Loading..."
113
+
`,
114
);
115
116
// Action returns new items
···
152
hasMore: true
153
}"
154
`);
155
+
expect(await h.preview("Beef Tacos")).toMatchInlineSnapshot(
156
+
`
157
+
"Pagination
158
+
Pasta Carbonara
159
+
160
+
25 min · Medium
161
+
162
+
Grilled Cheese
163
+
164
+
10 min · Easy
165
+
166
+
Chicken Stir Fry
167
+
168
+
20 min · Easy
169
+
170
+
Beef Tacos
171
+
172
+
30 min · Medium
173
+
174
+
Load More"
175
+
`,
176
);
177
178
// Second Load More
179
await h.frame().locator(".preview-container button").click();
180
+
expect(await h.preview("Loading...")).toMatchInlineSnapshot(
181
+
`
182
+
"Pagination
183
+
Pasta Carbonara
184
+
185
+
25 min · Medium
186
+
187
+
Grilled Cheese
188
+
189
+
10 min · Easy
190
+
191
+
Chicken Stir Fry
192
+
193
+
20 min · Easy
194
+
195
+
Beef Tacos
196
+
197
+
30 min · Medium
198
+
199
+
Loading..."
200
+
`,
201
);
202
203
// Final items, hasMore: false
···
239
hasMore: false
240
}"
241
`);
242
+
expect(await h.preview("Mushroom Risotto")).toMatchInlineSnapshot(
243
+
`
244
+
"Pagination
245
+
Pasta Carbonara
246
+
247
+
25 min · Medium
248
+
249
+
Grilled Cheese
250
+
251
+
10 min · Easy
252
+
253
+
Chicken Stir Fry
254
+
255
+
20 min · Easy
256
+
257
+
Beef Tacos
258
+
259
+
30 min · Medium
260
+
261
+
Caesar Salad
262
+
263
+
15 min · Easy
264
+
265
+
Mushroom Risotto
266
+
267
+
45 min · Hard"
268
+
`,
269
);
270
271
// No more items - button should be gone
+13
-12
tests/refresh.spec.js
···
1
import { test, expect, beforeAll, afterAll, afterEach } from "vitest";
2
-
import { chromium } from "playwright";
3
-
import { createHelpers } from "./helpers.js";
4
5
let browser, page, h;
6
7
beforeAll(async () => {
8
-
browser = await chromium.launch();
9
page = await browser.newPage();
10
h = createHelpers(page);
11
});
···
33
</Suspense>
34
</div>"
35
`);
36
-
expect(await h.preview()).toMatchInlineSnapshot(
37
-
`"Router Refresh Client state persists across server navigations Loading..."`,
0
0
0
0
0
0
38
);
39
40
// Step to resolve async Timer content (color is random hsl value)
···
42
expect(tree).toMatch(/Router Refresh/);
43
expect(tree).toMatch(/<Timer color="hsl\(\d+, 70%, 85%\)" \/>/);
44
45
-
// Wait for preview to render the timer (shows "0s" or similar)
46
-
await h.waitFor(
47
-
() => /\d+s/.test(document.querySelector(".preview-container")?.textContent || ""),
48
-
{ timeout: 5000 },
49
-
);
50
-
const preview = await h.preview();
51
-
expect(preview).toMatch(/Router Refresh.*\d+s.*Refresh/);
52
});
···
1
import { test, expect, beforeAll, afterAll, afterEach } from "vitest";
2
+
import { createHelpers, launchBrowser } from "./helpers.js";
0
3
4
let browser, page, h;
5
6
beforeAll(async () => {
7
+
browser = await launchBrowser();
8
page = await browser.newPage();
9
h = createHelpers(page);
10
});
···
32
</Suspense>
33
</div>"
34
`);
35
+
expect(await h.preview("Loading...")).toMatchInlineSnapshot(
36
+
`
37
+
"Router Refresh
38
+
39
+
Client state persists across server navigations
40
+
41
+
Loading..."
42
+
`,
43
);
44
45
// Step to resolve async Timer content (color is random hsl value)
···
47
expect(tree).toMatch(/Router Refresh/);
48
expect(tree).toMatch(/<Timer color="hsl\(\d+, 70%, 85%\)" \/>/);
49
50
+
// Wait for preview to render the timer
51
+
const preview = await h.preview("Timer:");
52
+
expect(preview).toMatch(/Timer: \d+s[\s\S]*Refresh/);
0
0
0
0
53
});
+1
-1
vitest.config.js
···
2
3
export default defineConfig({
4
test: {
5
-
testTimeout: 15000,
6
fileParallelism: true,
7
globalSetup: "./tests/globalSetup.js",
8
},
···
2
3
export default defineConfig({
4
test: {
5
+
testTimeout: 30000,
6
fileParallelism: true,
7
globalSetup: "./tests/globalSetup.js",
8
},