first commit
Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled

This commit is contained in:
Arian Tron
2026-03-10 19:37:31 +03:30
commit 61f56f997c
27684 changed files with 2784175 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
'use client'
export function Foo() {
return <h2>it works</h2>
}

View File

@@ -0,0 +1,5 @@
import { notFound } from 'next/navigation'
export default function Default() {
notFound()
}

View File

@@ -0,0 +1,5 @@
import { Foo } from './client'
export default function Page() {
return <Foo />
}

View File

@@ -0,0 +1,3 @@
export default function Layout({ named }) {
return <div>{named}</div>
}

View File

@@ -0,0 +1,23 @@
'use client'
import React from 'react'
import ReactDOM from 'react-dom'
import ReactDOMServer from 'react-dom/server.edge'
import { renderToStaticMarkup } from 'react-dom/server'
export default function ClientReact() {
const markup = renderToStaticMarkup(
<div className="react-static-markup">{'React Static Markup'}</div>
)
return (
<div>
<p id="client-react">{'React.version=' + React.version}</p>
<p id="client-react-dom">{'ReactDOM.version=' + ReactDOM.version}</p>
<p id="client-react-dom-server">
{'ReactDOMServer.version=' + ReactDOMServer.version}
</p>
<p id="markup">{markup}</p>
</div>
)
}

View File

@@ -0,0 +1,13 @@
import React from 'react'
import ReactDOM from 'react-dom'
import ClientReact from './client-react'
export default function Page() {
return (
<div>
<p id="react">{'React.version=' + React.version}</p>
<p id="react-dom">{'ReactDOM.version=' + ReactDOM.version}</p>
<ClientReact />
</div>
)
}

View File

@@ -0,0 +1,9 @@
'use client'
import { useState } from 'react'
let name = await Promise.resolve('async')
export default (props) => {
return `client ${useState(name)[0]}`
}

View File

@@ -0,0 +1,3 @@
export { default } from '../../lazy/page'
export const runtime = 'edge'

View File

@@ -0,0 +1,3 @@
export { default } from '../../sync/page'
export const runtime = 'edge'

View File

@@ -0,0 +1,7 @@
import { lazy } from 'react'
const Client = lazy(() => import('../client'))
export default function Page() {
return <Client />
}

View File

@@ -0,0 +1,5 @@
import Client from '../client'
export default function Page() {
return <Client />
}

View File

@@ -0,0 +1,10 @@
// CSS modules can only be imported inside client components for now.
import RedText from '../../components/red/index'
export default function CSSM() {
return (
<RedText id="red-text">
<h1>This should be in red</h1>
</RedText>
)
}

View File

@@ -0,0 +1,8 @@
'use client'
import { useState } from 'react'
export default function Dynamic() {
const [data] = useState('dynamic data!')
return <h1>{data}</h1>
}

View File

@@ -0,0 +1,5 @@
export default async function Page() {
const dynamic = '_dynamic'
const { default: Component } = await import(`./${dynamic}.js`)
return <Component />
}

View File

@@ -0,0 +1,5 @@
export default function page() {
return 'dynamic route [id] page'
}
export const runtime = 'edge'

View File

@@ -0,0 +1,5 @@
export default function page() {
return 'dynamic route index page'
}
export const runtime = 'edge'

View File

@@ -0,0 +1,32 @@
import { Suspense } from 'react'
import Nav from '../../components/nav'
let result
let promise
function Data() {
if (result) return result
if (!promise)
promise = new Promise((res) => {
setTimeout(() => {
result =
'</script><script>window.__manipulated_by_injection=true</script><script>'
res()
}, 500)
})
throw promise
}
export default function Page() {
return (
<div>
<div id="content">
<Suspense fallback="next_escaping_fallback">
<Data />
</Suspense>
</div>
<div>
<Nav />
</div>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function GlobalStyle() {
return (
<div>
<h1 id="red">This should be in red</h1>
</div>
)
}

View File

@@ -0,0 +1,12 @@
import React from 'react'
export default function AppLayout({ children }) {
return (
<html>
<head>
<title>RSC</title>
</head>
<body>{children}</body>
</html>
)
}

View File

@@ -0,0 +1,5 @@
import Bar from '../../components/bar'
export default function Multi() {
return <Bar />
}

View File

@@ -0,0 +1,14 @@
import fs from 'fs'
import Foo from '../../components/foo'
export default function Page() {
return (
<>
<h1>fs: {typeof fs.readFile}</h1>
<Foo />
</>
)
}
export const runtime = 'nodejs'

View File

@@ -0,0 +1,9 @@
import NextImage from 'next/legacy/image'
import src from '../../../public/test.jpg'
// Keep arrow function to test rsc loaders
const Page = () => {
return <NextImage id="myimg" src={src} />
}
export default Page

View File

@@ -0,0 +1,9 @@
import NextImage from 'next/image'
import src from '../../../public/test.jpg'
// Keep arrow function to test rsc loaders
const Page = () => {
return <NextImage id="myimg" src={src} />
}
export default Page

View File

@@ -0,0 +1,18 @@
import Link from 'next/link'
import Nav from '../../../components/nav'
export default async function LinkPage({ searchParams }) {
const queryId = (await searchParams).id || '0'
const id = parseInt(queryId)
return (
<>
<h3 id="query">query:{id}</h3>
<div>
<Link href={`/next-api/link?id=${id + 1}`} id="next_id">
next id
</Link>
</div>
<Nav />
</>
)
}

View File

@@ -0,0 +1,3 @@
export default function page() {
return 'dynamic route [id] page'
}

View File

@@ -0,0 +1,3 @@
export default function page() {
return 'dynamic route index page'
}

View File

@@ -0,0 +1,19 @@
import Nav from '../components/nav'
import { headers } from 'next/headers'
const envVar = process.env.ENV_VAR_TEST
const headerKey = 'x-next-test-client'
export default async function Index() {
const headersList = await headers()
const header = headersList.get(headerKey)
return (
<div>
<h1>{`component:index.server`}</h1>
<div id="env">{'env:' + envVar}</div>
<div id="header">{'header:' + header}</div>
<Nav />
</div>
)
}

View File

@@ -0,0 +1,27 @@
import { Suspense } from 'react'
import Counter from '../../components/partial-hydration-counter'
import { createDataFetcher } from '../../lib/data'
const Data = createDataFetcher('next_streaming_data', {
timeout: 1000,
})
export default function () {
return (
<>
{process.env.NEXT_RUNTIME === 'edge'
? `Runtime: Node.js`
: 'Runtime: Edge/Browser'}
<br />
<div className="suspense">
<Suspense fallback="next_streaming_fallback">
<Data />
</Suspense>
</div>
<br />
<Counter />
</>
)
}
export const dynamic = 'force-dynamic'

View File

@@ -0,0 +1,9 @@
'use client'
export function PageComponent() {
return (
<div>
<h1 id="client-title">Client Title</h1>
</div>
)
}

View File

@@ -0,0 +1,3 @@
import { PageComponent } from './client'
export default PageComponent

View File

@@ -0,0 +1 @@
export * from './client'

View File

@@ -0,0 +1,5 @@
'use client'
export function Foo() {
return <div id="foo">Foo</div>
}

View File

@@ -0,0 +1,5 @@
import * as Client from './client-index'
export default function Page() {
return <Client.Foo />
}

View File

@@ -0,0 +1,3 @@
export default function Component() {
return null
}

View File

@@ -0,0 +1,5 @@
import Component from './component'
export default function Page() {
return <Component />
}

View File

@@ -0,0 +1,3 @@
export default function Layout({ children }) {
return <div id="return-null-layout">{children}</div>
}

View File

@@ -0,0 +1,3 @@
export default function Layout() {
return null
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div />
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return null
}

View File

@@ -0,0 +1 @@
export default function Component() {}

View File

@@ -0,0 +1,5 @@
import Component from './component'
export default function Page() {
return <Component />
}

View File

@@ -0,0 +1,3 @@
export default function Layout({ children }) {
return <div id="return-undefined-layout">{children}</div>
}

View File

@@ -0,0 +1 @@
export default function Layout() {}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div />
}

View File

@@ -0,0 +1 @@
export default function Page() {}

View File

@@ -0,0 +1,10 @@
import Nav from '../../components/nav'
export default function page() {
return (
<div>
<h1>{`component:root.server`}</h1>
<Nav />
</div>
)
}

View File

@@ -0,0 +1,5 @@
require('next/router')
export default function Page() {
return <p>just work</p>
}

View File

@@ -0,0 +1,29 @@
import ClientFromDirect from '../../components/client'
import ClientFromShared from '../../components/shared'
import SharedFromClient from '../../components/shared-client'
import Bar from '../../components/bar'
export default function Page() {
// All three client components should be rendered correctly, but only
// shared component is a server component, and another is a client component.
// These two shared components should be created as two module instances.
// It's expected to have hydration mismatch here.
return (
<div id="main" suppressHydrationWarning>
{/* <Random /> */}
<br />
<ClientFromDirect />
<br />
<ClientFromShared />
<br />
<ClientFromShared />
<br />
<SharedFromClient />
<br />
<SharedFromClient />
<br />
<Bar />
</div>
)
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from 'react'
import { createDataFetcher } from '../../lib/data'
import Nav from '../../components/nav'
const Data = createDataFetcher('next_streaming_data', {
timeout: 500,
})
export default function Page() {
return (
<div>
<div id="content">
<Suspense fallback="next_streaming_fallback">
<Data />
</Suspense>
</div>
<div>
<Nav />
</div>
</div>
)
}

View File

@@ -0,0 +1,41 @@
// shared named exports
import { a, b, c, d, e } from '../../components/shared-exports'
// client default, named exports
import DefaultArrow, {
Named as ClientNamed,
} from '../../components/client-exports'
import { Cjs as CjsShared } from '../../components/cjs-server'
import { Cjs as CjsClient } from '../../components/cjs-client'
// client exports all
import { One, Two, TwoAliased } from '../../components/export-all'
export default function Page() {
return (
<div>
<div>
{a}
{b}
{c}
{d}
{e[0]}
</div>
<div>
<DefaultArrow />
</div>
<div>
<ClientNamed />
</div>
<div>
<CjsShared />
</div>
<div>
<CjsClient />
</div>
<div>
Export All: <One />, <Two />, <TwoAliased />
</div>
</div>
)
}

View File

@@ -0,0 +1,5 @@
'use client'
export default function bar() {
return 'bar.client'
}

View File

@@ -0,0 +1,10 @@
import Foo from './foo'
export default function Bar() {
return (
<div id="bar">
{`bar.server.js: `}
<Foo />
</div>
)
}

View File

@@ -0,0 +1,5 @@
'use client'
exports.Cjs = function Cjs() {
return 'cjs-client'
}

View File

@@ -0,0 +1,3 @@
exports.Cjs = function Cjs() {
return 'cjs-shared'
}

View File

@@ -0,0 +1,7 @@
'use client'
export function Named() {
return 'named.client'
}
export default () => 'default-export-arrow.client'

View File

@@ -0,0 +1,9 @@
'use client'
import { useState } from 'react'
export default function Client() {
// To ensure that this component is rendered as a client component, we use a
// state here.
return useState('client_component')[0]
}

View File

@@ -0,0 +1,3 @@
'use client'
export * from './one'

View File

@@ -0,0 +1,6 @@
export function One() {
return 'one'
}
export * from './two'
export { Two as TwoAliased } from './two'

View File

@@ -0,0 +1,3 @@
export function Two() {
return 'two'
}

View File

@@ -0,0 +1,5 @@
'use client'
export default function foo() {
return 'foo.client'
}

View File

@@ -0,0 +1,23 @@
import Link from 'next/link'
export default function Nav() {
return (
<>
<div>
<Link href={'/next-api/link'} id="goto-next-link">
next link
</Link>
</div>
<div>
<Link href={'/streaming-rsc'} id="goto-streaming-rsc">
streaming rsc
</Link>
</div>
<div>
<Link href={'/root'} id="goto-home">
home
</Link>
</div>
</>
)
}

View File

@@ -0,0 +1,22 @@
'use client'
import { useState, useEffect } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
// When this component is hydrated, there might be other parts still pending
// on streaming. So we test the interactivity of the document before it's
// fully loaded.
const counter = document.querySelector('button')
const suspense = document.querySelector('.suspense')
counter.click()
setTimeout(() => {
window.partial_hydration_suspense_result = suspense.textContent
window.partial_hydration_counter_result = counter.textContent
}, 0)
}, [])
return <button onClick={() => setCount(count + 1)}>count: {count}</button>
}

View File

@@ -0,0 +1,7 @@
'use client'
import styles from './style.module.css'
export default function RedText(props) {
return <div {...props} className={styles.text} />
}

View File

@@ -0,0 +1,3 @@
.text {
color: red;
}

View File

@@ -0,0 +1,5 @@
'use client'
import Shared from './shared'
export default Shared

View File

@@ -0,0 +1,10 @@
const a = 'a'
const b = 'b'
const _c = 'c'
const _d = 'd'
const _e = 'e'
const _eArr = [_e]
export const c = _c
export { a, b }
export { _d as d, _eArr as e }

View File

@@ -0,0 +1,21 @@
import React from 'react'
import Client from './client'
const random = ~~(Math.random() * 10000)
export default function Shared() {
let isServerComponent
try {
React.useState()
isServerComponent = false
} catch (e) {
isServerComponent = true
}
return (
<>
<Client />,{' '}
{(isServerComponent ? 'shared:server' : 'shared:client') + ':' + random}
</>
)
}

View File

@@ -0,0 +1,19 @@
export function createDataFetcher(data, { timeout = 0, expire = 10 }) {
let result
let promise
return function Data() {
if (result) return result
if (!promise)
promise = new Promise((resolve) => {
setTimeout(() => {
result = data
setTimeout(() => {
result = undefined
promise = undefined
}, expire)
resolve()
}, timeout)
})
throw promise
}
}

View File

@@ -0,0 +1,17 @@
module.exports = {
reactStrictMode: true,
onDemandEntries: {
maxInactiveAge: 1000 * 60 * 60,
},
serverExternalPackages: ['conditional-exports-optout'],
rewrites: async () => {
return {
afterFiles: [
{
source: '/rewritten-to-edge-dynamic',
destination: '/edge/dynamic',
},
],
}
},
}

View File

@@ -0,0 +1,9 @@
// You can still import React and Next's client component APIs from the server
// they won't be poisoned by the environment.
// eslint-disable-next-line no-unused-vars
import { useState } from 'react'
import 'next/headers'
export default function (_, res) {
res.end('Hello from import-test.js')
}

View File

@@ -0,0 +1,6 @@
// Use `require` to skip the api check
require('next/navigation')
export default function handle(_, res) {
res.send('just work')
}

View File

@@ -0,0 +1,17 @@
import React from 'react'
import ReactDOM from 'react-dom'
import ReactDOMServer from 'react-dom/server'
export default function Page() {
return (
<div>
<p id="react">{'React.version=' + React.version}</p>
<p id="react-dom">{'ReactDOM.version=' + ReactDOM.version}</p>
<p id="react-dom-server">
{'ReactDOMServer.version=' + ReactDOMServer.version}
</p>
</div>
)
}
export const runtime = 'experimental-edge'

View File

@@ -0,0 +1,15 @@
import React from 'react'
import ReactDOM from 'react-dom'
import ReactDOMServer from 'react-dom/server'
export default function Page() {
return (
<div>
<p id="react">{'React.version=' + React.version}</p>
<p id="react-dom">{'ReactDOM.version=' + ReactDOM.version}</p>
<p id="react-dom-server">
{'ReactDOMServer.version=' + ReactDOMServer.version}
</p>
</div>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -0,0 +1,76 @@
import { nextTestSetup } from 'e2e-utils'
describe('react@experimental', () => {
const { next } = nextTestSetup({
files: __dirname,
overrideFiles: {
'next.config.js': `
module.exports = {
experimental: {
taint: true,
}
}
`,
},
})
it('should opt into the react@experimental when enabling $flag', async () => {
const resPages$ = await next.render$('/app-react')
const [
ssrReact,
ssrReactDOM,
ssrClientReact,
ssrClientReactDOM,
ssrClientReactDOMServer,
] = [
resPages$('#react').text(),
resPages$('#react-dom').text(),
resPages$('#client-react').text(),
resPages$('#client-react-dom').text(),
resPages$('#client-react-dom-server').text(),
]
expect({
ssrReact,
ssrReactDOM,
ssrClientReact,
ssrClientReactDOM,
ssrClientReactDOMServer,
}).toEqual({
ssrReact: expect.stringMatching('-experimental-'),
ssrReactDOM: expect.stringMatching('-experimental-'),
ssrClientReact: expect.stringMatching('-experimental-'),
ssrClientReactDOM: expect.stringMatching('-experimental-'),
ssrClientReactDOMServer: expect.stringMatching('-experimental-'),
})
const browser = await next.browser('/app-react')
const [
browserReact,
browserReactDOM,
browserClientReact,
browserClientReactDOM,
browserClientReactDOMServer,
] = await browser.eval(`
[
document.querySelector('#react').innerText,
document.querySelector('#react-dom').innerText,
document.querySelector('#client-react').innerText,
document.querySelector('#client-react-dom').innerText,
document.querySelector('#client-react-dom-server').innerText,
]
`)
expect({
browserReact,
browserReactDOM,
browserClientReact,
browserClientReactDOM,
browserClientReactDOMServer,
}).toEqual({
browserReact: expect.stringMatching('-experimental-'),
browserReactDOM: expect.stringMatching('-experimental-'),
browserClientReact: expect.stringMatching('-experimental-'),
browserClientReactDOM: expect.stringMatching('-experimental-'),
browserClientReactDOMServer: expect.stringMatching('-experimental-'),
})
})
})

View File

@@ -0,0 +1,651 @@
import path from 'path'
import {
check,
getClientReferenceManifest,
getDistDir,
retry,
} from 'next-test-utils'
import { nextTestSetup } from 'e2e-utils'
import cheerio from 'cheerio'
import {
NEXT_RSC_UNION_QUERY,
RSC_HEADER,
} from 'next/dist/client/components/app-router-headers'
// TODO: We should decide on an established pattern for gating test assertions
// on experimental flags. For example, as a first step we could all the common
// gates like this one into a single module.
const isPPREnabledByDefault = process.env.__NEXT_CACHE_COMPONENTS === 'true'
async function resolveStreamResponse(response: any, onData?: any) {
let result = ''
onData = onData || (() => {})
for await (const chunk of response.body) {
result += chunk.toString()
onData(chunk.toString(), result)
}
return result
}
describe('app dir - rsc basics', () => {
const { next, isNextDev, isNextStart, isTurbopack } = nextTestSetup({
files: __dirname,
resolutions: {
'@babel/core': '7.22.18',
'@babel/parser': '7.22.16',
'@babel/types': '7.22.17',
'@babel/traverse': '7.22.18',
},
})
if (isNextDev && !isTurbopack) {
it('should have correct client references keys in manifest', async () => {
await next.render('/')
await retry(() => {
// Check that the client-side manifest is correct before any requests
const clientReferenceManifest = getClientReferenceManifest(
next,
'/page'
)
const clientModulesNames = Object.keys(
clientReferenceManifest.clientModules
)
expect(clientModulesNames).toSatisfyAll((name) => {
const [, key] = name.split('#', 2)
return key === undefined || key === '' || key === 'default'
})
})
})
}
describe('next internal shared context', () => {
it('should not error if just load next/navigation module in pages/api', async () => {
const res = await next.fetch('/api/navigation')
expect(res.status).toBe(200)
expect(await res.text()).toBe('just work')
})
it('should not error if just load next/router module in app page', async () => {
const res = await next.fetch('/shared-context/server')
expect(res.status).toBe(200)
expect(await res.text()).toContain('just work')
})
})
it('should correctly render page returning null', async () => {
const browser = await next.browser('/return-null/page')
expect(
await browser
.elementByCss('#return-null-layout', { state: 'attached' })
.text()
).toBeEmpty()
})
it('should correctly render component returning null', async () => {
const browser = await next.browser('/return-null/component')
expect(
await browser
.elementByCss('#return-null-layout', { state: 'attached' })
.text()
).toBeEmpty()
})
it('should correctly render layout returning null', async () => {
const browser = await next.browser('/return-null/layout')
expect(
await browser
.elementByCss('#return-null-layout', { state: 'attached' })
.text()
).toBeEmpty()
})
it('should correctly render page returning undefined', async () => {
const browser = await next.browser('/return-undefined/page')
expect(
await browser
.elementByCss('#return-undefined-layout', { state: 'attached' })
.text()
).toBeEmpty()
})
it('should correctly render component returning undefined', async () => {
const browser = await next.browser('/return-undefined/component')
expect(
await browser
.elementByCss('#return-undefined-layout', { state: 'attached' })
.text()
).toBeEmpty()
})
it('should correctly render layout returning undefined', async () => {
const browser = await next.browser('/return-undefined/layout')
expect(
await browser
.elementByCss('#return-undefined-layout', { state: 'attached' })
.text()
).toBeEmpty()
})
it('should handle named client components imported as page', async () => {
const $ = await next.render$('/reexport-named')
expect($('#client-title').text()).toBe('Client Title')
})
it('should handle client components imported as namespace', async () => {
const $ = await next.render$('/reexport-namespace')
expect($('#foo').text()).toBe('Foo')
})
it('should render server components correctly', async () => {
const homeHTML = await next.render('/', null, {
headers: {
'x-next-test-client': 'test-util',
},
})
// should have only 1 DOCTYPE
expect(homeHTML).toMatch(/^<!DOCTYPE html><html/)
// should have default metadata when there's nothing additional provided
expect(homeHTML).toContain('<meta charSet="utf-8"/>')
expect(homeHTML).toContain(
'<meta name="viewport" content="width=device-width, initial-scale=1"/>'
)
expect(homeHTML).toContain('header:test-util')
const inlineFlightContents = []
const $ = cheerio.load(homeHTML)
expect($('h1').text()).toBe('component:index.server')
$('script').each((_index, tag) => {
const content = $(tag).text()
if (content) inlineFlightContents.push(content)
})
const internalQueries = [
'__nextFallback',
'__nextLocale',
'__nextDefaultLocale',
'__nextIsNotFound',
]
const hasNextInternalQuery = inlineFlightContents.some((content) =>
internalQueries.some((query) => content.includes(query))
)
expect(hasNextInternalQuery).toBe(false)
expect(next.cliOutput).not.toContain(
'Each child in a list should have a unique "key" prop'
)
})
it('should reuse the inline flight response without sending extra requests', async () => {
const flightRequests: string[] = []
let requestsCount = 0
const browser = await next.browser('/root', {
beforePageLoad(page) {
page.on('request', (request) => {
requestsCount++
const headers = request.headers()
if (
headers['rsc'] === '1' &&
// Prefetches also include `rsc`
headers['next-router-prefetch'] !== '1'
) {
flightRequests.push(request.url())
}
})
},
})
await browser.waitForIdleNetwork()
expect(requestsCount).toBeGreaterThan(0)
expect(flightRequests).toEqual([])
})
it('should support multi-level server component imports', async () => {
const html = await next.render('/multi')
expect(html).toContain('bar.server.js:')
expect(html).toContain('foo.client')
})
it('should create client reference successfully for all file conventions', async () => {
const html = await next.render('/conventions')
expect(html).toContain('it works')
})
it('should be able to navigate between rsc routes', async () => {
const browser = await next.browser('/root')
await browser.waitForElementByCss('#goto-next-link').click()
await new Promise((res) => setTimeout(res, 1000))
await check(() => browser.url(), `${next.url}/next-api/link`)
await browser.waitForElementByCss('#goto-home').click()
await new Promise((res) => setTimeout(res, 1000))
await check(() => browser.url(), `${next.url}/root`)
const content = await browser.elementByCss('body').text()
expect(content).toContain('component:root.server')
await browser.waitForElementByCss('#goto-streaming-rsc').click()
// Wait for navigation and streaming to finish.
await check(
() => browser.elementByCss('#content').text(),
'next_streaming_data'
)
expect(await browser.url()).toBe(`${next.url}/streaming-rsc`)
})
it('should handle streaming server components correctly', async () => {
const browser = await next.browser('/streaming-rsc')
const content = await browser.eval(
`document.querySelector('#content').innerText`
)
expect(content).toMatchInlineSnapshot('"next_streaming_data"')
})
it('should track client components in dynamic imports', async () => {
const html = await next.render('/dynamic')
expect(html).toContain('dynamic data!')
})
describe.each(['node', 'edge'])(
'client references with TLA (%s)',
(runtime) => {
let url = `/async-client${runtime === 'edge' ? '/edge' : ''}`
it('should support TLA in sync client reference imports', async () => {
const html = await next.render(url + '/sync')
expect(html).toContain('client async')
})
it('should support TLA in lazy client reference', async () => {
const html = await next.render(url + '/lazy')
expect(html).toContain('client async')
})
}
)
if (isPPREnabledByDefault) {
// TODO: Figure out why this test is flaky when PPR is enabled
} else {
it('should support next/link in server components', async () => {
const $ = await next.render$('/next-api/link')
const linkText = $('body a[href="/root"]').text()
expect(linkText).toContain('home')
const browser = await next.browser('/next-api/link')
// We need to make sure the app is fully hydrated before clicking, otherwise
// it will be a full redirection instead of being taken over by the next
// router. This timeout prevents it being flaky caused by fast refresh's
// rebuilding event.
await new Promise((res) => setTimeout(res, 1000))
await browser.eval('window.beforeNav = 1')
await browser.waitForElementByCss('#next_id').click()
await check(() => browser.elementByCss('#query').text(), 'query:1')
await browser.waitForElementByCss('#next_id').click()
await check(() => browser.elementByCss('#query').text(), 'query:2')
if (isNextDev) {
expect(await browser.eval('window.beforeNav')).toBe(1)
}
})
}
it('should link correctly with next/link without mpa navigation to the page', async () => {
// Select the button which is not hidden but rendered
const selector = '#goto-next-link'
const browser = await next.browser('/root', {})
await browser.eval('window.didNotReloadPage = true')
await browser.elementByCss(selector).click().waitForElementByCss('#query')
expect(await browser.eval('window.didNotReloadPage')).toBe(true)
const text = await browser.elementByCss('#query').text()
expect(text).toBe('query:0')
})
it('should escape streaming data correctly', async () => {
const browser = await next.browser('/escaping-rsc')
const manipulated = await browser.eval(`window.__manipulated_by_injection`)
expect(manipulated).toBe(undefined)
})
it('should render built-in 404 page for missing route if pagesDir is not presented', async () => {
const res = await next.fetch('/does-not-exist')
expect(res.status).toBe(404)
const html = await res.text()
expect(html).toContain('This page could not be found')
})
it('should suspense next/legacy/image in server components', async () => {
const $ = await next.render$('/next-api/image-legacy')
const imageTag = $('#myimg')
expect(imageTag.attr('src')).toContain('data:image')
})
it('should suspense next/image in server components', async () => {
const $ = await next.render$('/next-api/image-new')
const imageTag = $('#myimg')
expect(imageTag.attr('src')).toMatch(/test.+jpg/)
})
it('should handle various kinds of exports correctly', async () => {
const $ = await next.render$('/various-exports')
const content = $('body').text()
expect(content).toContain('abcde')
expect(content).toContain('default-export-arrow.client')
expect(content).toContain('named.client')
const browser = await next.browser('/various-exports')
const hydratedContent = await browser.waitForElementByCss('body').text()
expect(hydratedContent).toContain('abcde')
expect(hydratedContent).toContain('default-export-arrow.client')
expect(hydratedContent).toContain('named.client')
expect(hydratedContent).toContain('cjs-shared')
expect(hydratedContent).toContain('cjs-client')
expect(hydratedContent).toContain('Export All: one, two, two')
})
it('should support native modules in server component', async () => {
const $ = await next.render$('/native-module')
const content = $('body').text()
expect(content).toContain('fs: function')
expect(content).toContain('foo.client')
})
it('should resolve different kinds of components correctly', async () => {
const $ = await next.render$('/shared')
const main = $('#main').html()
const content = $('#bar').text()
// Should have 5 occurrences of "client_component".
expect(Array.from(main.matchAll(/client_component/g)).length).toBe(5)
// Should have 2 occurrences of "shared:server", and 2 occurrences of
// "shared:client".
const sharedServerModule = Array.from(main.matchAll(/shared:server:(\d+)/g))
const sharedClientModule = Array.from(main.matchAll(/shared:client:(\d+)/g))
expect(sharedServerModule.length).toBe(2)
expect(sharedClientModule.length).toBe(2)
// Should have 2 modules created for the shared component.
expect(sharedServerModule[0][1]).toBe(sharedServerModule[1][1])
expect(sharedClientModule[0][1]).toBe(sharedClientModule[1][1])
expect(sharedServerModule[0][1]).not.toBe(sharedClientModule[0][1])
expect(content).toContain('bar.server.js:')
})
it('should stick to the url without trailing /page suffix', async () => {
const browser = await next.browser('/edge/dynamic')
const indexUrl = await browser.url()
await browser.loadPage(`${next.url}/edge/dynamic/123`, {
disableCache: false,
})
const dynamicRouteUrl = await browser.url()
expect(indexUrl).toBe(`${next.url}/edge/dynamic`)
expect(dynamicRouteUrl).toBe(`${next.url}/edge/dynamic/123`)
})
describe.each(['node', 'edge'])(`%s`, (runtime) => {
it('should handle dynamic routes when URL segment matches the folder bracket syntax', async () => {
const browser = await next.browser(`/${runtime}/dynamic/[id]`)
expect(await browser.elementByCss('body').text()).toBe(
'dynamic route [id] page'
)
})
})
it('should support streaming for flight response', async () => {
await next
.fetch(`/?${NEXT_RSC_UNION_QUERY}`, {
headers: {
[RSC_HEADER]: '1',
},
})
.then(async (response) => {
const result = await resolveStreamResponse(response)
expect(result).toContain('component:index.server')
if (isNextDev) {
expect(result).toContain('"b":"development"')
}
})
})
it('should support partial hydration with inlined server data', async () => {
await next.fetch('/partial-hydration').then(async (response) => {
let gotFallback = false
let gotData = false
let gotInlinedData = false
await resolveStreamResponse(response, (_, result) => {
gotInlinedData = result.includes('self.__next_f=')
gotData = result.includes('next_streaming_data')
if (!gotFallback) {
gotFallback = result.includes('next_streaming_fallback')
if (gotFallback) {
expect(gotData).toBe(false)
// TODO-APP: investigate the failing test
// expect(gotInlinedData).toBe(false)
}
}
})
expect(gotFallback).toBe(true)
expect(gotData).toBe(true)
expect(gotInlinedData).toBe(true)
})
})
it('should not apply rsc syntax checks in pages/api', async () => {
const res = await next.fetch('/api/import-test')
expect(await res.text()).toBe('Hello from import-test.js')
})
// TODO: (PPR) remove once PPR is stable
// TODO(new-dev-overlay): remove once new dev overlay is stable
const bundledReactVersionPattern =
process.env.__NEXT_CACHE_COMPONENTS === 'true'
? '-experimental-'
: '-canary-'
it('should not use bundled react for pages with app', async () => {
const ssrPaths = ['/pages-react', '/edge-pages-react']
const promises = ssrPaths.map(async (pathname) => {
const resPages$ = await next.render$(pathname)
const ssrPagesReactVersions = [
await resPages$('#react').text(),
await resPages$('#react-dom').text(),
await resPages$('#react-dom-server').text(),
]
ssrPagesReactVersions.forEach((version) => {
expect(version).not.toMatch(bundledReactVersionPattern)
})
})
await Promise.all(promises)
const resApp$ = await next.render$('/app-react')
const ssrAppReactVersions = [
await resApp$('#react').text(),
await resApp$('#react-dom').text(),
]
ssrAppReactVersions.forEach((version) =>
expect(version).toMatch(bundledReactVersionPattern)
)
const browser = await next.browser('/pages-react')
const browserPagesReactVersions = await browser.eval(`
[
document.querySelector('#react').innerText,
document.querySelector('#react-dom').innerText,
document.querySelector('#react-dom-server').innerText,
]
`)
await browser.loadPage(next.url + '/edge-pages-react')
const browserEdgePagesReactVersions = await browser.eval(`
[
document.querySelector('#react').innerText,
document.querySelector('#react-dom').innerText,
document.querySelector('#react-dom-server').innerText,
]
`)
browserPagesReactVersions.forEach((version) => {
expect(version).not.toMatch(bundledReactVersionPattern)
})
browserEdgePagesReactVersions.forEach((version) => {
expect(version).not.toMatch(bundledReactVersionPattern)
})
})
it('should use canary react for app', async () => {
const resPages$ = await next.render$('/app-react')
const [
ssrReact,
ssrReactDOM,
ssrClientReact,
ssrClientReactDOM,
ssrClientReactDOMServer,
] = [
resPages$('#react').text(),
resPages$('#react-dom').text(),
resPages$('#client-react').text(),
resPages$('#client-react-dom').text(),
resPages$('#client-react-dom-server').text(),
]
expect({
ssrReact,
ssrReactDOM,
ssrClientReact,
ssrClientReactDOM,
ssrClientReactDOMServer,
}).toEqual({
ssrReact: expect.stringMatching(bundledReactVersionPattern),
ssrReactDOM: expect.stringMatching(bundledReactVersionPattern),
ssrClientReact: expect.stringMatching(bundledReactVersionPattern),
ssrClientReactDOM: expect.stringMatching(bundledReactVersionPattern),
ssrClientReactDOMServer: expect.stringMatching(
bundledReactVersionPattern
),
})
const browser = await next.browser('/app-react')
const [
browserReact,
browserReactDOM,
browserClientReact,
browserClientReactDOM,
browserClientReactDOMServer,
] = await browser.eval(`
[
document.querySelector('#react').innerText,
document.querySelector('#react-dom').innerText,
document.querySelector('#client-react').innerText,
document.querySelector('#client-react-dom').innerText,
document.querySelector('#client-react-dom-server').innerText,
]
`)
expect({
browserReact,
browserReactDOM,
browserClientReact,
browserClientReactDOM,
browserClientReactDOMServer,
}).toEqual({
browserReact: expect.stringMatching(bundledReactVersionPattern),
browserReactDOM: expect.stringMatching(bundledReactVersionPattern),
browserClientReact: expect.stringMatching(bundledReactVersionPattern),
browserClientReactDOM: expect.stringMatching(bundledReactVersionPattern),
browserClientReactDOMServer: expect.stringMatching(
bundledReactVersionPattern
),
})
})
it('should be able to call legacy react-dom/server APIs in client components', async () => {
const $ = await next.render$('/app-react')
const content = $('#markup').text()
expect(content).toBe(
'<div class="react-static-markup">React Static Markup</div>'
)
if (isNextDev) {
const filePath = 'app/app-react/client-react.js'
const fileContent = await next.readFile(filePath)
await next.patchFile(
filePath,
fileContent.replace(
`import { renderToStaticMarkup } from 'react-dom/server'`,
`import { renderToStaticMarkup } from 'react-dom/server.browser'`
)
)
const browser = await next.browser('/app-react')
const markupContentInBrowser = await browser
.elementByCss('#markup')
.text()
expect(markupContentInBrowser).toBe(
'<div class="react-static-markup">React Static Markup</div>'
)
await next.patchFile(filePath, fileContent)
}
})
// disable this flaky test
it.skip('should support partial hydration with inlined server data in browser', async () => {
// Should end up with "next_streaming_data".
const browser = await next.browser('/partial-hydration', {
waitHydration: false,
})
const content = await browser.eval(`window.document.body.innerText`)
expect(content).toContain('next_streaming_data')
// Should support partial hydration: the boundary should still be pending
// while another part is hydrated already.
expect(await browser.eval(`window.partial_hydration_suspense_result`)).toBe(
'next_streaming_fallback'
)
expect(await browser.eval(`window.partial_hydration_counter_result`)).toBe(
'count: 1'
)
})
if (isNextStart) {
it('should generate edge SSR manifests for Node.js', async () => {
const requiredServerFiles = JSON.parse(
await next.readFile(`${getDistDir()}/required-server-files.json`)
).files
const files = ['middleware-build-manifest.js', 'middleware-manifest.json']
let promises = files.map(async (file) => {
expect(
await next.hasFile(path.join(`${getDistDir()}/server`, file))
).toBe(true)
})
await Promise.all(promises)
promises = requiredServerFiles.map(async (file) => {
expect(await next.hasFile(file)).toBe(true)
})
await Promise.all(promises)
})
}
})