Files
next.js/test/e2e/legacy-link-behavior/validations.console.test.ts
Arian Tron 61f56f997c
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
first commit
2026-03-10 19:37:31 +03:30

393 lines
16 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { isNextDev, nextTestSetup } from 'e2e-utils'
import { waitForNoRedbox } from 'next-test-utils'
describe('Validations for <Link legacyBehavior>', () => {
const { next, isNextDeploy } = nextTestSetup({
files: __dirname,
skipDeployment: true,
})
if (isNextDeploy) {
return it('should skip deploy', () => {})
}
let previousOutputIndex
beforeEach(() => {
previousOutputIndex = next.cliOutput.length
})
function newConsoleOutput() {
return next.cliOutput.slice(previousOutputIndex)
}
describe('When rendering from a Server Component', () => {
describe('Rendering <Link> directly', () => {
it('warns if the child is a synchronous server component', async () => {
const browser = await next.browser(
'/validations/rsc-that-renders-link/synchronous'
)
if (isNextDev) {
await expect(browser).toDisplayCollapsedRedbox(`
[
{
"code": "E394",
"description": "Using a Server Component as a direct child of \`<Link legacyBehavior>\` is not supported. If you need legacyBehavior, wrap your Server Component in a Client Component that renders the Link's \`<a>\` tag.",
"environmentLabel": "Prerender",
"label": "Console Error",
"source": "app/validations/rsc-that-renders-link/synchronous/page.tsx (7:7) @ Page
> 7 | <Link href="/about" legacyBehavior>
| ^",
"stack": [
"Page app/validations/rsc-that-renders-link/synchronous/page.tsx (7:7)",
],
},
{
"code": "E394",
"description": "\`legacyBehavior\` is deprecated and will be removed in a future release. A codemod is available to upgrade your components:
npx @next/codemod@latest new-link .
Learn more: https://nextjs.org/docs/app/building-your-application/upgrading/codemods#remove-a-tags-from-link-components",
"environmentLabel": null,
"label": "Console Error",
"source": "app/validations/rsc-that-renders-link/synchronous/page.tsx (7:7) @ Page
> 7 | <Link href="/about" legacyBehavior>
| ^",
"stack": [
"Page app/validations/rsc-that-renders-link/synchronous/page.tsx (7:7)",
],
},
]
`)
} else {
expect(newConsoleOutput()).toMatchInlineSnapshot(`
"Using a Server Component as a direct child of \`<Link legacyBehavior>\` is not supported. If you need legacyBehavior, wrap your Server Component in a Client Component that renders the Link's \`<a>\` tag.
"
`)
}
})
it('warns and throws an error if the child is an asynchronous server component', async () => {
const browser = await next.browser(
'/validations/rsc-that-renders-link/asynchronous'
)
if (isNextDev) {
await expect(browser).toDisplayRedbox(`
[
{
"code": "E394",
"description": "Using a Server Component as a direct child of \`<Link legacyBehavior>\` is not supported. If you need legacyBehavior, wrap your Server Component in a Client Component that renders the Link's \`<a>\` tag.",
"environmentLabel": "Prerender",
"label": "Console Error",
"source": "app/validations/rsc-that-renders-link/asynchronous/page.tsx (7:7) @ Page
> 7 | <Link href="/about" legacyBehavior>
| ^",
"stack": [
"Page app/validations/rsc-that-renders-link/asynchronous/page.tsx (7:7)",
],
},
{
"code": "E863",
"description": "\`<Link legacyBehavior>\` received a direct child that is either a Server Component, or JSX that was loaded with React.lazy(). This is not supported. Either remove legacyBehavior, or make the direct child a Client Component that renders the Link's \`<a>\` tag.",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/validations/rsc-that-renders-link/asynchronous/page.tsx (7:7) @ Page
> 7 | <Link href="/about" legacyBehavior>
| ^",
"stack": [
"Page app/validations/rsc-that-renders-link/asynchronous/page.tsx (7:7)",
],
},
]
`)
} else {
const output = getContentBetween({
input: newConsoleOutput(),
endContent: ' at',
})
expect(output).toMatchInlineSnapshot(`
"Using a Server Component as a direct child of \`<Link legacyBehavior>\` is not supported. If you need legacyBehavior, wrap your Server Component in a Client Component that renders the Link's \`<a>\` tag.
Error: \`<Link legacyBehavior>\` received a direct child that is either a Server Component, or JSX that was loaded with React.lazy(). This is not supported. Either remove legacyBehavior, or make the direct child a Client Component that renders the Link's \`<a>\` tag."
`)
}
})
it('does not warn or throw if you pass a client component', async () => {
const browser = await next.browser(
'/validations/rsc-that-renders-link/client'
)
if (isNextDev) {
await waitForNoRedbox(browser)
} else {
expect(newConsoleOutput()).toEqual('')
}
})
it('does not warn or throw if you pass a server component into a client component', async () => {
const browser = await next.browser(
'/validations/rsc-that-renders-link/client-with-rsc-child'
)
if (isNextDev) {
await waitForNoRedbox(browser)
} else {
expect(newConsoleOutput()).toEqual('')
}
})
it('warns if the child is a lazy component', async () => {
const browser = await next.browser(
'/validations/rsc-that-renders-link/lazy'
)
if (isNextDev) {
await expect(browser).toDisplayRedbox(`
[
{
"code": "E394",
"description": "Using a Lazy Component as a direct child of \`<Link legacyBehavior>\` from a Server Component is not supported. If you need legacyBehavior, wrap your Lazy Component in a Client Component that renders the Link's \`<a>\` tag.",
"environmentLabel": "Prerender",
"label": "Console Error",
"source": "app/validations/rsc-that-renders-link/lazy/page.tsx (9:7) @ Page
> 9 | <Link href="/about" legacyBehavior passHref>
| ^",
"stack": [
"Page app/validations/rsc-that-renders-link/lazy/page.tsx (9:7)",
],
},
{
"code": "E863",
"description": "\`<Link legacyBehavior>\` received a direct child that is either a Server Component, or JSX that was loaded with React.lazy(). This is not supported. Either remove legacyBehavior, or make the direct child a Client Component that renders the Link's \`<a>\` tag.",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/validations/rsc-that-renders-link/lazy/page.tsx (9:7) @ Page
> 9 | <Link href="/about" legacyBehavior passHref>
| ^",
"stack": [
"Page app/validations/rsc-that-renders-link/lazy/page.tsx (9:7)",
],
},
]
`)
} else {
const output = getContentBetween({
input: newConsoleOutput(),
endContent: ' at',
})
expect(output).toMatchInlineSnapshot(`
"Using a Lazy Component as a direct child of \`<Link legacyBehavior>\` from a Server Component is not supported. If you need legacyBehavior, wrap your Lazy Component in a Client Component that renders the Link's \`<a>\` tag.
Error: \`<Link legacyBehavior>\` received a direct child that is either a Server Component, or JSX that was loaded with React.lazy(). This is not supported. Either remove legacyBehavior, or make the direct child a Client Component that renders the Link's \`<a>\` tag."
`)
}
})
})
describe('Rendering a Client Component that renders <Link>', () => {
it('does not warn if the child is a synchronous server component', async () => {
const browser = await next.browser(
'/validations/rsc-that-renders-client/synchronous'
)
if (isNextDev) {
await waitForNoRedbox(browser)
} else {
expect(newConsoleOutput()).toEqual('')
}
})
it('throws an error if the child is an asynchronous server component', async () => {
const browser = await next.browser(
'/validations/rsc-that-renders-client/asynchronous'
)
if (isNextDev) {
await expect(browser).toDisplayRedbox(`
{
"code": "E863",
"description": "\`<Link legacyBehavior>\` received a direct child that is either a Server Component, or JSX that was loaded with React.lazy(). This is not supported. Either remove legacyBehavior, or make the direct child a Client Component that renders the Link's \`<a>\` tag.",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/validations/rsc-that-renders-client/client-link.tsx (7:10) @ ClientLink
> 7 | return <Link legacyBehavior passHref {...props} />
| ^",
"stack": [
"ClientLink app/validations/rsc-that-renders-client/client-link.tsx (7:10)",
"Page app/validations/rsc-that-renders-client/asynchronous/page.tsx (7:7)",
],
}
`)
} else {
const output = getContentBetween({
input: newConsoleOutput(),
endContent: ' at',
})
expect(output).toMatchInlineSnapshot(
`" Error: \`<Link legacyBehavior>\` received a direct child that is either a Server Component, or JSX that was loaded with React.lazy(). This is not supported. Either remove legacyBehavior, or make the direct child a Client Component that renders the Link's \`<a>\` tag."`
)
}
})
it('does not warn or throw if you pass a client component', async () => {
const browser = await next.browser(
'/validations/rsc-that-renders-client/client'
)
if (isNextDev) {
await waitForNoRedbox(browser)
} else {
expect(newConsoleOutput()).toEqual('')
}
})
it('does not warn or throw if you pass a server component into a client component', async () => {
const browser = await next.browser(
'/validations/rsc-that-renders-client/client-with-rsc-child'
)
if (isNextDev) {
await waitForNoRedbox(browser)
} else {
expect(newConsoleOutput()).toEqual('')
}
})
})
})
describe('When rendering from a Client Component', () => {
it('errors if there are no children', async () => {
const browser = await next.browser('/validations/client/missing-child')
if (isNextDev) {
await expect(browser).toDisplayRedbox(`
{
"code": "E320",
"description": "No children were passed to <Link> with \`href\` of \`/about\` but one child is required https://nextjs.org/docs/messages/link-no-children",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/validations/client/missing-child/page.tsx (6:10) @ Page
> 6 | return <Link href="/about" legacyBehavior></Link>
| ^",
"stack": [
"Page app/validations/client/missing-child/page.tsx (6:10)",
],
}
`)
} else {
const output = getContentBetween({
input: newConsoleOutput(),
endContent: ' at',
})
expect(output).toMatchInlineSnapshot(
`" Error: React.Children.only expected to receive a single React element child."`
)
}
})
it('errors if there are multiple children', async () => {
const browser = await next.browser(
'/validations/client/multiple-children'
)
if (isNextDev) {
await expect(browser).toDisplayRedbox(`
{
"code": "E266",
"description": "Multiple children were passed to <Link> with \`href\` of \`/about\` but only one child is supported https://nextjs.org/docs/messages/link-multiple-children
Open your browser's console to view the Component stack trace.",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/validations/client/multiple-children/page.tsx (7:5) @ Page
> 7 | <Link href="/about" legacyBehavior>
| ^",
"stack": [
"Page app/validations/client/multiple-children/page.tsx (7:5)",
],
}
`)
} else {
const output = getContentBetween({
input: newConsoleOutput(),
endContent: ' at',
})
expect(output).toMatchInlineSnapshot(
`" Error: React.Children.only expected to receive a single React element child."`
)
}
})
it('does not warn or throw if you pass a child component', async () => {
const browser = await next.browser('/validations/client/child-component')
if (isNextDev) {
await waitForNoRedbox(browser)
} else {
expect(newConsoleOutput()).toEqual('')
}
})
it('warns and throws an error if the child is lazy JSX', async () => {
const browser = await next.browser('/validations/client/lazy-jsx')
if (isNextDev) {
await expect(browser).toDisplayRedbox(`
{
"code": "E863",
"description": "\`<Link legacyBehavior>\` received a direct child that is either a Server Component, or JSX that was loaded with React.lazy(). This is not supported. Either remove legacyBehavior, or make the direct child a Client Component that renders the Link's \`<a>\` tag.",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/validations/client/lazy-jsx/page.tsx (9:7) @ Page
> 9 | <Link href="/about" legacyBehavior passHref>
| ^",
"stack": [
"Page app/validations/client/lazy-jsx/page.tsx (9:7)",
],
}
`)
} else {
const output = getContentBetween({
input: newConsoleOutput(),
endContent: ' at',
})
expect(output).toMatchInlineSnapshot(
`" Error: \`<Link legacyBehavior>\` received a direct child that is either a Server Component, or JSX that was loaded with React.lazy(). This is not supported. Either remove legacyBehavior, or make the direct child a Client Component that renders the Link's \`<a>\` tag."`
)
}
})
})
})
function getContentBetween({
input,
startContent,
endContent,
}: {
input: string
startContent?: string
endContent?: string
}): string {
const lines = input.split('\n')
const startIdx = startContent
? lines.findIndex((line) => line.includes(startContent))
: -1
const endIdx = endContent
? lines.findIndex((line) => line.includes(endContent))
: -1
if (startContent && startIdx < 0) return ''
if (endContent && endIdx >= 0 && endIdx <= startIdx) return ''
const sliceStart = startIdx >= 0 ? startIdx + 1 : 0
const sliceEnd = endIdx > sliceStart ? endIdx : undefined
return lines.slice(sliceStart, sliceEnd).join('\n')
}