Add tests for routing experiment (#36618)
## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `yarn lint`
This commit is contained in:
parent
ddba1aab1f
commit
6bb0e91a0c
29 changed files with 711 additions and 0 deletions
205
packages/next/client/root-index.tsx
Normal file
205
packages/next/client/root-index.tsx
Normal file
|
@ -0,0 +1,205 @@
|
|||
/* global location */
|
||||
import '../build/polyfills/polyfill-module'
|
||||
// @ts-ignore react-dom/client exists when using React 18
|
||||
import ReactDOMClient from 'react-dom/client'
|
||||
// @ts-ignore startTransition exists when using React 18
|
||||
import React, { useState, startTransition } from 'react'
|
||||
import { RefreshContext } from './streaming/refresh'
|
||||
import { createFromFetch } from 'next/dist/compiled/react-server-dom-webpack'
|
||||
|
||||
/// <reference types="react-dom/experimental" />
|
||||
|
||||
export const version = process.env.__NEXT_VERSION
|
||||
|
||||
const appElement: HTMLElement | Document | null = document
|
||||
|
||||
let reactRoot: any = null
|
||||
|
||||
function renderReactElement(
|
||||
domEl: HTMLElement | Document,
|
||||
fn: () => JSX.Element
|
||||
): void {
|
||||
const reactEl = fn()
|
||||
if (!reactRoot) {
|
||||
// Unlike with createRoot, you don't need a separate root.render() call here
|
||||
reactRoot = (ReactDOMClient as any).hydrateRoot(domEl, reactEl)
|
||||
} else {
|
||||
reactRoot.render(reactEl)
|
||||
}
|
||||
}
|
||||
|
||||
const getCacheKey = () => {
|
||||
const { pathname, search } = location
|
||||
return pathname + search
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
let initialServerDataBuffer: string[] | undefined = undefined
|
||||
let initialServerDataWriter: WritableStreamDefaultWriter | undefined = undefined
|
||||
let initialServerDataLoaded = false
|
||||
let initialServerDataFlushed = false
|
||||
|
||||
function nextServerDataCallback(seg: [number, string, string]) {
|
||||
if (seg[0] === 0) {
|
||||
initialServerDataBuffer = []
|
||||
} else {
|
||||
if (!initialServerDataBuffer)
|
||||
throw new Error('Unexpected server data: missing bootstrap script.')
|
||||
|
||||
if (initialServerDataWriter) {
|
||||
initialServerDataWriter.write(encoder.encode(seg[2]))
|
||||
} else {
|
||||
initialServerDataBuffer.push(seg[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// There might be race conditions between `nextServerDataRegisterWriter` and
|
||||
// `DOMContentLoaded`. The former will be called when React starts to hydrate
|
||||
// the root, the latter will be called when the DOM is fully loaded.
|
||||
// For streaming, the former is called first due to partial hydration.
|
||||
// For non-streaming, the latter can be called first.
|
||||
// Hence, we use two variables `initialServerDataLoaded` and
|
||||
// `initialServerDataFlushed` to make sure the writer will be closed and
|
||||
// `initialServerDataBuffer` will be cleared in the right time.
|
||||
function nextServerDataRegisterWriter(writer: WritableStreamDefaultWriter) {
|
||||
if (initialServerDataBuffer) {
|
||||
initialServerDataBuffer.forEach((val) => {
|
||||
writer.write(encoder.encode(val))
|
||||
})
|
||||
if (initialServerDataLoaded && !initialServerDataFlushed) {
|
||||
writer.close()
|
||||
initialServerDataFlushed = true
|
||||
initialServerDataBuffer = undefined
|
||||
}
|
||||
}
|
||||
|
||||
initialServerDataWriter = writer
|
||||
}
|
||||
|
||||
// When `DOMContentLoaded`, we can close all pending writers to finish hydration.
|
||||
const DOMContentLoaded = function () {
|
||||
if (initialServerDataWriter && !initialServerDataFlushed) {
|
||||
initialServerDataWriter.close()
|
||||
initialServerDataFlushed = true
|
||||
initialServerDataBuffer = undefined
|
||||
}
|
||||
initialServerDataLoaded = true
|
||||
}
|
||||
// It's possible that the DOM is already loaded.
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', DOMContentLoaded, false)
|
||||
} else {
|
||||
DOMContentLoaded()
|
||||
}
|
||||
|
||||
const nextServerDataLoadingGlobal = ((self as any).__next_s =
|
||||
(self as any).__next_s || [])
|
||||
nextServerDataLoadingGlobal.forEach(nextServerDataCallback)
|
||||
nextServerDataLoadingGlobal.push = nextServerDataCallback
|
||||
|
||||
function createResponseCache() {
|
||||
return new Map<string, any>()
|
||||
}
|
||||
const rscCache = createResponseCache()
|
||||
|
||||
function fetchFlight(href: string, props?: any) {
|
||||
const url = new URL(href, location.origin)
|
||||
const searchParams = url.searchParams
|
||||
searchParams.append('__flight__', '1')
|
||||
if (props) {
|
||||
searchParams.append('__props__', JSON.stringify(props))
|
||||
}
|
||||
return fetch(url.toString())
|
||||
}
|
||||
|
||||
function useServerResponse(cacheKey: string, serialized?: string) {
|
||||
let response = rscCache.get(cacheKey)
|
||||
if (response) return response
|
||||
|
||||
if (initialServerDataBuffer) {
|
||||
const t = new TransformStream()
|
||||
const writer = t.writable.getWriter()
|
||||
response = createFromFetch(Promise.resolve({ body: t.readable }))
|
||||
nextServerDataRegisterWriter(writer)
|
||||
} else {
|
||||
const fetchPromise = serialized
|
||||
? (() => {
|
||||
const t = new TransformStream()
|
||||
const writer = t.writable.getWriter()
|
||||
writer.ready.then(() => {
|
||||
writer.write(new TextEncoder().encode(serialized))
|
||||
})
|
||||
return Promise.resolve({ body: t.readable })
|
||||
})()
|
||||
: fetchFlight(getCacheKey())
|
||||
response = createFromFetch(fetchPromise)
|
||||
}
|
||||
|
||||
rscCache.set(cacheKey, response)
|
||||
return response
|
||||
}
|
||||
|
||||
const ServerRoot = ({
|
||||
cacheKey,
|
||||
serialized,
|
||||
}: {
|
||||
cacheKey: string
|
||||
serialized?: string
|
||||
}) => {
|
||||
React.useEffect(() => {
|
||||
rscCache.delete(cacheKey)
|
||||
})
|
||||
const response = useServerResponse(cacheKey, serialized)
|
||||
const root = response.readRoot()
|
||||
return root
|
||||
}
|
||||
|
||||
function Root({ children }: React.PropsWithChildren<{}>): React.ReactElement {
|
||||
if (process.env.__NEXT_TEST_MODE) {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
React.useEffect(() => {
|
||||
window.__NEXT_HYDRATED = true
|
||||
|
||||
if (window.__NEXT_HYDRATED_CB) {
|
||||
window.__NEXT_HYDRATED_CB()
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
||||
return children as React.ReactElement
|
||||
}
|
||||
|
||||
const RSCComponent = (props: any) => {
|
||||
const cacheKey = getCacheKey()
|
||||
const { __flight_serialized__ } = props
|
||||
const [, dispatch] = useState({})
|
||||
const rerender = () => dispatch({})
|
||||
// If there is no cache, or there is serialized data already
|
||||
function refreshCache(nextProps: any) {
|
||||
startTransition(() => {
|
||||
const currentCacheKey = getCacheKey()
|
||||
const response = createFromFetch(fetchFlight(currentCacheKey, nextProps))
|
||||
|
||||
rscCache.set(currentCacheKey, response)
|
||||
rerender()
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<RefreshContext.Provider value={refreshCache}>
|
||||
<ServerRoot cacheKey={cacheKey} serialized={__flight_serialized__} />
|
||||
</RefreshContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function hydrate() {
|
||||
renderReactElement(appElement!, () => (
|
||||
<React.StrictMode>
|
||||
<Root>
|
||||
<RSCComponent />
|
||||
</Root>
|
||||
</React.StrictMode>
|
||||
))
|
||||
}
|
8
packages/next/client/root-next.js
Normal file
8
packages/next/client/root-next.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { hydrate, version } from './root-index'
|
||||
|
||||
window.next = {
|
||||
version,
|
||||
root: true,
|
||||
}
|
||||
|
||||
hydrate()
|
|
@ -95,6 +95,7 @@ export interface ExperimentalConfig {
|
|||
scrollRestoration?: boolean
|
||||
externalDir?: boolean
|
||||
conformance?: boolean
|
||||
rootDir?: boolean
|
||||
amp?: {
|
||||
optimizer?: any
|
||||
validator?: string
|
||||
|
@ -490,6 +491,7 @@ export const defaultConfig: NextConfig = {
|
|||
swcFileReading: true,
|
||||
craCompat: false,
|
||||
esmExternals: true,
|
||||
rootDir: false,
|
||||
// default to 50MB limit
|
||||
isrMemoryCacheSize: 50 * 1024 * 1024,
|
||||
serverComponents: false,
|
||||
|
|
8
test/e2e/root-dir/app/next.config.js
Normal file
8
test/e2e/root-dir/app/next.config.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
module.exports = {
|
||||
experimental: {
|
||||
rootDir: true,
|
||||
runtime: 'nodejs',
|
||||
reactRoot: true,
|
||||
serverComponents: true,
|
||||
},
|
||||
}
|
7
test/e2e/root-dir/app/pages/blog/[slug].js
Normal file
7
test/e2e/root-dir/app/pages/blog/[slug].js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default function Page(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from pages/blog/[slug]</p>
|
||||
</>
|
||||
)
|
||||
}
|
7
test/e2e/root-dir/app/pages/index.js
Normal file
7
test/e2e/root-dir/app/pages/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default function Page(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from pages/index</p>
|
||||
</>
|
||||
)
|
||||
}
|
1
test/e2e/root-dir/app/public/hello.txt
Normal file
1
test/e2e/root-dir/app/public/hello.txt
Normal file
|
@ -0,0 +1 @@
|
|||
hello world
|
11
test/e2e/root-dir/app/root.server.js
Normal file
11
test/e2e/root-dir/app/root.server.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
export default function Root({ headChildren, bodyChildren }) {
|
||||
return (
|
||||
<html className="this-is-the-document-html">
|
||||
<head>
|
||||
{headChildren}
|
||||
<title>Test</title>
|
||||
</head>
|
||||
<body className="this-is-the-document-body">{bodyChildren}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
12
test/e2e/root-dir/app/root/client-component-route.client.js
Normal file
12
test/e2e/root-dir/app/root/client-component-route.client.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
export default function ClientComponentRoute() {
|
||||
const [count, setCount] = useState(0)
|
||||
useEffect(() => {
|
||||
setCount(1)
|
||||
}, [count])
|
||||
return (
|
||||
<>
|
||||
<p>hello from root/client-component-route. count: {count}</p>
|
||||
</>
|
||||
)
|
||||
}
|
15
test/e2e/root-dir/app/root/client-nested.client.js
Normal file
15
test/e2e/root-dir/app/root/client-nested.client.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
|
||||
export default function ClientNestedLayout({ children }) {
|
||||
const [count, setCount] = useState(0)
|
||||
useEffect(() => {
|
||||
setCount(1)
|
||||
}, [])
|
||||
return (
|
||||
<>
|
||||
<h1>Client Nested. Count: {count}</h1>
|
||||
<button onClick={() => setCount(count + 1)}>{count}</button>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
7
test/e2e/root-dir/app/root/client-nested/index.server.js
Normal file
7
test/e2e/root-dir/app/root/client-nested/index.server.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default function ClientPage() {
|
||||
return (
|
||||
<>
|
||||
<p>hello from root/client-nested</p>
|
||||
</>
|
||||
)
|
||||
}
|
27
test/e2e/root-dir/app/root/conditional/[slug].server.js
Normal file
27
test/e2e/root-dir/app/root/conditional/[slug].server.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
export async function getServerSideProps({ params }) {
|
||||
if (params.slug === 'nonexistent') {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
isUser: params.slug === 'tim',
|
||||
isBoth: params.slug === 'both',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function UserOrTeam({ isUser, isBoth, user, team }) {
|
||||
return (
|
||||
<>
|
||||
{isUser && !isBoth ? user : team}
|
||||
{isBoth ? (
|
||||
<>
|
||||
{user}
|
||||
{team}
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export default function TeamHomePage(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from team homepage</p>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export default function TeamMembersPage(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from team/members</p>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export default function UserHomePage(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from user homepage</p>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export default function UserTeamsPage(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from user/teams</p>
|
||||
</>
|
||||
)
|
||||
}
|
7
test/e2e/root-dir/app/root/dashboard+changelog.server.js
Normal file
7
test/e2e/root-dir/app/root/dashboard+changelog.server.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default function ChangelogPage(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from root/dashboard/changelog</p>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export default function HelloPage(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from root/dashboard/rootonly/hello</p>
|
||||
</>
|
||||
)
|
||||
}
|
8
test/e2e/root-dir/app/root/dashboard.server.js
Normal file
8
test/e2e/root-dir/app/root/dashboard.server.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
export default function DashboardLayout({ children }) {
|
||||
return (
|
||||
<>
|
||||
<h1>Dashboard</h1>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
16
test/e2e/root-dir/app/root/dashboard/deployments.server.js
Normal file
16
test/e2e/root-dir/app/root/dashboard/deployments.server.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
export function getServerSideProps() {
|
||||
return {
|
||||
props: {
|
||||
message: 'hello',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function DeploymentsLayout({ message, children }) {
|
||||
return (
|
||||
<>
|
||||
<h2>Deployments {message}</h2>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
export async function getServerSideProps({ params }) {
|
||||
return {
|
||||
props: {
|
||||
id: params.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function DeploymentsPage(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from root/dashboard/deployments/[id]. ID is: {props.id}</p>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export default function DeploymentsInfoPage(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from root/dashboard/deployments/info</p>
|
||||
</>
|
||||
)
|
||||
}
|
7
test/e2e/root-dir/app/root/dashboard/index.server.js
Normal file
7
test/e2e/root-dir/app/root/dashboard/index.server.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default function DashboardPage(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from root/dashboard</p>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export default function IntegrationsPage(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from root/dashboard/integrations</p>
|
||||
</>
|
||||
)
|
||||
}
|
15
test/e2e/root-dir/app/root/partial-match-[id].server.js
Normal file
15
test/e2e/root-dir/app/root/partial-match-[id].server.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
export async function getServerSideProps({ params }) {
|
||||
return {
|
||||
props: {
|
||||
id: params.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function DeploymentsPage(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from root/partial-match-[id]. ID is: {props.id}</p>
|
||||
</>
|
||||
)
|
||||
}
|
7
test/e2e/root-dir/app/root/shared-component-route.js
Normal file
7
test/e2e/root-dir/app/root/shared-component-route.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default function SharedComponentRoute() {
|
||||
return (
|
||||
<>
|
||||
<p>hello from root/shared-component-route</p>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export default function ShouldNotServeClientDotJs(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from root/should-not-serve-client</p>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export default function ShouldNotServeServerDotJs(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from root/should-not-serve-server</p>
|
||||
</>
|
||||
)
|
||||
}
|
263
test/e2e/root-dir/index.test.ts
Normal file
263
test/e2e/root-dir/index.test.ts
Normal file
|
@ -0,0 +1,263 @@
|
|||
import { createNext, FileRef } from 'e2e-utils'
|
||||
import { NextInstance } from 'test/lib/next-modes/base'
|
||||
import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils'
|
||||
import path from 'path'
|
||||
import cheerio from 'cheerio'
|
||||
import webdriver from 'next-webdriver'
|
||||
|
||||
// TODO: implementation
|
||||
describe.skip('root dir', () => {
|
||||
let next: NextInstance
|
||||
|
||||
beforeAll(async () => {
|
||||
next = await createNext({
|
||||
files: {
|
||||
public: new FileRef(path.join(__dirname, 'app/public')),
|
||||
pages: new FileRef(path.join(__dirname, 'app/pages')),
|
||||
root: new FileRef(path.join(__dirname, 'app/root')),
|
||||
'root.server.js': new FileRef(
|
||||
path.join(__dirname, 'app/root.server.js')
|
||||
),
|
||||
'next.config.js': new FileRef(
|
||||
path.join(__dirname, 'app/next.config.js')
|
||||
),
|
||||
},
|
||||
dependencies: {
|
||||
react: '18.0.0-rc.2',
|
||||
'react-dom': '18.0.0-rc.2',
|
||||
},
|
||||
})
|
||||
})
|
||||
afterAll(() => next.destroy())
|
||||
|
||||
it('should serve from pages', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/')
|
||||
expect(html).toContain('hello from pages/index')
|
||||
})
|
||||
|
||||
it('should serve dynamic route from pages', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/blog/first')
|
||||
expect(html).toContain('hello from pages/blog/[slug]')
|
||||
})
|
||||
|
||||
it('should serve from public', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/hello.txt')
|
||||
expect(html).toContain('hello world')
|
||||
})
|
||||
|
||||
it('should serve from root', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/dashboard')
|
||||
expect(html).toContain('hello from root/dashboard')
|
||||
})
|
||||
|
||||
it('should include layouts when no direct parent layout', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/dashboard/integrations')
|
||||
const $ = cheerio.load(html)
|
||||
// Should not be nested in dashboard
|
||||
expect($('h1').text()).toBe('Dashboard')
|
||||
// Should include the page text
|
||||
expect($('p').text()).toBe('hello from root/dashboard/integrations')
|
||||
})
|
||||
|
||||
// TODO: why is this routable but /should-not-serve-server.server.js
|
||||
it('should not include parent when not in parent directory with route in directory', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/dashboard/rootonly/hello')
|
||||
const $ = cheerio.load(html)
|
||||
|
||||
// Should be nested in /root.js
|
||||
expect($('html').hasClass('this-is-the-document-html')).toBeTruthy()
|
||||
expect($('body').hasClass('this-is-the-document-body')).toBeTruthy()
|
||||
|
||||
// Should not be nested in dashboard
|
||||
expect($('h1').text()).toBeFalsy()
|
||||
|
||||
// Should render the page text
|
||||
expect($('p').text()).toBe('hello from root/dashboard/rootonly/hello')
|
||||
})
|
||||
|
||||
it('should include parent document when no direct parent layout', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/dashboard/integrations')
|
||||
const $ = cheerio.load(html)
|
||||
|
||||
// Root has to provide it's own document
|
||||
expect($('html').hasClass('this-is-the-document-html')).toBeTruthy()
|
||||
expect($('body').hasClass('this-is-the-document-body')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not include parent when not in parent directory', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/dashboard/changelog')
|
||||
const $ = cheerio.load(html)
|
||||
// Should not be nested in dashboard
|
||||
expect($('h1').text()).toBeFalsy()
|
||||
// Should include the page text
|
||||
expect($('p').text()).toBe('hello from root/dashboard/changelog')
|
||||
})
|
||||
|
||||
it('should serve nested parent', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/dashboard/deployments/123')
|
||||
const $ = cheerio.load(html)
|
||||
// Should be nested in dashboard
|
||||
expect($('h1').text()).toBe('Dashboard')
|
||||
// Should be nested in deployments
|
||||
expect($('h2').text()).toBe('Deployments hello')
|
||||
})
|
||||
|
||||
it('should serve dynamic parameter', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/dashboard/deployments/123')
|
||||
const $ = cheerio.load(html)
|
||||
// Should include the page text with the parameter
|
||||
expect($('p').text()).toBe(
|
||||
'hello from root/dashboard/deployments/[id]. ID is: 123'
|
||||
)
|
||||
})
|
||||
|
||||
it('should include document html and body', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/dashboard')
|
||||
const $ = cheerio.load(html)
|
||||
|
||||
expect($('html').hasClass('this-is-the-document-html')).toBeTruthy()
|
||||
expect($('body').hasClass('this-is-the-document-body')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not serve when layout is provided but no folder index', async () => {
|
||||
const res = await fetchViaHTTP(next.url, '/dashboard/deployments')
|
||||
expect(res.status).toBe(404)
|
||||
expect(await res.text()).toContain('This page could not be found')
|
||||
})
|
||||
|
||||
// TODO: do we want to make this only work for /root or is it allowed
|
||||
// to work for /pages as well?
|
||||
it('should match partial parameters', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/partial-match-123')
|
||||
expect(html).toContain('hello from root/partial-match-[id]. ID is: 123')
|
||||
})
|
||||
|
||||
describe('parallel routes', () => {
|
||||
describe('conditional routes', () => {
|
||||
it('should serve user page', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/conditional/tim')
|
||||
expect(html).toContain('hello from user homepage')
|
||||
})
|
||||
|
||||
it('should serve user teams page', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/conditional/tim/teams')
|
||||
expect(html).toContain('hello from user/teams')
|
||||
})
|
||||
|
||||
it('should not serve teams page to user', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/conditional/tim/members')
|
||||
expect(html).not.toContain('hello from team/members')
|
||||
})
|
||||
|
||||
it('should serve team page', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/conditional/vercel')
|
||||
expect(html).toContain('hello from team homepage')
|
||||
})
|
||||
|
||||
it('should serve team members page', async () => {
|
||||
const html = await renderViaHTTP(
|
||||
next.url,
|
||||
'/conditional/vercel/members'
|
||||
)
|
||||
expect(html).toContain('hello from team/members')
|
||||
})
|
||||
|
||||
it('should provide both matches if both paths match', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/conditional/both')
|
||||
expect(html).toContain('hello from team homepage')
|
||||
expect(html).toContain('hello from user homepage')
|
||||
})
|
||||
|
||||
it('should 404 based on getServerSideProps', async () => {
|
||||
const res = await fetchViaHTTP(next.url, '/conditional/nonexistent')
|
||||
expect(res.status).toBe(404)
|
||||
expect(await res.text()).toContain('This page could not be found')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('server components', () => {
|
||||
// TODO: why is this not servable but /dashboard+rootonly/hello.server.js
|
||||
// should be? Seems like they both either should be servable or not
|
||||
it('should not serve .server.js as a path', async () => {
|
||||
// Without .server.js should serve
|
||||
const html = await renderViaHTTP(next.url, '/should-not-serve-server')
|
||||
expect(html).toContain('hello from root/should-not-serve-server')
|
||||
|
||||
// Should not serve `.server`
|
||||
const res = await fetchViaHTTP(
|
||||
next.url,
|
||||
'/should-not-serve-server.server'
|
||||
)
|
||||
expect(res.status).toBe(404)
|
||||
expect(await res.text()).toContain('This page could not be found')
|
||||
|
||||
// Should not serve `.server.js`
|
||||
const res2 = await fetchViaHTTP(
|
||||
next.url,
|
||||
'/should-not-serve-server.server.js'
|
||||
)
|
||||
expect(res2.status).toBe(404)
|
||||
expect(await res2.text()).toContain('This page could not be found')
|
||||
})
|
||||
|
||||
it('should not serve .client.js as a path', async () => {
|
||||
// Without .client.js should serve
|
||||
const html = await renderViaHTTP(next.url, '/should-not-serve-client')
|
||||
expect(html).toContain('hello from root/should-not-serve-client')
|
||||
|
||||
// Should not serve `.client`
|
||||
const res = await fetchViaHTTP(
|
||||
next.url,
|
||||
'/should-not-serve-client.client'
|
||||
)
|
||||
expect(res.status).toBe(404)
|
||||
expect(await res.text()).toContain('This page could not be found')
|
||||
|
||||
// Should not serve `.client.js`
|
||||
const res2 = await fetchViaHTTP(
|
||||
next.url,
|
||||
'/should-not-serve-client.client.js'
|
||||
)
|
||||
expect(res2.status).toBe(404)
|
||||
expect(await res2.text()).toContain('This page could not be found')
|
||||
})
|
||||
|
||||
it('should serve shared component', async () => {
|
||||
// Without .client.js should serve
|
||||
const html = await renderViaHTTP(next.url, '/shared-component-route')
|
||||
expect(html).toContain('hello from root/shared-component-route')
|
||||
})
|
||||
|
||||
it('should serve client component', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/client-component-route')
|
||||
expect(html).toContain('hello from root/client-component-route. count: 0')
|
||||
|
||||
const browser = await webdriver(next.url, '/client-component-route')
|
||||
// After hydration count should be 1
|
||||
expect(await browser.elementByCss('p').text()).toBe(
|
||||
'hello from root/client-component-route. count: 1'
|
||||
)
|
||||
})
|
||||
|
||||
it('should include client component layout with server component route', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/client-nested')
|
||||
const $ = cheerio.load(html)
|
||||
// Should not be nested in dashboard
|
||||
expect($('h1').text()).toBe('Client Nested. Count: 0')
|
||||
// Should include the page text
|
||||
expect($('p').text()).toBe('hello from root/client-nested')
|
||||
|
||||
const browser = await webdriver(next.url, '/client-nested')
|
||||
// After hydration count should be 1
|
||||
expect(await browser.elementByCss('h1').text()).toBe(
|
||||
'Client Nested. Count: 0'
|
||||
)
|
||||
|
||||
// After hydration count should be 1
|
||||
expect(await browser.elementByCss('h1').text()).toBe(
|
||||
'hello from root/client-nested'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue