Skip to content

Commit

Permalink
RSC: Add RSC+SSR smoke test to CI (#10477)
Browse files Browse the repository at this point in the history
Co-authored-by: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com>
  • Loading branch information
Tobbe and Josh-Walker-GM committed Apr 26, 2024
1 parent d8ee4cd commit ebb3475
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// In this file, all Page components from 'src/pages` are auto-imported. Nested
// directories are supported, and should be uppercase. Each subdirectory will be
// prepended onto the component name.
//
// Examples:
//
// 'src/pages/HomePage/HomePage.js' -> HomePage
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage

import { Route } from '@redwoodjs/router/dist/Route'
import { Router } from '@redwoodjs/router/dist/server-router'
import { Set } from '@redwoodjs/router/dist/Set'

import NavigationLayout from './layouts/NavigationLayout/NavigationLayout'
import ScaffoldLayout from './layouts/ScaffoldLayout/ScaffoldLayout'
import AboutPage from './pages/AboutPage/AboutPage'
import EmptyUserEmptyUsersPage from './pages/EmptyUser/EmptyUsersPage/EmptyUsersPage'
import EmptyUserNewEmptyUserPage from './pages/EmptyUser/NewEmptyUserPage/NewEmptyUserPage'
import HomePage from './pages/HomePage/HomePage'
import MultiCellPage from './pages/MultiCellPage/MultiCellPage'
import UserExampleNewUserExamplePage from './pages/UserExample/NewUserExamplePage/NewUserExamplePage'
import UserExampleUserExamplePage from './pages/UserExample/UserExamplePage/UserExamplePage'
import UserExampleUserExamplesPage from './pages/UserExample/UserExamplesPage/UserExamplesPage'

const NotFoundPage = () => {
return <div>Not Found</div>
}

const Routes = ({ location }) => {
return (
<Router location={location}>
<Set wrap={NavigationLayout} rnd={Math.random()}>
<Route path="/" page={HomePage} name="home" />
<Route path="/about" page={AboutPage} name="about" />
<Route path="/multi-cell" page={MultiCellPage} name="multiCell" />

<Set
wrap={ScaffoldLayout}
title="EmptyUsers"
titleTo="emptyUsers"
buttonLabel="New EmptyUser"
buttonTo="newEmptyUser"
>
<Route
path="/empty-users/new"
page={EmptyUserNewEmptyUserPage}
name="newEmptyUser"
/>
<Route
path="/empty-users"
page={EmptyUserEmptyUsersPage}
name="emptyUsers"
/>
</Set>

<Set
wrap={ScaffoldLayout}
title="UserExamples"
titleTo="userExamples"
buttonLabel="New UserExample"
buttonTo="newUserExample"
>
<Route
path="/user-examples/new"
page={UserExampleNewUserExamplePage}
name="newUserExample"
/>
<Route
path="/user-examples/{id:Int}"
page={UserExampleUserExamplePage}
name="userExample"
/>
<Route
path="/user-examples"
page={UserExampleUserExamplesPage}
name="userExamples"
/>
</Set>
</Set>
<Route notfound page={NotFoundPage} />
</Router>
)
}

export default Routes
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import type { TagDescriptor } from '@redwoodjs/web/dist/components/htmlTags'

import { Document } from './Document'
import ServerRoutes from './ServerRoutes'

interface Props {
css: string[]
meta?: TagDescriptor[]
location: {
pathname: string
hash?: string
search?: string
}
}

export const ServerEntry: React.FC<Props> = ({ css, meta }) => {
export const ServerEntry: React.FC<Props> = ({ css, meta, location }) => {
return (
<Document css={css} meta={meta}>
<div>App</div>
<ServerRoutes location={location} />
</Document>
)
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { Link, routes } from '@redwoodjs/router'
import { namedRoutes as routes } from '@redwoodjs/router/dist/namedRoutes'

import './NavigationLayout.css'

type NavigationLayoutProps = {
children?: React.ReactNode
rnd?: number
}

const NavigationLayout = ({ children }: NavigationLayoutProps) => {
const Link = (props: any) => {
return <a href={props.to}>{props.children}</a>
}

const NavigationLayout = ({ children, rnd }: NavigationLayoutProps) => {
return (
<div className="navigation-layout">
<nav>
Expand All @@ -28,6 +33,7 @@ const NavigationLayout = ({ children }: NavigationLayoutProps) => {
</li>
</ul>
</nav>
<div id="rnd">{Math.round(rnd * 100)}</div>
<main>{children}</main>
</div>
)
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/clientSsr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export function renderFromDist<TProps extends Record<string, any>>(

const id = resolveClientEntryForProd(filePath, clientEntries)

console.log('Proxy id', id)
console.log('clientSsr.ts::Proxy id', id)
// id /Users/tobbe/tmp/test-project-rsc-external-packages-and-cells/web/dist/client/assets/rsc-AboutCounter.tsx-1-4kTKU8GC.mjs
return { id, chunks: [id], name, async: true }
},
Expand Down
2 changes: 2 additions & 0 deletions packages/vite/src/lib/registerFwGlobalsAndShims.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ function registerFwGlobals() {
* We have to call it early in the app's lifecycle, before code that depends on
* it runs and do so at the server start in (src/devFeServer.ts and
* src/runFeServer.ts).
*
* We generate the input to the shims in the `bundlerConfig` Proxies we have
*/
function registerFwShims() {
if (!getConfig().experimental?.rsc?.enabled) {
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/rsc/rscWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ async function renderRsc(input: RenderInput): Promise<PipeableStream> {
id = resolveClientEntryForProd(filePath, config)
}

console.log('Proxy id', id)
console.log('rscWorker proxy id', id)
// id /assets/rsc0-beb48afe.js
return { id, chunks: [id], name, async: true }
},
Expand Down
4 changes: 4 additions & 0 deletions packages/vite/src/streaming/ssrModuleMap.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { makeFilePath } from '../utils'

type SSRModuleMap = null | {
[clientId: string]: {
[clientExportName: string]: ClientReferenceManifestEntry
Expand All @@ -24,6 +26,8 @@ export const moduleMap: SSRModuleMap = new Proxy(
{},
{
get(_target, name: string) {
filePath = makeFilePath(filePath)

const manifestEntry: ClientReferenceManifestEntry = {
id: filePath,
chunks: [filePath],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { test, expect } from '@playwright/test'

// UA taken from https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers
const BOT_USERAGENT =
'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'

test('Check that homepage has content fully rendered from the server, without JS', async ({
browser,
}) => {
// Rendering as a bot here, to make sure we're getting the full page and not
// just some of it, with the rest streamed in later
const botContext = await browser.newContext({
userAgent: BOT_USERAGENT,
// Even without JS, this should be a fully rendered page
javaScriptEnabled: false,
})

const page = await botContext.newPage()

await page.goto('/')

// Appears when the navigation layout has successfully rendered
await page.waitForSelector('main')

// The NavigationLayout should have a random number in it
const rnd = await page.locator('div#rnd').innerHTML()
expect(rnd).toMatch(/\s*\d+\s*/)

// expect there to only be one h1 heading element on the page
await expect(page.locator('h1')).toHaveCount(1)
await expect(page.locator('h1').first()).toHaveText('Hello Anonymous!!')

// There should be a link to the about page
await expect(page.locator('a').getByText('About')).toBeVisible()

await botContext.close()
})

test('Make sure navigation works even without JS', async ({ browser }) => {
// Rendering as a bot here, to make sure we're getting the full page and not
// just some of it, with the rest streamed in later
const botContext = await browser.newContext({
userAgent: BOT_USERAGENT,
// Even without JS, this should be a fully rendered page
javaScriptEnabled: false,
})

const page = await botContext.newPage()

await page.goto('/')

// There should be a link to the about page
const aboutLink = page.locator('a').getByText('About')
expect(aboutLink).toBeVisible()

// Clicking on the about link should take us to the about page
await aboutLink.click()

expect(page.url()).toMatch(/\/about$/)

// expect there to only be one h1 heading element on the page
await expect(page.locator('h1')).toHaveCount(1)
await expect(page.locator('h1').first()).toHaveText('About Redwood')

await botContext.close()
})

test('The page should have a form button, but it should be non-interactive', async ({
browser,
}) => {
// Rendering as a bot here, to make sure we're getting the full page and not
// just some of it, with the rest streamed in later
const botContext = await browser.newContext({
userAgent: BOT_USERAGENT,
// Even without JS, this should be a fully rendered page
javaScriptEnabled: false,
})

const page = await botContext.newPage()

await page.goto('/about')

const paragraphs = page.locator('p')

// Expect the count to be 0 when the page is first loaded
expect(paragraphs.getByText('Count: 0')).toBeVisible()

await page.getByRole('button', { name: 'Increment' }).click()

// The count should stay at 0, because the page should not be interactive
// (This is the SSRed version of the page, with no JS)
await expect(paragraphs.getByText('Count: 0')).toBeVisible()

await expect(paragraphs.getByText('RSC on client: enabled')).toBeVisible()
await expect(paragraphs.getByText('RSC on server: enabled')).toBeVisible()

await botContext.close()
})

0 comments on commit ebb3475

Please sign in to comment.