Skip to content

Commit 5df0070

Browse files
hi-ogawaclaude
andauthored
feat(rsc): support client environment as react-server (#657)
Co-authored-by: Claude <[email protected]>
1 parent ff44ae4 commit 5df0070

27 files changed

+848
-54
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { expect, test, type Page } from '@playwright/test'
2+
import { useFixture } from './fixture'
3+
import { defineStarterTest } from './starter'
4+
5+
test.describe('dev-browser-mode', () => {
6+
// Webkit fails by
7+
// > TypeError: ReadableByteStreamController is not implemented
8+
test.skip(({ browserName }) => browserName === 'webkit')
9+
10+
const f = useFixture({ root: 'examples/browser-mode', mode: 'dev' })
11+
defineStarterTest(f, 'browser-mode')
12+
13+
// action-bind tests copied from basic.test.ts
14+
15+
test('action bind simple', async ({ page }) => {
16+
await page.goto(f.url())
17+
await testActionBindSimple(page)
18+
})
19+
20+
async function testActionBindSimple(page: Page) {
21+
await expect(page.getByTestId('test-server-action-bind-simple')).toHaveText(
22+
'[?]',
23+
)
24+
await page
25+
.getByRole('button', { name: 'test-server-action-bind-simple' })
26+
.click()
27+
await expect(page.getByTestId('test-server-action-bind-simple')).toHaveText(
28+
'true',
29+
)
30+
await page
31+
.getByRole('button', { name: 'test-server-action-bind-reset' })
32+
.click()
33+
}
34+
35+
test('action bind client', async ({ page }) => {
36+
await page.goto(f.url())
37+
await testActionBindClient(page)
38+
})
39+
40+
async function testActionBindClient(page: Page) {
41+
await expect(page.getByTestId('test-server-action-bind-client')).toHaveText(
42+
'[?]',
43+
)
44+
await page
45+
.getByRole('button', { name: 'test-server-action-bind-client' })
46+
.click()
47+
await expect(page.getByTestId('test-server-action-bind-client')).toHaveText(
48+
'true',
49+
)
50+
await page
51+
.getByRole('button', { name: 'test-server-action-bind-reset' })
52+
.click()
53+
}
54+
55+
test('action bind action', async ({ page }) => {
56+
await page.goto(f.url())
57+
await testActionBindAction(page)
58+
})
59+
60+
async function testActionBindAction(page: Page) {
61+
await expect(page.getByTestId('test-server-action-bind-action')).toHaveText(
62+
'[?]',
63+
)
64+
await page
65+
.getByRole('button', { name: 'test-server-action-bind-action' })
66+
.click()
67+
await expect(page.getByTestId('test-server-action-bind-action')).toHaveText(
68+
'[true,true]',
69+
)
70+
await page
71+
.getByRole('button', { name: 'test-server-action-bind-reset' })
72+
.click()
73+
}
74+
})

packages/plugin-rsc/e2e/helper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export async function waitForHydration(page: Page, locator: string = 'body') {
1515
el &&
1616
Object.keys(el).some((key) => key.startsWith('__reactFiber')),
1717
),
18-
{ timeout: 3000 },
18+
{ timeout: 10000 },
1919
)
2020
.toBeTruthy()
2121
}

packages/plugin-rsc/e2e/starter.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ import {
99

1010
export function defineStarterTest(
1111
f: Fixture,
12-
variant?: 'no-ssr' | 'dev-production',
12+
variant?: 'no-ssr' | 'dev-production' | 'browser-mode',
1313
) {
1414
const waitForHydration: typeof waitForHydration_ = (page) =>
15-
waitForHydration_(page, variant === 'no-ssr' ? '#root' : 'body')
15+
waitForHydration_(
16+
page,
17+
variant === 'no-ssr' || variant === 'browser-mode' ? '#root' : 'body',
18+
)
1619

1720
test('basic', async ({ page }) => {
1821
using _ = expectNoPageError(page)
@@ -40,7 +43,7 @@ export function defineStarterTest(
4043
})
4144

4245
testNoJs('server action @nojs', async ({ page }) => {
43-
test.skip(variant === 'no-ssr')
46+
test.skip(variant === 'no-ssr' || variant === 'browser-mode')
4447

4548
await page.goto(f.url())
4649
await page.getByRole('button', { name: 'Server Counter: 1' }).click()
@@ -50,7 +53,11 @@ export function defineStarterTest(
5053
})
5154

5255
test('client hmr', async ({ page }) => {
53-
test.skip(f.mode === 'build' || variant === 'dev-production')
56+
test.skip(
57+
f.mode === 'build' ||
58+
variant === 'dev-production' ||
59+
variant === 'browser-mode',
60+
)
5461

5562
await page.goto(f.url())
5663
await waitForHydration(page)
@@ -80,7 +87,7 @@ export function defineStarterTest(
8087
})
8188

8289
test.describe(() => {
83-
test.skip(f.mode === 'build')
90+
test.skip(f.mode === 'build' || variant === 'browser-mode')
8491

8592
test('server hmr', async ({ page }) => {
8693
await page.goto(f.url())
@@ -113,20 +120,17 @@ export function defineStarterTest(
113120
test('css @js', async ({ page }) => {
114121
await page.goto(f.url())
115122
await waitForHydration(page)
116-
await expect(page.locator('.read-the-docs')).toHaveCSS(
117-
'color',
118-
'rgb(136, 136, 136)',
119-
)
123+
await expect(page.locator('.card').nth(0)).toHaveCSS('padding-left', '16px')
120124
})
121125

122126
test.describe(() => {
123-
test.skip(variant === 'no-ssr')
127+
test.skip(variant === 'no-ssr' || variant === 'browser-mode')
124128

125129
testNoJs('css @nojs', async ({ page }) => {
126130
await page.goto(f.url())
127-
await expect(page.locator('.read-the-docs')).toHaveCSS(
128-
'color',
129-
'rgb(136, 136, 136)',
131+
await expect(page.locator('.card').nth(0)).toHaveCSS(
132+
'padding-left',
133+
'16px',
130134
)
131135
})
132136
})

packages/plugin-rsc/e2e/syntax-error.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,11 @@ test.describe(() => {
164164
)
165165
await expect(async () => {
166166
await page.goto(f.url())
167-
await waitForHydration(page)
168167
await expect(page.getByTestId('client-content')).toHaveText(
169168
'client:fixed',
170169
)
171170
}).toPass()
171+
await waitForHydration(page)
172172
})
173173
})
174174

@@ -197,11 +197,11 @@ test.describe(() => {
197197
)
198198
await expect(async () => {
199199
await page.goto(f.url())
200-
await waitForHydration(page)
201200
await expect(page.getByTestId('server-content')).toHaveText(
202201
'server:fixed',
203202
)
204203
}).toPass()
204+
await waitForHydration(page)
205205
})
206206
})
207207
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[examples/starter](https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc/examples/starter) but entirely on Browser. Inspired by https://github.com/kasperpeulen/vitest-plugin-rsc/
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>RSC Browser Mode</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
8+
<script async type="module" src="/src/framework/main.tsx"></script>
9+
</head>
10+
<body>
11+
<div id="root"></div>
12+
</body>
13+
</html>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "@vitejs/plugin-rsc-examples-browser-mode",
3+
"version": "0.0.0",
4+
"private": true,
5+
"license": "MIT",
6+
"type": "module",
7+
"scripts": {
8+
"dev": "vite",
9+
"build": "false && vite build",
10+
"preview": "false && vite preview"
11+
},
12+
"dependencies": {
13+
"@vitejs/plugin-rsc": "latest",
14+
"react": "^19.1.0",
15+
"react-dom": "^19.1.0"
16+
},
17+
"devDependencies": {
18+
"@types/react": "^19.1.8",
19+
"@types/react-dom": "^19.1.6",
20+
"@vitejs/plugin-react": "latest",
21+
"vite": "^7.0.6"
22+
}
23+
}
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
'use client'
2+
3+
import React from 'react'
4+
5+
export function ActionBindClient() {
6+
const hydrated = React.useSyncExternalStore(
7+
React.useCallback(() => () => {}, []),
8+
() => true,
9+
() => false,
10+
)
11+
return <>{String(hydrated)}</>
12+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use client'
2+
3+
import React from 'react'
4+
5+
export function TestServerActionBindClientForm(props: {
6+
action: () => Promise<React.ReactNode>
7+
}) {
8+
const [result, formAction] = React.useActionState(props.action, '[?]')
9+
10+
return (
11+
<form action={formAction}>
12+
<button>test-server-action-bind-client</button>
13+
<span data-testid="test-server-action-bind-client">{result}</span>
14+
</form>
15+
)
16+
}

0 commit comments

Comments
 (0)