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