Skip to content

Commit

Permalink
test: revalidations within after()
Browse files Browse the repository at this point in the history
  • Loading branch information
lubieowoce committed Sep 25, 2024
1 parent f23d383 commit e19ddad
Show file tree
Hide file tree
Showing 18 changed files with 292 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '../../nodejs/dynamic-page/page'
3 changes: 3 additions & 0 deletions test/e2e/app-dir/next-after-app-deploy/app/edge/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default } from '../nodejs/layout'

export const runtime = 'edge'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '../../nodejs/middleware/page'
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { GET } from '../../nodejs/route/route'

export const runtime = 'edge'
export const dynamic = 'force-dynamic'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '../../nodejs/server-action/page'
10 changes: 10 additions & 0 deletions test/e2e/app-dir/next-after-app-deploy/app/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default function AppLayout({ children }) {
return (
<html>
<head>
<title>after</title>
</head>
<body>{children}</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { unstable_after as after } from 'next/server'
import { revalidateTimestampPage } from '../../timestamp/revalidate'
import { pathPrefix } from '../../path-prefix'

export default function Page() {
after(async () => {
await revalidateTimestampPage(pathPrefix + `/dynamic-page`)
})

return <div>Page with after()</div>
}
5 changes: 5 additions & 0 deletions test/e2e/app-dir/next-after-app-deploy/app/nodejs/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const runtime = 'nodejs'

export default function Layout({ children }) {
return <>{children}</>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <div>Redirect</div>
}
15 changes: 15 additions & 0 deletions test/e2e/app-dir/next-after-app-deploy/app/nodejs/route/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { unstable_after as after } from 'next/server'
import { revalidateTimestampPage } from '../../timestamp/revalidate'
import { pathPrefix } from '../../path-prefix'

export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'

export async function GET() {
const data = { message: 'Hello, world!' }
after(async () => {
await revalidateTimestampPage(pathPrefix + `/route`)
})

return Response.json({ data })
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { unstable_after as after } from 'next/server'
import { revalidateTimestampPage } from '../../timestamp/revalidate'
import { pathPrefix } from '../../path-prefix'

export default function Page() {
return (
<div>
<form
action={async () => {
'use server'
after(async () => {
await revalidateTimestampPage(pathPrefix + `/server-action`)
})
}}
>
<button type="submit">Submit</button>
</form>
</div>
)
}
1 change: 1 addition & 0 deletions test/e2e/app-dir/next-after-app-deploy/app/path-prefix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const pathPrefix = '/' + process.env.NEXT_RUNTIME
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const dynamic = 'error'
export const revalidate = 3600 // arbitrarily long, just so that it doesn't happen during a test run
export const dynamicParams = true

export async function generateStaticParams() {
return []
}

export default function Page({ params }) {
const data = {
key: params.key,
timestamp: Date.now(),
}
console.log('/timestamp/key/[key] rendered', data)
return <div id="page-info">{JSON.stringify(data)}</div>
}
33 changes: 33 additions & 0 deletions test/e2e/app-dir/next-after-app-deploy/app/timestamp/revalidate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { revalidatePath } from 'next/cache'

export async function revalidateTimestampPage(/** @type {string} */ key) {
const path = `/timestamp/key/${encodeURIComponent(key)}`

const sleepDuration = getSleepDuration()
if (sleepDuration > 0) {
console.log(`revalidateTimestampPage :: sleeping for ${sleepDuration} ms`)
await sleep(sleepDuration)
}

console.log('revalidateTimestampPage :: revalidating', path)
revalidatePath(path)
}

const WAIT_BEFORE_REVALIDATING_DEFAULT = 5000

function getSleepDuration() {
const raw = process.env.WAIT_BEFORE_REVALIDATING
if (!raw) return WAIT_BEFORE_REVALIDATING_DEFAULT

const parsed = Number.parseInt(raw)
if (Number.isNaN(parsed)) {
throw new Error(
`WAIT_BEFORE_REVALIDATING must be a valid number, got: ${JSON.stringify(raw)}`
)
}
return parsed
}

function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { revalidateTimestampPage } from '../revalidate'

export async function POST(/** @type {Request} */ request) {
// we can't call revalidatePath from middleware, so we need to do it from here instead
const path = new URL(request.url).searchParams.get('path')
if (!path) {
return Response.json(
{ message: 'Missing "path" search param' },
{ status: 400 }
)
}
await revalidateTimestampPage(path)
return Response.json({})
}
116 changes: 116 additions & 0 deletions test/e2e/app-dir/next-after-app-deploy/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/* eslint-env jest */
import { nextTestSetup, isNextDev, isNextDeploy } from 'e2e-utils'
import { retry } from 'next-test-utils'

const runtimes = ['nodejs', 'edge']

const WAIT_BEFORE_REVALIDATING = isNextDeploy ? 10_000 : 5_000

// If we want to verify that `unstable_after()` ran its callback,
// we need it to perform some kind of side effect (because it can't affect the response).
// In other tests, we often use logs for this, but we don't have access to those in deploy tests.
// So instead this test relies on calling `revalidatePath` inside `unstable_after`
// to revalidate an ISR page '/timestamp/key/[key]', and then checking if the timestamp changed --
// if it did, we successfully ran the callback (and performed a side effect).

// This test relies on revalidating a static page, so it can't work in dev mode.
const _describe = isNextDev ? describe : describe.skip

_describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => {
const { next, skipped } = nextTestSetup({
files: __dirname,
env: { WAIT_BEFORE_REVALIDATING: WAIT_BEFORE_REVALIDATING + '' },
})
const retryDuration = WAIT_BEFORE_REVALIDATING * 2

if (skipped) return
const pathPrefix = '/' + runtimeValue

type PageInfo = {
key: string
timestamp: number
}

const getTimestampPageData = async (path: string): Promise<PageInfo> => {
const fullPath = `/timestamp/key/${encodeURIComponent(path)}`
const response = await next.render$(fullPath)
const dataStr = response('#page-info').text()
if (!dataStr) {
throw new Error(`No page data found for '${fullPath}'`)
}
return JSON.parse(dataStr) as PageInfo
}

it('triggers revalidate from a page', async () => {
const path = pathPrefix + '/dynamic-page'
const dataBefore = await getTimestampPageData(path)
expect(dataBefore).toEqual(await getTimestampPageData(path)) // sanity check that it's static

await next.render(path) // trigger revalidate

await retry(
async () => {
const dataAfter = await getTimestampPageData(path)
expect(dataAfter.timestamp).toBeGreaterThan(dataBefore.timestamp)
},
retryDuration,
1000,
'check if timestamp page updated'
)
})

it('triggers revalidate from a server action', async () => {
const path = pathPrefix + '/server-action'
const dataBefore = await getTimestampPageData(path)
expect(dataBefore).toEqual(await getTimestampPageData(path)) // sanity check that it's static

const session = await next.browser(path)
await session.elementByCss('button[type="submit"]').click() // trigger revalidate

await retry(
async () => {
const dataAfter = await getTimestampPageData(path)
expect(dataAfter.timestamp).toBeGreaterThan(dataBefore.timestamp)
},
retryDuration,
1000,
'check if timestamp page updated'
)
})

it('triggers revalidate from a route handler', async () => {
const path = pathPrefix + '/route'
const dataBefore = await getTimestampPageData(path)
expect(dataBefore).toEqual(await getTimestampPageData(path)) // sanity check that it's static

await next.fetch(path).then((res) => res.text()) // trigger revalidate

await retry(
async () => {
const dataAfter = await getTimestampPageData(path)
expect(dataAfter.timestamp).toBeGreaterThan(dataBefore.timestamp)
},
retryDuration,
1000,
'check if timestamp page updated'
)
})

it('triggers revalidate from middleware', async () => {
const path = pathPrefix + '/middleware'
const dataBefore = await getTimestampPageData(path)
expect(dataBefore).toEqual(await getTimestampPageData(path)) // sanity check that it's static

await next.render(path) // trigger revalidate

await retry(
async () => {
const dataAfter = await getTimestampPageData(path)
expect(dataAfter.timestamp).toBeGreaterThan(dataBefore.timestamp)
},
retryDuration,
1000,
'check if timestamp page updated'
)
})
})
30 changes: 30 additions & 0 deletions test/e2e/app-dir/next-after-app-deploy/middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { unstable_after as after } from 'next/server'

export function middleware(
/** @type {import ('next/server').NextRequest} */ request
) {
const url = new URL(request.url)

const match = url.pathname.match(/^(?<prefix>\/[^/]+?)\/middleware/)
if (match) {
const pathPrefix = match.groups.prefix
after(async () => {
// we can't call revalidatePath from middleware, so we need to do it via an endpoint instead
const pathToRevalidate = pathPrefix + `/middleware`

const postUrl = new URL('/timestamp/trigger-revalidate', url.href)
postUrl.searchParams.append('path', pathToRevalidate)

const response = await fetch(postUrl, { method: 'POST' })
if (!response.ok) {
throw new Error(
`Failed to revalidate path '${pathToRevalidate}' (status: ${response.status})`
)
}
})
}
}

export const config = {
matcher: ['/:prefix/middleware'],
}
8 changes: 8 additions & 0 deletions test/e2e/app-dir/next-after-app-deploy/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('next').NextConfig} */
module.exports = {
experimental: {
after: true,
// DO NOT turn this on, it disables the incremental cache! (see `disableForTestmode`)
// testProxy: true,
},
}

0 comments on commit e19ddad

Please sign in to comment.