mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-06 12:57:46 +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: {
|
||||
'content-type': rscContentTypeHeader,
|
||||
vary: rscVaryHeader,
|
||||
...(experimentalPPR && rscDidPostponeHeader
|
||||
// If it contains a pre-render, then it was postponed.
|
||||
...(prerender && rscDidPostponeHeader
|
||||
? { [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;
|
||||
} else if (probe.notResponseHeaders) {
|
||||
}
|
||||
|
||||
if (probe.notResponseHeaders) {
|
||||
Object.keys(probe.notResponseHeaders).forEach(header => {
|
||||
const headerValue = resp.headers.get(header);
|
||||
const expected = probe.notResponseHeaders[header];
|
||||
|
||||
Reference in New Issue
Block a user