Skip to content

Commit ebb3475

Browse files
RSC: Add RSC+SSR smoke test to CI (#10477)
Co-authored-by: Josh GM Walker <[email protected]>
1 parent d8ee4cd commit ebb3475

File tree

8 files changed

+207
-6
lines changed

8 files changed

+207
-6
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// In this file, all Page components from 'src/pages` are auto-imported. Nested
2+
// directories are supported, and should be uppercase. Each subdirectory will be
3+
// prepended onto the component name.
4+
//
5+
// Examples:
6+
//
7+
// 'src/pages/HomePage/HomePage.js' -> HomePage
8+
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
9+
10+
import { Route } from '@redwoodjs/router/dist/Route'
11+
import { Router } from '@redwoodjs/router/dist/server-router'
12+
import { Set } from '@redwoodjs/router/dist/Set'
13+
14+
import NavigationLayout from './layouts/NavigationLayout/NavigationLayout'
15+
import ScaffoldLayout from './layouts/ScaffoldLayout/ScaffoldLayout'
16+
import AboutPage from './pages/AboutPage/AboutPage'
17+
import EmptyUserEmptyUsersPage from './pages/EmptyUser/EmptyUsersPage/EmptyUsersPage'
18+
import EmptyUserNewEmptyUserPage from './pages/EmptyUser/NewEmptyUserPage/NewEmptyUserPage'
19+
import HomePage from './pages/HomePage/HomePage'
20+
import MultiCellPage from './pages/MultiCellPage/MultiCellPage'
21+
import UserExampleNewUserExamplePage from './pages/UserExample/NewUserExamplePage/NewUserExamplePage'
22+
import UserExampleUserExamplePage from './pages/UserExample/UserExamplePage/UserExamplePage'
23+
import UserExampleUserExamplesPage from './pages/UserExample/UserExamplesPage/UserExamplesPage'
24+
25+
const NotFoundPage = () => {
26+
return <div>Not Found</div>
27+
}
28+
29+
const Routes = ({ location }) => {
30+
return (
31+
<Router location={location}>
32+
<Set wrap={NavigationLayout} rnd={Math.random()}>
33+
<Route path="/" page={HomePage} name="home" />
34+
<Route path="/about" page={AboutPage} name="about" />
35+
<Route path="/multi-cell" page={MultiCellPage} name="multiCell" />
36+
37+
<Set
38+
wrap={ScaffoldLayout}
39+
title="EmptyUsers"
40+
titleTo="emptyUsers"
41+
buttonLabel="New EmptyUser"
42+
buttonTo="newEmptyUser"
43+
>
44+
<Route
45+
path="/empty-users/new"
46+
page={EmptyUserNewEmptyUserPage}
47+
name="newEmptyUser"
48+
/>
49+
<Route
50+
path="/empty-users"
51+
page={EmptyUserEmptyUsersPage}
52+
name="emptyUsers"
53+
/>
54+
</Set>
55+
56+
<Set
57+
wrap={ScaffoldLayout}
58+
title="UserExamples"
59+
titleTo="userExamples"
60+
buttonLabel="New UserExample"
61+
buttonTo="newUserExample"
62+
>
63+
<Route
64+
path="/user-examples/new"
65+
page={UserExampleNewUserExamplePage}
66+
name="newUserExample"
67+
/>
68+
<Route
69+
path="/user-examples/{id:Int}"
70+
page={UserExampleUserExamplePage}
71+
name="userExample"
72+
/>
73+
<Route
74+
path="/user-examples"
75+
page={UserExampleUserExamplesPage}
76+
name="userExamples"
77+
/>
78+
</Set>
79+
</Set>
80+
<Route notfound page={NotFoundPage} />
81+
</Router>
82+
)
83+
}
84+
85+
export default Routes
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
import type { TagDescriptor } from '@redwoodjs/web/dist/components/htmlTags'
22

33
import { Document } from './Document'
4+
import ServerRoutes from './ServerRoutes'
45

56
interface Props {
67
css: string[]
78
meta?: TagDescriptor[]
9+
location: {
10+
pathname: string
11+
hash?: string
12+
search?: string
13+
}
814
}
915

10-
export const ServerEntry: React.FC<Props> = ({ css, meta }) => {
16+
export const ServerEntry: React.FC<Props> = ({ css, meta, location }) => {
1117
return (
1218
<Document css={css} meta={meta}>
13-
<div>App</div>
19+
<ServerRoutes location={location} />
1420
</Document>
1521
)
1622
}

__fixtures__/test-project-rsc-external-packages-and-cells/web/src/layouts/NavigationLayout/NavigationLayout.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
import { Link, routes } from '@redwoodjs/router'
1+
import { namedRoutes as routes } from '@redwoodjs/router/dist/namedRoutes'
22

33
import './NavigationLayout.css'
44

55
type NavigationLayoutProps = {
66
children?: React.ReactNode
7+
rnd?: number
78
}
89

9-
const NavigationLayout = ({ children }: NavigationLayoutProps) => {
10+
const Link = (props: any) => {
11+
return <a href={props.to}>{props.children}</a>
12+
}
13+
14+
const NavigationLayout = ({ children, rnd }: NavigationLayoutProps) => {
1015
return (
1116
<div className="navigation-layout">
1217
<nav>
@@ -28,6 +33,7 @@ const NavigationLayout = ({ children }: NavigationLayoutProps) => {
2833
</li>
2934
</ul>
3035
</nav>
36+
<div id="rnd">{Math.round(rnd * 100)}</div>
3137
<main>{children}</main>
3238
</div>
3339
)

packages/vite/src/clientSsr.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export function renderFromDist<TProps extends Record<string, any>>(
124124

125125
const id = resolveClientEntryForProd(filePath, clientEntries)
126126

127-
console.log('Proxy id', id)
127+
console.log('clientSsr.ts::Proxy id', id)
128128
// id /Users/tobbe/tmp/test-project-rsc-external-packages-and-cells/web/dist/client/assets/rsc-AboutCounter.tsx-1-4kTKU8GC.mjs
129129
return { id, chunks: [id], name, async: true }
130130
},

packages/vite/src/lib/registerFwGlobalsAndShims.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ function registerFwGlobals() {
100100
* We have to call it early in the app's lifecycle, before code that depends on
101101
* it runs and do so at the server start in (src/devFeServer.ts and
102102
* src/runFeServer.ts).
103+
*
104+
* We generate the input to the shims in the `bundlerConfig` Proxies we have
103105
*/
104106
function registerFwShims() {
105107
if (!getConfig().experimental?.rsc?.enabled) {

packages/vite/src/rsc/rscWorker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ async function renderRsc(input: RenderInput): Promise<PipeableStream> {
369369
id = resolveClientEntryForProd(filePath, config)
370370
}
371371

372-
console.log('Proxy id', id)
372+
console.log('rscWorker proxy id', id)
373373
// id /assets/rsc0-beb48afe.js
374374
return { id, chunks: [id], name, async: true }
375375
},

packages/vite/src/streaming/ssrModuleMap.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { makeFilePath } from '../utils'
2+
13
type SSRModuleMap = null | {
24
[clientId: string]: {
35
[clientExportName: string]: ClientReferenceManifestEntry
@@ -24,6 +26,8 @@ export const moduleMap: SSRModuleMap = new Proxy(
2426
{},
2527
{
2628
get(_target, name: string) {
29+
filePath = makeFilePath(filePath)
30+
2731
const manifestEntry: ClientReferenceManifestEntry = {
2832
id: filePath,
2933
chunks: [filePath],
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { test, expect } from '@playwright/test'
2+
3+
// UA taken from https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers
4+
const BOT_USERAGENT =
5+
'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Chrome/W.X.Y.Z Safari/537.36'
6+
7+
test('Check that homepage has content fully rendered from the server, without JS', async ({
8+
browser,
9+
}) => {
10+
// Rendering as a bot here, to make sure we're getting the full page and not
11+
// just some of it, with the rest streamed in later
12+
const botContext = await browser.newContext({
13+
userAgent: BOT_USERAGENT,
14+
// Even without JS, this should be a fully rendered page
15+
javaScriptEnabled: false,
16+
})
17+
18+
const page = await botContext.newPage()
19+
20+
await page.goto('/')
21+
22+
// Appears when the navigation layout has successfully rendered
23+
await page.waitForSelector('main')
24+
25+
// The NavigationLayout should have a random number in it
26+
const rnd = await page.locator('div#rnd').innerHTML()
27+
expect(rnd).toMatch(/\s*\d+\s*/)
28+
29+
// expect there to only be one h1 heading element on the page
30+
await expect(page.locator('h1')).toHaveCount(1)
31+
await expect(page.locator('h1').first()).toHaveText('Hello Anonymous!!')
32+
33+
// There should be a link to the about page
34+
await expect(page.locator('a').getByText('About')).toBeVisible()
35+
36+
await botContext.close()
37+
})
38+
39+
test('Make sure navigation works even without JS', async ({ browser }) => {
40+
// Rendering as a bot here, to make sure we're getting the full page and not
41+
// just some of it, with the rest streamed in later
42+
const botContext = await browser.newContext({
43+
userAgent: BOT_USERAGENT,
44+
// Even without JS, this should be a fully rendered page
45+
javaScriptEnabled: false,
46+
})
47+
48+
const page = await botContext.newPage()
49+
50+
await page.goto('/')
51+
52+
// There should be a link to the about page
53+
const aboutLink = page.locator('a').getByText('About')
54+
expect(aboutLink).toBeVisible()
55+
56+
// Clicking on the about link should take us to the about page
57+
await aboutLink.click()
58+
59+
expect(page.url()).toMatch(/\/about$/)
60+
61+
// expect there to only be one h1 heading element on the page
62+
await expect(page.locator('h1')).toHaveCount(1)
63+
await expect(page.locator('h1').first()).toHaveText('About Redwood')
64+
65+
await botContext.close()
66+
})
67+
68+
test('The page should have a form button, but it should be non-interactive', async ({
69+
browser,
70+
}) => {
71+
// Rendering as a bot here, to make sure we're getting the full page and not
72+
// just some of it, with the rest streamed in later
73+
const botContext = await browser.newContext({
74+
userAgent: BOT_USERAGENT,
75+
// Even without JS, this should be a fully rendered page
76+
javaScriptEnabled: false,
77+
})
78+
79+
const page = await botContext.newPage()
80+
81+
await page.goto('/about')
82+
83+
const paragraphs = page.locator('p')
84+
85+
// Expect the count to be 0 when the page is first loaded
86+
expect(paragraphs.getByText('Count: 0')).toBeVisible()
87+
88+
await page.getByRole('button', { name: 'Increment' }).click()
89+
90+
// The count should stay at 0, because the page should not be interactive
91+
// (This is the SSRed version of the page, with no JS)
92+
await expect(paragraphs.getByText('Count: 0')).toBeVisible()
93+
94+
await expect(paragraphs.getByText('RSC on client: enabled')).toBeVisible()
95+
await expect(paragraphs.getByText('RSC on server: enabled')).toBeVisible()
96+
97+
await botContext.close()
98+
})

0 commit comments

Comments
 (0)