mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-07 12:57:47 +00:00
tests: added tests for PPR (#10808)
This adds some tests to the PPR implementation for Next.js. This also fixes a bug where the static pages were incorrectly generating a header that falsly indicated that it postponed.
This commit is contained in:
5
.changeset/green-bananas-guess.md
Normal file
5
.changeset/green-bananas-guess.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@vercel/next': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixed headers for static routes when PPR is enabled
|
||||||
@@ -2429,7 +2429,8 @@ export const onPrerenderRoute =
|
|||||||
initialHeaders: {
|
initialHeaders: {
|
||||||
'content-type': rscContentTypeHeader,
|
'content-type': rscContentTypeHeader,
|
||||||
vary: rscVaryHeader,
|
vary: rscVaryHeader,
|
||||||
...(experimentalPPR && rscDidPostponeHeader
|
// If it contains a pre-render, then it was postponed.
|
||||||
|
...(prerender && rscDidPostponeHeader
|
||||||
? { [rscDidPostponeHeader]: '1' }
|
? { [rscDidPostponeHeader]: '1' }
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
|
|||||||
13
packages/next/test/fixtures/00-app-dir-ppr-full/app/dynamic/force-dynamic/page.jsx
vendored
Normal file
13
packages/next/test/fixtures/00-app-dir-ppr-full/app/dynamic/force-dynamic/page.jsx
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React, { Suspense } from 'react'
|
||||||
|
import { Dynamic } from '../../../components/dynamic'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
export const revalidate = 60
|
||||||
|
|
||||||
|
export default ({ params: { slug } }) => {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<Dynamic pathname={`/dynamic/force-dynamic/${slug}`} fallback />}>
|
||||||
|
<Dynamic pathname={`/dynamic/force-dynamic/${slug}`} />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
13
packages/next/test/fixtures/00-app-dir-ppr-full/app/dynamic/force-static/page.jsx
vendored
Normal file
13
packages/next/test/fixtures/00-app-dir-ppr-full/app/dynamic/force-static/page.jsx
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React, { Suspense } from 'react'
|
||||||
|
import { Dynamic } from '../../../components/dynamic'
|
||||||
|
|
||||||
|
export const dynamic = 'force-static'
|
||||||
|
export const revalidate = 60
|
||||||
|
|
||||||
|
export default ({ params: { slug } }) => {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<Dynamic pathname={`/dynamic/force-static/${slug}`} fallback />}>
|
||||||
|
<Dynamic pathname={`/dynamic/force-static/${slug}`} />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
43
packages/next/test/fixtures/00-app-dir-ppr-full/app/layout.jsx
vendored
Normal file
43
packages/next/test/fixtures/00-app-dir-ppr-full/app/layout.jsx
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{ href: '/', tag: 'pre-generated' },
|
||||||
|
{ href: '/nested/a', tag: 'pre-generated' },
|
||||||
|
{ href: '/nested/b', tag: 'on-demand' },
|
||||||
|
{ href: '/nested/c', tag: 'on-demand' },
|
||||||
|
{ href: '/on-demand/a', tag: 'on-demand, no-gsp' },
|
||||||
|
{ href: '/on-demand/b', tag: 'on-demand, no-gsp' },
|
||||||
|
{ href: '/on-demand/c', tag: 'on-demand, no-gsp' },
|
||||||
|
{ href: '/static', tag: 'static' },
|
||||||
|
{ href: '/no-suspense', tag: 'no suspense' },
|
||||||
|
{ href: '/no-suspense/nested/a', tag: 'no suspense, pre-generated' },
|
||||||
|
{ href: '/no-suspense/nested/b', tag: 'no suspense, on-demand' },
|
||||||
|
{ href: '/no-suspense/nested/c', tag: 'no suspense, on-demand' },
|
||||||
|
{ href: '/dynamic/force-dynamic', tag: "dynamic = 'force-dynamic'" },
|
||||||
|
{ href: '/dynamic/force-static', tag: "dynamic = 'force-static'" },
|
||||||
|
]
|
||||||
|
|
||||||
|
export default ({ children }) => {
|
||||||
|
return (
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Partial Prerendering</h1>
|
||||||
|
<p>
|
||||||
|
Below are links that are associated with different pages that all will
|
||||||
|
partially prerender
|
||||||
|
</p>
|
||||||
|
<aside>
|
||||||
|
<ul>
|
||||||
|
{links.map(({ href, tag }) => (
|
||||||
|
<li key={href}>
|
||||||
|
<Link href={href}>{href}</Link> <span>{tag}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
<main>{children}</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
16
packages/next/test/fixtures/00-app-dir-ppr-full/app/nested/[slug]/page.jsx
vendored
Normal file
16
packages/next/test/fixtures/00-app-dir-ppr-full/app/nested/[slug]/page.jsx
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import React, { Suspense } from 'react'
|
||||||
|
import { Dynamic } from '../../../components/dynamic'
|
||||||
|
|
||||||
|
export const revalidate = 60
|
||||||
|
|
||||||
|
export default ({ params: { slug } }) => {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<Dynamic pathname={`/nested/${slug}`} fallback />}>
|
||||||
|
<Dynamic pathname={`/nested/${slug}`} />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateStaticParams = async () => {
|
||||||
|
return [{ slug: 'a' }]
|
||||||
|
}
|
||||||
10
packages/next/test/fixtures/00-app-dir-ppr-full/app/no-suspense/nested/[slug]/page.jsx
vendored
Normal file
10
packages/next/test/fixtures/00-app-dir-ppr-full/app/no-suspense/nested/[slug]/page.jsx
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Dynamic } from '../../../../components/dynamic'
|
||||||
|
|
||||||
|
export default ({ params: { slug } }) => {
|
||||||
|
return <Dynamic pathname={`/no-suspense/nested/${slug}`} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateStaticParams = async () => {
|
||||||
|
return [{ slug: 'a' }]
|
||||||
|
}
|
||||||
6
packages/next/test/fixtures/00-app-dir-ppr-full/app/no-suspense/page.jsx
vendored
Normal file
6
packages/next/test/fixtures/00-app-dir-ppr-full/app/no-suspense/page.jsx
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Dynamic } from '../../components/dynamic'
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return <Dynamic pathname="/no-suspense" />
|
||||||
|
}
|
||||||
10
packages/next/test/fixtures/00-app-dir-ppr-full/app/on-demand/[slug]/page.jsx
vendored
Normal file
10
packages/next/test/fixtures/00-app-dir-ppr-full/app/on-demand/[slug]/page.jsx
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React, { Suspense } from 'react'
|
||||||
|
import { Dynamic } from '../../../components/dynamic'
|
||||||
|
|
||||||
|
export default ({ params: { slug } }) => {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<Dynamic pathname={`/on-demand/${slug}`} fallback />}>
|
||||||
|
<Dynamic pathname={`/on-demand/${slug}`} />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
10
packages/next/test/fixtures/00-app-dir-ppr-full/app/page.jsx
vendored
Normal file
10
packages/next/test/fixtures/00-app-dir-ppr-full/app/page.jsx
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React, { Suspense } from 'react'
|
||||||
|
import { Dynamic } from '../components/dynamic'
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<Dynamic pathname="/" fallback />}>
|
||||||
|
<Dynamic pathname="/" />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
6
packages/next/test/fixtures/00-app-dir-ppr-full/app/static/page.jsx
vendored
Normal file
6
packages/next/test/fixtures/00-app-dir-ppr-full/app/static/page.jsx
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Dynamic } from '../../components/dynamic'
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return <Dynamic pathname="/static" fallback />
|
||||||
|
}
|
||||||
37
packages/next/test/fixtures/00-app-dir-ppr-full/components/dynamic.jsx
vendored
Normal file
37
packages/next/test/fixtures/00-app-dir-ppr-full/components/dynamic.jsx
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { headers } from 'next/headers'
|
||||||
|
|
||||||
|
export const Dynamic = ({ pathname, fallback }) => {
|
||||||
|
if (fallback) {
|
||||||
|
return <div>Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = []
|
||||||
|
const names = ['x-test-input', 'user-agent']
|
||||||
|
const list = headers()
|
||||||
|
|
||||||
|
for (const name of names) {
|
||||||
|
messages.push({ name, value: list.get(name) })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<dl>
|
||||||
|
{pathname && (
|
||||||
|
<>
|
||||||
|
<dt>Pathname</dt>
|
||||||
|
<dd>{pathname}</dd>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{messages.map(({ name, value }) => (
|
||||||
|
<React.Fragment key={name}>
|
||||||
|
<dt>
|
||||||
|
Header: <code>{name}</code>
|
||||||
|
</dt>
|
||||||
|
<dd>{value ?? 'null'}</dd>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
103
packages/next/test/fixtures/00-app-dir-ppr-full/index.test.js
vendored
Normal file
103
packages/next/test/fixtures/00-app-dir-ppr-full/index.test.js
vendored
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/* eslint-env jest */
|
||||||
|
const path = require('path');
|
||||||
|
const { deployAndTest } = require('../../utils');
|
||||||
|
const fetch = require('../../../../../test/lib/deployment/fetch-retry');
|
||||||
|
|
||||||
|
const pages = [
|
||||||
|
{ pathname: '/', dynamic: true },
|
||||||
|
{ pathname: '/nested/a', dynamic: true },
|
||||||
|
{ pathname: '/nested/b', dynamic: true },
|
||||||
|
{ pathname: '/nested/c', dynamic: true },
|
||||||
|
{ pathname: '/on-demand/a', dynamic: true },
|
||||||
|
{ pathname: '/on-demand/b', dynamic: true },
|
||||||
|
{ pathname: '/on-demand/c', dynamic: true },
|
||||||
|
{ pathname: '/static', dynamic: false },
|
||||||
|
{ pathname: '/no-suspense', dynamic: true },
|
||||||
|
{ pathname: '/no-suspense/nested/a', dynamic: true },
|
||||||
|
{ pathname: '/no-suspense/nested/b', dynamic: true },
|
||||||
|
{ pathname: '/no-suspense/nested/c', dynamic: true },
|
||||||
|
// TODO: uncomment when we've fixed the 404 case for force-dynamic pages
|
||||||
|
// { pathname: '/dynamic/force-dynamic', dynamic: 'force-dynamic' },
|
||||||
|
{ pathname: '/dynamic/force-static', dynamic: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
const ctx = {};
|
||||||
|
|
||||||
|
describe(`${__dirname.split(path.sep).pop()}`, () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
const info = await deployAndTest(__dirname);
|
||||||
|
Object.assign(ctx, info);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dynamic pages should resume', () => {
|
||||||
|
it.each(pages.filter(p => p.dynamic))(
|
||||||
|
'should resume $pathname',
|
||||||
|
async ({ pathname }) => {
|
||||||
|
const expected = `${Date.now()}:${Math.random()}`;
|
||||||
|
const res = await fetch(`${ctx.deploymentUrl}${pathname}`, {
|
||||||
|
headers: { 'X-Test-Input': expected },
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers.get('content-type')).toEqual(
|
||||||
|
'text/html; charset=utf-8'
|
||||||
|
);
|
||||||
|
const html = await res.text();
|
||||||
|
expect(html).toContain(expected);
|
||||||
|
expect(html).toContain('</html>');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('prefetch RSC payloads should return', () => {
|
||||||
|
it.each(pages)(
|
||||||
|
'should prefetch $pathname',
|
||||||
|
async ({ pathname, dynamic }) => {
|
||||||
|
const unexpected = `${Date.now()}:${Math.random()}`;
|
||||||
|
const res = await fetch(`${ctx.deploymentUrl}${pathname}`, {
|
||||||
|
headers: {
|
||||||
|
RSC: '1',
|
||||||
|
'Next-Router-Prefetch': '1',
|
||||||
|
'X-Test-Input': unexpected,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers.get('content-type')).toEqual('text/x-component');
|
||||||
|
console.log(
|
||||||
|
'X-NextJS-Postponed-Reason',
|
||||||
|
res.headers.get('X-NextJS-Postponed-Reason')
|
||||||
|
);
|
||||||
|
if (dynamic) {
|
||||||
|
expect(res.headers.get('X-NextJS-Postponed')).toEqual('1');
|
||||||
|
} else {
|
||||||
|
expect(res.headers.has('X-NextJS-Postponed')).toEqual(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expect that static RSC prefetches do not contain the dynamic text.
|
||||||
|
const text = await res.text();
|
||||||
|
expect(text).not.toContain(unexpected);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dynamic RSC payloads should return', () => {
|
||||||
|
it.each(pages)('should fetch $pathname', async ({ pathname, dynamic }) => {
|
||||||
|
const expected = `${Date.now()}:${Math.random()}`;
|
||||||
|
const res = await fetch(`${ctx.deploymentUrl}${pathname}`, {
|
||||||
|
headers: { RSC: '1', 'X-Test-Input': expected },
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(res.headers.get('content-type')).toEqual('text/x-component');
|
||||||
|
expect(res.headers.has('X-NextJS-Postponed')).toEqual(false);
|
||||||
|
|
||||||
|
const text = await res.text();
|
||||||
|
if (dynamic) {
|
||||||
|
// Expect that dynamic RSC prefetches do contain the dynamic text.
|
||||||
|
expect(text).toContain(expected);
|
||||||
|
} else {
|
||||||
|
// Expect that dynamic RSC prefetches do not contain the dynamic text
|
||||||
|
// when we're forced static.
|
||||||
|
expect(text).not.toContain(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
14
packages/next/test/fixtures/00-app-dir-ppr-full/next.config.js
vendored
Normal file
14
packages/next/test/fixtures/00-app-dir-ppr-full/next.config.js
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
experimental: {
|
||||||
|
ppr: true,
|
||||||
|
},
|
||||||
|
eslint: {
|
||||||
|
// Warning: This allows production builds to successfully complete even if
|
||||||
|
// your project has ESLint errors.
|
||||||
|
ignoreDuringBuilds: true,
|
||||||
|
},
|
||||||
|
productionBrowserSourceMaps: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = nextConfig;
|
||||||
8
packages/next/test/fixtures/00-app-dir-ppr-full/package.json
vendored
Normal file
8
packages/next/test/fixtures/00-app-dir-ppr-full/package.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"next": "canary",
|
||||||
|
"react": "experimental",
|
||||||
|
"react-dom": "experimental"
|
||||||
|
},
|
||||||
|
"ignoreNextjsUpdates": true
|
||||||
|
}
|
||||||
25
packages/next/test/fixtures/00-app-dir-ppr-full/vercel.json
vendored
Normal file
25
packages/next/test/fixtures/00-app-dir-ppr-full/vercel.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"builds": [
|
||||||
|
{
|
||||||
|
"src": "package.json",
|
||||||
|
"use": "@vercel/next",
|
||||||
|
"config": {
|
||||||
|
"functions": {
|
||||||
|
"app/**/*": {
|
||||||
|
"maxDuration": 5,
|
||||||
|
"memory": 512
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"probes": [
|
||||||
|
{
|
||||||
|
"path": "/",
|
||||||
|
"status": 200,
|
||||||
|
"responseHeaders": {
|
||||||
|
"Content-Type": "text/html; charset=utf-8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -283,7 +283,9 @@ async function runProbe(probe, deploymentId, deploymentUrl, ctx) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
hadTest = true;
|
hadTest = true;
|
||||||
} else if (probe.notResponseHeaders) {
|
}
|
||||||
|
|
||||||
|
if (probe.notResponseHeaders) {
|
||||||
Object.keys(probe.notResponseHeaders).forEach(header => {
|
Object.keys(probe.notResponseHeaders).forEach(header => {
|
||||||
const headerValue = resp.headers.get(header);
|
const headerValue = resp.headers.get(header);
|
||||||
const expected = probe.notResponseHeaders[header];
|
const expected = probe.notResponseHeaders[header];
|
||||||
|
|||||||
Reference in New Issue
Block a user