Skip to content

Commit e19ddad

Browse files
committed
test: revalidations within after()
1 parent f23d383 commit e19ddad

File tree

18 files changed

+292
-0
lines changed

18 files changed

+292
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from '../../nodejs/dynamic-page/page'
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { default } from '../nodejs/layout'
2+
3+
export const runtime = 'edge'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from '../../nodejs/middleware/page'
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export { GET } from '../../nodejs/route/route'
2+
3+
export const runtime = 'edge'
4+
export const dynamic = 'force-dynamic'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from '../../nodejs/server-action/page'
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export default function AppLayout({ children }) {
2+
return (
3+
<html>
4+
<head>
5+
<title>after</title>
6+
</head>
7+
<body>{children}</body>
8+
</html>
9+
)
10+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { unstable_after as after } from 'next/server'
2+
import { revalidateTimestampPage } from '../../timestamp/revalidate'
3+
import { pathPrefix } from '../../path-prefix'
4+
5+
export default function Page() {
6+
after(async () => {
7+
await revalidateTimestampPage(pathPrefix + `/dynamic-page`)
8+
})
9+
10+
return <div>Page with after()</div>
11+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const runtime = 'nodejs'
2+
3+
export default function Layout({ children }) {
4+
return <>{children}</>
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <div>Redirect</div>
3+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { unstable_after as after } from 'next/server'
2+
import { revalidateTimestampPage } from '../../timestamp/revalidate'
3+
import { pathPrefix } from '../../path-prefix'
4+
5+
export const runtime = 'nodejs'
6+
export const dynamic = 'force-dynamic'
7+
8+
export async function GET() {
9+
const data = { message: 'Hello, world!' }
10+
after(async () => {
11+
await revalidateTimestampPage(pathPrefix + `/route`)
12+
})
13+
14+
return Response.json({ data })
15+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { unstable_after as after } from 'next/server'
2+
import { revalidateTimestampPage } from '../../timestamp/revalidate'
3+
import { pathPrefix } from '../../path-prefix'
4+
5+
export default function Page() {
6+
return (
7+
<div>
8+
<form
9+
action={async () => {
10+
'use server'
11+
after(async () => {
12+
await revalidateTimestampPage(pathPrefix + `/server-action`)
13+
})
14+
}}
15+
>
16+
<button type="submit">Submit</button>
17+
</form>
18+
</div>
19+
)
20+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const pathPrefix = '/' + process.env.NEXT_RUNTIME
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export const dynamic = 'error'
2+
export const revalidate = 3600 // arbitrarily long, just so that it doesn't happen during a test run
3+
export const dynamicParams = true
4+
5+
export async function generateStaticParams() {
6+
return []
7+
}
8+
9+
export default function Page({ params }) {
10+
const data = {
11+
key: params.key,
12+
timestamp: Date.now(),
13+
}
14+
console.log('/timestamp/key/[key] rendered', data)
15+
return <div id="page-info">{JSON.stringify(data)}</div>
16+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { revalidatePath } from 'next/cache'
2+
3+
export async function revalidateTimestampPage(/** @type {string} */ key) {
4+
const path = `/timestamp/key/${encodeURIComponent(key)}`
5+
6+
const sleepDuration = getSleepDuration()
7+
if (sleepDuration > 0) {
8+
console.log(`revalidateTimestampPage :: sleeping for ${sleepDuration} ms`)
9+
await sleep(sleepDuration)
10+
}
11+
12+
console.log('revalidateTimestampPage :: revalidating', path)
13+
revalidatePath(path)
14+
}
15+
16+
const WAIT_BEFORE_REVALIDATING_DEFAULT = 5000
17+
18+
function getSleepDuration() {
19+
const raw = process.env.WAIT_BEFORE_REVALIDATING
20+
if (!raw) return WAIT_BEFORE_REVALIDATING_DEFAULT
21+
22+
const parsed = Number.parseInt(raw)
23+
if (Number.isNaN(parsed)) {
24+
throw new Error(
25+
`WAIT_BEFORE_REVALIDATING must be a valid number, got: ${JSON.stringify(raw)}`
26+
)
27+
}
28+
return parsed
29+
}
30+
31+
function sleep(ms) {
32+
return new Promise((resolve) => setTimeout(resolve, ms))
33+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { revalidateTimestampPage } from '../revalidate'
2+
3+
export async function POST(/** @type {Request} */ request) {
4+
// we can't call revalidatePath from middleware, so we need to do it from here instead
5+
const path = new URL(request.url).searchParams.get('path')
6+
if (!path) {
7+
return Response.json(
8+
{ message: 'Missing "path" search param' },
9+
{ status: 400 }
10+
)
11+
}
12+
await revalidateTimestampPage(path)
13+
return Response.json({})
14+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/* eslint-env jest */
2+
import { nextTestSetup, isNextDev, isNextDeploy } from 'e2e-utils'
3+
import { retry } from 'next-test-utils'
4+
5+
const runtimes = ['nodejs', 'edge']
6+
7+
const WAIT_BEFORE_REVALIDATING = isNextDeploy ? 10_000 : 5_000
8+
9+
// If we want to verify that `unstable_after()` ran its callback,
10+
// we need it to perform some kind of side effect (because it can't affect the response).
11+
// In other tests, we often use logs for this, but we don't have access to those in deploy tests.
12+
// So instead this test relies on calling `revalidatePath` inside `unstable_after`
13+
// to revalidate an ISR page '/timestamp/key/[key]', and then checking if the timestamp changed --
14+
// if it did, we successfully ran the callback (and performed a side effect).
15+
16+
// This test relies on revalidating a static page, so it can't work in dev mode.
17+
const _describe = isNextDev ? describe : describe.skip
18+
19+
_describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => {
20+
const { next, skipped } = nextTestSetup({
21+
files: __dirname,
22+
env: { WAIT_BEFORE_REVALIDATING: WAIT_BEFORE_REVALIDATING + '' },
23+
})
24+
const retryDuration = WAIT_BEFORE_REVALIDATING * 2
25+
26+
if (skipped) return
27+
const pathPrefix = '/' + runtimeValue
28+
29+
type PageInfo = {
30+
key: string
31+
timestamp: number
32+
}
33+
34+
const getTimestampPageData = async (path: string): Promise<PageInfo> => {
35+
const fullPath = `/timestamp/key/${encodeURIComponent(path)}`
36+
const response = await next.render$(fullPath)
37+
const dataStr = response('#page-info').text()
38+
if (!dataStr) {
39+
throw new Error(`No page data found for '${fullPath}'`)
40+
}
41+
return JSON.parse(dataStr) as PageInfo
42+
}
43+
44+
it('triggers revalidate from a page', async () => {
45+
const path = pathPrefix + '/dynamic-page'
46+
const dataBefore = await getTimestampPageData(path)
47+
expect(dataBefore).toEqual(await getTimestampPageData(path)) // sanity check that it's static
48+
49+
await next.render(path) // trigger revalidate
50+
51+
await retry(
52+
async () => {
53+
const dataAfter = await getTimestampPageData(path)
54+
expect(dataAfter.timestamp).toBeGreaterThan(dataBefore.timestamp)
55+
},
56+
retryDuration,
57+
1000,
58+
'check if timestamp page updated'
59+
)
60+
})
61+
62+
it('triggers revalidate from a server action', async () => {
63+
const path = pathPrefix + '/server-action'
64+
const dataBefore = await getTimestampPageData(path)
65+
expect(dataBefore).toEqual(await getTimestampPageData(path)) // sanity check that it's static
66+
67+
const session = await next.browser(path)
68+
await session.elementByCss('button[type="submit"]').click() // trigger revalidate
69+
70+
await retry(
71+
async () => {
72+
const dataAfter = await getTimestampPageData(path)
73+
expect(dataAfter.timestamp).toBeGreaterThan(dataBefore.timestamp)
74+
},
75+
retryDuration,
76+
1000,
77+
'check if timestamp page updated'
78+
)
79+
})
80+
81+
it('triggers revalidate from a route handler', async () => {
82+
const path = pathPrefix + '/route'
83+
const dataBefore = await getTimestampPageData(path)
84+
expect(dataBefore).toEqual(await getTimestampPageData(path)) // sanity check that it's static
85+
86+
await next.fetch(path).then((res) => res.text()) // trigger revalidate
87+
88+
await retry(
89+
async () => {
90+
const dataAfter = await getTimestampPageData(path)
91+
expect(dataAfter.timestamp).toBeGreaterThan(dataBefore.timestamp)
92+
},
93+
retryDuration,
94+
1000,
95+
'check if timestamp page updated'
96+
)
97+
})
98+
99+
it('triggers revalidate from middleware', async () => {
100+
const path = pathPrefix + '/middleware'
101+
const dataBefore = await getTimestampPageData(path)
102+
expect(dataBefore).toEqual(await getTimestampPageData(path)) // sanity check that it's static
103+
104+
await next.render(path) // trigger revalidate
105+
106+
await retry(
107+
async () => {
108+
const dataAfter = await getTimestampPageData(path)
109+
expect(dataAfter.timestamp).toBeGreaterThan(dataBefore.timestamp)
110+
},
111+
retryDuration,
112+
1000,
113+
'check if timestamp page updated'
114+
)
115+
})
116+
})
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { unstable_after as after } from 'next/server'
2+
3+
export function middleware(
4+
/** @type {import ('next/server').NextRequest} */ request
5+
) {
6+
const url = new URL(request.url)
7+
8+
const match = url.pathname.match(/^(?<prefix>\/[^/]+?)\/middleware/)
9+
if (match) {
10+
const pathPrefix = match.groups.prefix
11+
after(async () => {
12+
// we can't call revalidatePath from middleware, so we need to do it via an endpoint instead
13+
const pathToRevalidate = pathPrefix + `/middleware`
14+
15+
const postUrl = new URL('/timestamp/trigger-revalidate', url.href)
16+
postUrl.searchParams.append('path', pathToRevalidate)
17+
18+
const response = await fetch(postUrl, { method: 'POST' })
19+
if (!response.ok) {
20+
throw new Error(
21+
`Failed to revalidate path '${pathToRevalidate}' (status: ${response.status})`
22+
)
23+
}
24+
})
25+
}
26+
}
27+
28+
export const config = {
29+
matcher: ['/:prefix/middleware'],
30+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/** @type {import('next').NextConfig} */
2+
module.exports = {
3+
experimental: {
4+
after: true,
5+
// DO NOT turn this on, it disables the incremental cache! (see `disableForTestmode`)
6+
// testProxy: true,
7+
},
8+
}

0 commit comments

Comments
 (0)