diff --git a/test/e2e/app-dir/next-after-app/app/invalid-in-client/page.js b/test/development/app-dir/next-after-app-invalid-usage/app/invalid-in-client/page.js
similarity index 94%
rename from test/e2e/app-dir/next-after-app/app/invalid-in-client/page.js
rename to test/development/app-dir/next-after-app-invalid-usage/app/invalid-in-client/page.js
index 6b185f4514b5f..3c3966b5c18f9 100644
--- a/test/e2e/app-dir/next-after-app/app/invalid-in-client/page.js
+++ b/test/development/app-dir/next-after-app-invalid-usage/app/invalid-in-client/page.js
@@ -1,4 +1,4 @@
-// 'use client'
+'use client'
import { unstable_after as after } from 'next/server'
import { cliLog } from '../../utils/log'
diff --git a/test/e2e/app-dir/next-after-app/app/static/page.js b/test/development/app-dir/next-after-app-invalid-usage/app/invalid-in-dynamic-error/page.js
similarity index 66%
rename from test/e2e/app-dir/next-after-app/app/static/page.js
rename to test/development/app-dir/next-after-app-invalid-usage/app/invalid-in-dynamic-error/page.js
index 80fafb550f11f..a2e1714170741 100644
--- a/test/e2e/app-dir/next-after-app/app/static/page.js
+++ b/test/development/app-dir/next-after-app-invalid-usage/app/invalid-in-dynamic-error/page.js
@@ -1,12 +1,11 @@
import { unstable_after as after } from 'next/server'
import { cliLog } from '../../utils/log'
-// (patched in tests)
-// export const dynamic = 'REPLACE_ME'
+export const dynamic = 'error'
export default function Index() {
after(async () => {
- cliLog({ source: '[page] /static' })
+ cliLog({ source: '[page] /invalid-in-dynamic-error' })
})
return
Page with after()
}
diff --git a/test/development/app-dir/next-after-app-invalid-usage/app/invalid-in-dynamic-force-static/page.js b/test/development/app-dir/next-after-app-invalid-usage/app/invalid-in-dynamic-force-static/page.js
new file mode 100644
index 0000000000000..e3d86ec431c50
--- /dev/null
+++ b/test/development/app-dir/next-after-app-invalid-usage/app/invalid-in-dynamic-force-static/page.js
@@ -0,0 +1,11 @@
+import { unstable_after as after } from 'next/server'
+import { cliLog } from '../../utils/log'
+
+export const dynamic = 'force-static'
+
+export default function Index() {
+ after(async () => {
+ cliLog({ source: '[page] /invalid-in-dynamic-force-static' })
+ })
+ return Page with after()
+}
diff --git a/test/development/app-dir/next-after-app-invalid-usage/app/layout.js b/test/development/app-dir/next-after-app-invalid-usage/app/layout.js
new file mode 100644
index 0000000000000..a55016aeb623b
--- /dev/null
+++ b/test/development/app-dir/next-after-app-invalid-usage/app/layout.js
@@ -0,0 +1,10 @@
+export default function AppLayout({ children }) {
+ return (
+
+
+ after
+
+ {children}
+
+ )
+}
diff --git a/test/development/app-dir/next-after-app-invalid-usage/index.test.ts b/test/development/app-dir/next-after-app-invalid-usage/index.test.ts
new file mode 100644
index 0000000000000..e9210b5326ed7
--- /dev/null
+++ b/test/development/app-dir/next-after-app-invalid-usage/index.test.ts
@@ -0,0 +1,50 @@
+/* eslint-env jest */
+import { nextTestSetup } from 'e2e-utils'
+import * as Log from './utils/log'
+import {
+ assertHasRedbox,
+ getRedboxDescription,
+ getRedboxSource,
+} from '../../../lib/next-test-utils'
+
+describe('unstable_after() - invalid usages', () => {
+ const { next } = nextTestSetup({
+ files: __dirname,
+ })
+
+ let currentCliOutputIndex = 0
+ beforeEach(() => {
+ currentCliOutputIndex = next.cliOutput.length
+ })
+
+ const getLogs = () => {
+ if (next.cliOutput.length < currentCliOutputIndex) {
+ // cliOutput shrank since we started the test, so something (like a `sandbox`) reset the logs
+ currentCliOutputIndex = 0
+ }
+ return Log.readCliLogs(next.cliOutput.slice(currentCliOutputIndex))
+ }
+
+ it.each(['error', 'force-static'])(
+ `errors at compile time with dynamic = "%s"`,
+ async (dynamicValue) => {
+ const pathname = '/invalid-in-dynamic-' + dynamicValue
+ const session = await next.browser(pathname)
+
+ await assertHasRedbox(session)
+ expect(await getRedboxDescription(session)).toContain(
+ `Route ${pathname} with \`dynamic = "${dynamicValue}"\` couldn't be rendered statically because it used \`unstable_after\``
+ )
+ expect(getLogs()).toHaveLength(0)
+ }
+ )
+ it('errors at compile time when used in a client module', async () => {
+ const session = await next.browser('/invalid-in-client')
+
+ await assertHasRedbox(session)
+ expect(await getRedboxSource(session)).toMatch(
+ /You're importing a component that needs "?unstable_after"?\. That only works in a Server Component but one of its parents is marked with "use client", so it's a Client Component\./
+ )
+ expect(getLogs()).toHaveLength(0)
+ })
+})
diff --git a/test/development/app-dir/next-after-app-invalid-usage/next.config.js b/test/development/app-dir/next-after-app-invalid-usage/next.config.js
new file mode 100644
index 0000000000000..ec0f3bcc9dad4
--- /dev/null
+++ b/test/development/app-dir/next-after-app-invalid-usage/next.config.js
@@ -0,0 +1,6 @@
+/** @type {import('next').NextConfig} */
+module.exports = {
+ experimental: {
+ after: true,
+ },
+}
diff --git a/test/development/app-dir/next-after-app-invalid-usage/utils/log.js b/test/development/app-dir/next-after-app-invalid-usage/utils/log.js
new file mode 100644
index 0000000000000..7811c609528ca
--- /dev/null
+++ b/test/development/app-dir/next-after-app-invalid-usage/utils/log.js
@@ -0,0 +1,16 @@
+export function cliLog(/** @type {string | Record} */ data) {
+ console.log('' + JSON.stringify(data) + '')
+}
+
+export function readCliLogs(/** @type {string} */ output) {
+ return output
+ .split('\n')
+ .map((line) => {
+ const match = line.match(/^(?.+?)<\/test-log>$/)
+ if (!match) {
+ return null
+ }
+ return JSON.parse(match.groups.value)
+ })
+ .filter(Boolean)
+}
diff --git a/test/e2e/app-dir/next-after-app/app/edge/[id]/dynamic/page.js b/test/e2e/app-dir/next-after-app/app/edge/[id]/dynamic/page.js
new file mode 100644
index 0000000000000..a3fef8fa1ba42
--- /dev/null
+++ b/test/e2e/app-dir/next-after-app/app/edge/[id]/dynamic/page.js
@@ -0,0 +1 @@
+export { default } from '../../../nodejs/[id]/dynamic/page'
diff --git a/test/e2e/app-dir/next-after-app/app/edge/[id]/layout.js b/test/e2e/app-dir/next-after-app/app/edge/[id]/layout.js
new file mode 100644
index 0000000000000..347eaad656059
--- /dev/null
+++ b/test/e2e/app-dir/next-after-app/app/edge/[id]/layout.js
@@ -0,0 +1 @@
+export { default } from '../../nodejs/[id]/layout'
diff --git a/test/e2e/app-dir/next-after-app/app/edge/[id]/setting-cookies/page.js b/test/e2e/app-dir/next-after-app/app/edge/[id]/setting-cookies/page.js
new file mode 100644
index 0000000000000..7d4b31e8d13d5
--- /dev/null
+++ b/test/e2e/app-dir/next-after-app/app/edge/[id]/setting-cookies/page.js
@@ -0,0 +1 @@
+export { default } from '../../../nodejs/[id]/setting-cookies/page'
diff --git a/test/e2e/app-dir/next-after-app/app/edge/[id]/with-action/page.js b/test/e2e/app-dir/next-after-app/app/edge/[id]/with-action/page.js
new file mode 100644
index 0000000000000..6cdab36d3a33e
--- /dev/null
+++ b/test/e2e/app-dir/next-after-app/app/edge/[id]/with-action/page.js
@@ -0,0 +1 @@
+export { default } from '../../../nodejs/[id]/with-action/page'
diff --git a/test/e2e/app-dir/next-after-app/app/edge/[id]/with-metadata/page.js b/test/e2e/app-dir/next-after-app/app/edge/[id]/with-metadata/page.js
new file mode 100644
index 0000000000000..7516c6ada37d1
--- /dev/null
+++ b/test/e2e/app-dir/next-after-app/app/edge/[id]/with-metadata/page.js
@@ -0,0 +1,4 @@
+export {
+ default,
+ generateMetadata,
+} from '../../../nodejs/[id]/with-metadata/page'
diff --git a/test/e2e/app-dir/next-after-app/app/edge/delay/page.js b/test/e2e/app-dir/next-after-app/app/edge/delay/page.js
new file mode 100644
index 0000000000000..95d978bf462c9
--- /dev/null
+++ b/test/e2e/app-dir/next-after-app/app/edge/delay/page.js
@@ -0,0 +1,3 @@
+export { default } from '../../nodejs/delay/page'
+
+export const dynamic = 'force-dynamic'
diff --git a/test/e2e/app-dir/next-after-app/app/edge/interrupted/calls-not-found/page.js b/test/e2e/app-dir/next-after-app/app/edge/interrupted/calls-not-found/page.js
new file mode 100644
index 0000000000000..76c3dd48baf60
--- /dev/null
+++ b/test/e2e/app-dir/next-after-app/app/edge/interrupted/calls-not-found/page.js
@@ -0,0 +1 @@
+export { default } from '../../../nodejs/interrupted/calls-not-found/page'
diff --git a/test/e2e/app-dir/next-after-app/app/edge/interrupted/calls-redirect/page.js b/test/e2e/app-dir/next-after-app/app/edge/interrupted/calls-redirect/page.js
new file mode 100644
index 0000000000000..28367a391dfb8
--- /dev/null
+++ b/test/e2e/app-dir/next-after-app/app/edge/interrupted/calls-redirect/page.js
@@ -0,0 +1,5 @@
+import { createPage } from '../../../nodejs/interrupted/calls-redirect/page'
+
+// NOTE: this page is forked from /nodejs
+
+export default createPage('/edge')
diff --git a/test/e2e/app-dir/next-after-app/app/edge/interrupted/redirect-target/page.js b/test/e2e/app-dir/next-after-app/app/edge/interrupted/redirect-target/page.js
new file mode 100644
index 0000000000000..bdf37ff651bba
--- /dev/null
+++ b/test/e2e/app-dir/next-after-app/app/edge/interrupted/redirect-target/page.js
@@ -0,0 +1 @@
+export { default } from '../../../nodejs/interrupted/redirect-target/page'
diff --git a/test/e2e/app-dir/next-after-app/app/edge/interrupted/throws-error/page.js b/test/e2e/app-dir/next-after-app/app/edge/interrupted/throws-error/page.js
new file mode 100644
index 0000000000000..5f2caf9a6e225
--- /dev/null
+++ b/test/e2e/app-dir/next-after-app/app/edge/interrupted/throws-error/page.js
@@ -0,0 +1 @@
+export { default } from '../../../nodejs/interrupted/throws-error/page'
diff --git a/test/e2e/app-dir/next-after-app/app/edge/layout.js b/test/e2e/app-dir/next-after-app/app/edge/layout.js
new file mode 100644
index 0000000000000..b19ff8efbdce8
--- /dev/null
+++ b/test/e2e/app-dir/next-after-app/app/edge/layout.js
@@ -0,0 +1,3 @@
+export { default } from '../nodejs/layout'
+
+export const runtime = 'edge'
diff --git a/test/e2e/app-dir/next-after-app/app/edge/middleware/redirect/page.js b/test/e2e/app-dir/next-after-app/app/edge/middleware/redirect/page.js
new file mode 100644
index 0000000000000..c5a458e6e3fe1
--- /dev/null
+++ b/test/e2e/app-dir/next-after-app/app/edge/middleware/redirect/page.js
@@ -0,0 +1 @@
+export { default } from '../../../nodejs/middleware/redirect/page'
diff --git a/test/e2e/app-dir/next-after-app/app/edge/nested-after/page.js b/test/e2e/app-dir/next-after-app/app/edge/nested-after/page.js
new file mode 100644
index 0000000000000..549fd5b9e0ab5
--- /dev/null
+++ b/test/e2e/app-dir/next-after-app/app/edge/nested-after/page.js
@@ -0,0 +1 @@
+export { default } from '../../nodejs/nested-after/page'
diff --git a/test/e2e/app-dir/next-after-app/app/edge/provided-request-context/page.js b/test/e2e/app-dir/next-after-app/app/edge/provided-request-context/page.js
new file mode 100644
index 0000000000000..a38874129d7ee
--- /dev/null
+++ b/test/e2e/app-dir/next-after-app/app/edge/provided-request-context/page.js
@@ -0,0 +1 @@
+export { default } from '../../nodejs/provided-request-context/page'
diff --git a/test/e2e/app-dir/next-after-app/app/edge/route/route.js b/test/e2e/app-dir/next-after-app/app/edge/route/route.js
new file mode 100644
index 0000000000000..8426f52cb33cd
--- /dev/null
+++ b/test/e2e/app-dir/next-after-app/app/edge/route/route.js
@@ -0,0 +1,4 @@
+export { GET } from '../../nodejs/route/route'
+
+export const runtime = 'edge'
+export const dynamic = 'force-dynamic'
diff --git a/test/e2e/app-dir/next-after-app/app/interrupted/calls-redirect/page.js b/test/e2e/app-dir/next-after-app/app/interrupted/calls-redirect/page.js
deleted file mode 100644
index 29eb6d9d15f7e..0000000000000
--- a/test/e2e/app-dir/next-after-app/app/interrupted/calls-redirect/page.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { redirect } from 'next/navigation'
-import { unstable_after as after } from 'next/server'
-import { cliLog } from '../../../utils/log'
-
-export default function Page() {
- after(() => {
- cliLog({
- source: '[page] /interrupted/calls-redirect',
- })
- })
- redirect('/interrupted/redirect-target')
-}
diff --git a/test/e2e/app-dir/next-after-app/app/[id]/dynamic/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/[id]/dynamic/page.js
similarity index 93%
rename from test/e2e/app-dir/next-after-app/app/[id]/dynamic/page.js
rename to test/e2e/app-dir/next-after-app/app/nodejs/[id]/dynamic/page.js
index 3794116ed4e8a..1903f5ce67113 100644
--- a/test/e2e/app-dir/next-after-app/app/[id]/dynamic/page.js
+++ b/test/e2e/app-dir/next-after-app/app/nodejs/[id]/dynamic/page.js
@@ -1,6 +1,6 @@
import { unstable_after as after } from 'next/server'
import { cache } from 'react'
-import { cliLog } from '../../../utils/log'
+import { cliLog } from '../../../../utils/log'
import { headers } from 'next/headers'
const thing = cache(() => Symbol('cache me please'))
diff --git a/test/e2e/app-dir/next-after-app/app/[id]/layout.js b/test/e2e/app-dir/next-after-app/app/nodejs/[id]/layout.js
similarity index 81%
rename from test/e2e/app-dir/next-after-app/app/[id]/layout.js
rename to test/e2e/app-dir/next-after-app/app/nodejs/[id]/layout.js
index 9c589f027344d..9bfd4d7040361 100644
--- a/test/e2e/app-dir/next-after-app/app/[id]/layout.js
+++ b/test/e2e/app-dir/next-after-app/app/nodejs/[id]/layout.js
@@ -1,5 +1,5 @@
import { unstable_after as after } from 'next/server'
-import { cliLog } from '../../utils/log'
+import { cliLog } from '../../../utils/log'
export default function Layout({ children }) {
after(async () => {
diff --git a/test/e2e/app-dir/next-after-app/app/[id]/setting-cookies/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/[id]/setting-cookies/page.js
similarity index 100%
rename from test/e2e/app-dir/next-after-app/app/[id]/setting-cookies/page.js
rename to test/e2e/app-dir/next-after-app/app/nodejs/[id]/setting-cookies/page.js
diff --git a/test/e2e/app-dir/next-after-app/app/[id]/with-action/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/[id]/with-action/page.js
similarity index 95%
rename from test/e2e/app-dir/next-after-app/app/[id]/with-action/page.js
rename to test/e2e/app-dir/next-after-app/app/nodejs/[id]/with-action/page.js
index 87108571b308f..37a098c12c2f8 100644
--- a/test/e2e/app-dir/next-after-app/app/[id]/with-action/page.js
+++ b/test/e2e/app-dir/next-after-app/app/nodejs/[id]/with-action/page.js
@@ -1,6 +1,6 @@
import { unstable_after as after } from 'next/server'
import { cache } from 'react'
-import { cliLog } from '../../../utils/log'
+import { cliLog } from '../../../../utils/log'
import { headers } from 'next/headers'
const thing = cache(() => Symbol('cache me please'))
diff --git a/test/e2e/app-dir/next-after-app/app/[id]/with-metadata/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/[id]/with-metadata/page.js
similarity index 87%
rename from test/e2e/app-dir/next-after-app/app/[id]/with-metadata/page.js
rename to test/e2e/app-dir/next-after-app/app/nodejs/[id]/with-metadata/page.js
index 8209df109face..696440e417d23 100644
--- a/test/e2e/app-dir/next-after-app/app/[id]/with-metadata/page.js
+++ b/test/e2e/app-dir/next-after-app/app/nodejs/[id]/with-metadata/page.js
@@ -1,5 +1,5 @@
import { unstable_after as after } from 'next/server'
-import { cliLog } from '../../../utils/log'
+import { cliLog } from '../../../../utils/log'
export function generateMetadata({ params }) {
after(() => {
diff --git a/test/e2e/app-dir/next-after-app/app/delay/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/delay/page.js
similarity index 94%
rename from test/e2e/app-dir/next-after-app/app/delay/page.js
rename to test/e2e/app-dir/next-after-app/app/nodejs/delay/page.js
index bf832369c558e..a168846557490 100644
--- a/test/e2e/app-dir/next-after-app/app/delay/page.js
+++ b/test/e2e/app-dir/next-after-app/app/nodejs/delay/page.js
@@ -1,6 +1,6 @@
import { Suspense } from 'react'
import { unstable_after as after } from 'next/server'
-import { cliLog } from '../../utils/log'
+import { cliLog } from '../../../utils/log'
export const dynamic = 'force-dynamic'
diff --git a/test/e2e/app-dir/next-after-app/app/interrupted/calls-not-found/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/interrupted/calls-not-found/page.js
similarity index 83%
rename from test/e2e/app-dir/next-after-app/app/interrupted/calls-not-found/page.js
rename to test/e2e/app-dir/next-after-app/app/nodejs/interrupted/calls-not-found/page.js
index 19fb589c8e24e..9ac1c2e1881bc 100644
--- a/test/e2e/app-dir/next-after-app/app/interrupted/calls-not-found/page.js
+++ b/test/e2e/app-dir/next-after-app/app/nodejs/interrupted/calls-not-found/page.js
@@ -1,6 +1,6 @@
import { notFound } from 'next/navigation'
import { unstable_after as after } from 'next/server'
-import { cliLog } from '../../../utils/log'
+import { cliLog } from '../../../../utils/log'
export default function Page() {
after(() => {
diff --git a/test/e2e/app-dir/next-after-app/app/nodejs/interrupted/calls-redirect/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/interrupted/calls-redirect/page.js
new file mode 100644
index 0000000000000..01e5824710f9a
--- /dev/null
+++ b/test/e2e/app-dir/next-after-app/app/nodejs/interrupted/calls-redirect/page.js
@@ -0,0 +1,19 @@
+import { unstable_after as after } from 'next/server'
+import { redirect } from 'next/navigation'
+import { cliLog } from '../../../../utils/log'
+
+// NOTE: this page is forked in /edge
+
+export function createPage(pathPrefix) {
+ return function Page() {
+ after(() => {
+ cliLog({
+ source: '[page] /interrupted/calls-redirect',
+ })
+ })
+
+ redirect(pathPrefix + '/interrupted/redirect-target')
+ }
+}
+
+export default createPage('/nodejs')
diff --git a/test/e2e/app-dir/next-after-app/app/interrupted/redirect-target/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/interrupted/redirect-target/page.js
similarity index 81%
rename from test/e2e/app-dir/next-after-app/app/interrupted/redirect-target/page.js
rename to test/e2e/app-dir/next-after-app/app/nodejs/interrupted/redirect-target/page.js
index 09aea952148c6..fbf0aceaa1ad5 100644
--- a/test/e2e/app-dir/next-after-app/app/interrupted/redirect-target/page.js
+++ b/test/e2e/app-dir/next-after-app/app/nodejs/interrupted/redirect-target/page.js
@@ -1,5 +1,5 @@
import { unstable_after as after } from 'next/server'
-import { cliLog } from '../../../utils/log'
+import { cliLog } from '../../../../utils/log'
export default function Page() {
after(() => {
diff --git a/test/e2e/app-dir/next-after-app/app/interrupted/throws-error/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/interrupted/throws-error/page.js
similarity index 81%
rename from test/e2e/app-dir/next-after-app/app/interrupted/throws-error/page.js
rename to test/e2e/app-dir/next-after-app/app/nodejs/interrupted/throws-error/page.js
index 2eee04cf2463b..ff7c1f5ff71d5 100644
--- a/test/e2e/app-dir/next-after-app/app/interrupted/throws-error/page.js
+++ b/test/e2e/app-dir/next-after-app/app/nodejs/interrupted/throws-error/page.js
@@ -1,5 +1,5 @@
import { unstable_after as after } from 'next/server'
-import { cliLog } from '../../../utils/log'
+import { cliLog } from '../../../../utils/log'
export default function Page() {
after(() => {
diff --git a/test/e2e/app-dir/next-after-app/app/layout.js b/test/e2e/app-dir/next-after-app/app/nodejs/layout.js
similarity index 74%
rename from test/e2e/app-dir/next-after-app/app/layout.js
rename to test/e2e/app-dir/next-after-app/app/nodejs/layout.js
index bece157b43025..3b3d1f37a51a0 100644
--- a/test/e2e/app-dir/next-after-app/app/layout.js
+++ b/test/e2e/app-dir/next-after-app/app/nodejs/layout.js
@@ -1,5 +1,4 @@
-// (patched in tests)
-// export const runtime = 'REPLACE_ME'
+export const runtime = 'nodejs'
export default function AppLayout({ children }) {
return (
diff --git a/test/e2e/app-dir/next-after-app/app/middleware/redirect/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/middleware/redirect/page.js
similarity index 100%
rename from test/e2e/app-dir/next-after-app/app/middleware/redirect/page.js
rename to test/e2e/app-dir/next-after-app/app/nodejs/middleware/redirect/page.js
diff --git a/test/e2e/app-dir/next-after-app/app/nested-after/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/nested-after/page.js
similarity index 97%
rename from test/e2e/app-dir/next-after-app/app/nested-after/page.js
rename to test/e2e/app-dir/next-after-app/app/nodejs/nested-after/page.js
index 39b1dbaffe49c..62b8460eb56cf 100644
--- a/test/e2e/app-dir/next-after-app/app/nested-after/page.js
+++ b/test/e2e/app-dir/next-after-app/app/nodejs/nested-after/page.js
@@ -1,6 +1,6 @@
import { unstable_after as after } from 'next/server'
import { cache } from 'react'
-import { cliLog } from '../../utils/log'
+import { cliLog } from '../../../utils/log'
import { headers } from 'next/headers'
const thing = cache(() => Symbol('cache me please'))
diff --git a/test/e2e/app-dir/next-after-app/app/provided-request-context/page.js b/test/e2e/app-dir/next-after-app/app/nodejs/provided-request-context/page.js
similarity index 83%
rename from test/e2e/app-dir/next-after-app/app/provided-request-context/page.js
rename to test/e2e/app-dir/next-after-app/app/nodejs/provided-request-context/page.js
index 4650954dec10b..b984c265810d5 100644
--- a/test/e2e/app-dir/next-after-app/app/provided-request-context/page.js
+++ b/test/e2e/app-dir/next-after-app/app/nodejs/provided-request-context/page.js
@@ -1,5 +1,5 @@
import { unstable_after as after } from 'next/server'
-import { cliLog } from '../../utils/log'
+import { cliLog } from '../../../utils/log'
export default function Page() {
after(() => {
diff --git a/test/e2e/app-dir/next-after-app/app/route/route.js b/test/e2e/app-dir/next-after-app/app/nodejs/route/route.js
similarity index 72%
rename from test/e2e/app-dir/next-after-app/app/route/route.js
rename to test/e2e/app-dir/next-after-app/app/nodejs/route/route.js
index e8c6290fc9011..7ebdad2dd54ea 100644
--- a/test/e2e/app-dir/next-after-app/app/route/route.js
+++ b/test/e2e/app-dir/next-after-app/app/nodejs/route/route.js
@@ -1,9 +1,7 @@
import { unstable_after as after } from 'next/server'
-import { cliLog } from '../../utils/log'
-
-// (patched in tests)
-// export const runtime = 'REPLACE_ME'
+import { cliLog } from '../../../utils/log'
+export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
export async function GET() {
diff --git a/test/e2e/app-dir/next-after-app/index.test.ts b/test/e2e/app-dir/next-after-app/index.test.ts
index 4fdc5d7dc1b03..efcba7aaa0c1d 100644
--- a/test/e2e/app-dir/next-after-app/index.test.ts
+++ b/test/e2e/app-dir/next-after-app/index.test.ts
@@ -4,57 +4,19 @@ import { retry } from 'next-test-utils'
import { createProxyServer } from 'next/experimental/testmode/proxy'
import { outdent } from 'outdent'
import { sandbox } from '../../../lib/development-sandbox'
-import * as fs from 'fs'
-import * as path from 'path'
-import * as os from 'os'
import * as Log from './utils/log'
const runtimes = ['nodejs', 'edge']
describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => {
- const logFileDir = fs.mkdtempSync(path.join(os.tmpdir(), 'logs-'))
- const logFile = path.join(logFileDir, 'logs.jsonl')
-
- const { next, isNextDev, isNextDeploy, skipped } = nextTestSetup({
+ const { next, isNextDeploy, skipped } = nextTestSetup({
files: __dirname,
// `patchFile` and reading runtime logs are not supported in a deployed environment
skipDeployment: true,
- env: {
- PERSISTENT_LOG_FILE: logFile,
- },
})
if (skipped) return
-
- {
- const originalContents: Record = {}
-
- beforeAll(async () => {
- const placeholder = `// export const runtime = 'REPLACE_ME'`
-
- const filesToPatch = ['app/layout.js', 'app/route/route.js']
-
- for (const file of filesToPatch) {
- await next.patchFile(file, (contents) => {
- if (!contents.includes(placeholder)) {
- throw new Error(`Placeholder "${placeholder}" not found in ${file}`)
- }
- originalContents[file] = contents
-
- return contents.replace(
- placeholder,
- `export const runtime = '${runtimeValue}'`
- )
- })
- }
- })
-
- afterAll(async () => {
- for (const [file, contents] of Object.entries(originalContents)) {
- await next.patchFile(file, contents)
- }
- })
- }
+ const pathPrefix = '/' + runtimeValue
let currentCliOutputIndex = 0
beforeEach(() => {
@@ -70,7 +32,8 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => {
}
it('runs in dynamic pages', async () => {
- await next.render('/123/dynamic')
+ const response = await next.fetch(pathPrefix + '/123/dynamic')
+ expect(response.status).toBe(200)
await retry(() => {
expect(getLogs()).toContainEqual({ source: '[layout] /[id]' })
expect(getLogs()).toContainEqual({
@@ -85,7 +48,7 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => {
})
it('runs in dynamic route handlers', async () => {
- const res = await next.fetch('/route')
+ const res = await next.fetch(pathPrefix + '/route')
expect(res.status).toBe(200)
await retry(() => {
expect(getLogs()).toContainEqual({ source: '[route handler] /route' })
@@ -93,7 +56,7 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => {
})
it('runs in server actions', async () => {
- const browser = await next.browser('/123/with-action')
+ const browser = await next.browser(pathPrefix + '/123/with-action')
expect(getLogs()).toContainEqual({
source: '[layout] /[id]',
})
@@ -114,7 +77,7 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => {
})
it('runs callbacks from nested unstable_after calls', async () => {
- await next.browser('/nested-after')
+ await next.browser(pathPrefix + '/nested-after')
await retry(() => {
for (const id of [1, 2, 3]) {
@@ -131,7 +94,7 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => {
describe('interrupted RSC renders', () => {
it('runs callbacks if redirect() was called', async () => {
- await next.browser('/interrupted/calls-redirect')
+ await next.browser(pathPrefix + '/interrupted/calls-redirect')
await retry(() => {
expect(getLogs()).toContainEqual({
@@ -144,14 +107,14 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => {
})
it('runs callbacks if notFound() was called', async () => {
- await next.browser('/interrupted/calls-not-found')
+ await next.browser(pathPrefix + '/interrupted/calls-not-found')
expect(getLogs()).toContainEqual({
source: '[page] /interrupted/calls-not-found',
})
})
it('runs callbacks if a user error was thrown in the RSC render', async () => {
- await next.browser('/interrupted/throws-error')
+ await next.browser(pathPrefix + '/interrupted/throws-error')
expect(getLogs()).toContainEqual({
source: '[page] /interrupted/throws-error',
})
@@ -161,7 +124,7 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => {
it('runs in middleware', async () => {
const requestId = `${Date.now()}`
const res = await next.fetch(
- `/middleware/redirect-source?requestId=${requestId}`,
+ pathPrefix + `/middleware/redirect-source?requestId=${requestId}`,
{
redirect: 'follow',
headers: {
@@ -183,10 +146,19 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => {
if (!isNextDeploy) {
it('only runs callbacks after the response is fully sent', async () => {
const pageStartedFetching = promiseWithResolvers()
+ pageStartedFetching.promise.catch(() => {})
const shouldSendResponse = promiseWithResolvers()
+ shouldSendResponse.promise.catch(() => {})
+
const abort = (error: Error) => {
- pageStartedFetching.reject(error)
- shouldSendResponse.reject(error)
+ pageStartedFetching.reject(
+ new Error('pageStartedFetching was aborted', { cause: error })
+ )
+ shouldSendResponse.reject(
+ new Error('shouldSendResponse was aborted', {
+ cause: error,
+ })
+ )
}
const proxyServer = await createProxyServer({
@@ -200,25 +172,30 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => {
})
try {
- const pendingReq = next.fetch('/delay', {
- headers: { 'Next-Test-Proxy-Port': String(proxyServer.port) },
- })
-
- pendingReq.then(
- async (res) => {
- if (res.status !== 200) {
- const msg = `Got non-200 response (${res.status}), aborting`
- console.error(msg + '\n', await res.text())
- abort(new Error(msg))
+ const pendingReq = next
+ .fetch(pathPrefix + '/delay', {
+ headers: { 'Next-Test-Proxy-Port': String(proxyServer.port) },
+ })
+ .then(
+ async (res) => {
+ if (res.status !== 200) {
+ const err = new Error(
+ `Got non-200 response (${res.status}) for ${res.url}, aborting`
+ )
+ abort(err)
+ throw err
+ }
+ return res
+ },
+ (err) => {
+ abort(err)
+ throw err
}
- },
- (err) => {
- abort(err)
- }
- )
+ )
await Promise.race([
pageStartedFetching.promise,
+ pendingReq, // if the page throws before it starts fetching, we want to catch that
timeoutPromise(
10_000,
'Timeout while waiting for the page to call fetch'
@@ -253,7 +230,7 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => {
}
it('runs in generateMetadata()', async () => {
- await next.browser('/123/with-metadata')
+ await next.browser(pathPrefix + '/123/with-metadata')
expect(getLogs()).toContainEqual({
source: '[metadata] /[id]/with-metadata',
value: '123',
@@ -264,7 +241,7 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => {
const EXPECTED_ERROR =
/An error occurred in a function passed to `unstable_after\(\)`: .+?: Cookies can only be modified in a Server Action or Route Handler\./
- const browser = await next.browser('/123/setting-cookies')
+ const browser = await next.browser(pathPrefix + '/123/setting-cookies')
// after() from render
expect(next.cliOutput).toMatch(EXPECTED_ERROR)
@@ -302,7 +279,7 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => {
`,
],
]),
- '/provided-request-context'
+ pathPrefix + '/provided-request-context'
)
try {
@@ -319,67 +296,6 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => {
await cleanup()
}
})
-
- if (isNextDev) {
- // TODO: these are at the end because they destroy the next server.
- // is there a cleaner way to do this without making the tests slower?
-
- describe('invalid usages', () => {
- it.each(['error', 'force-static'])(
- `errors at compile time with dynamic = "%s"`,
- async (dynamicValue) => {
- const { session, cleanup } = await sandbox(
- next,
- new Map([
- [
- 'app/static/page.js',
- (await next.readFile('app/static/page.js')).replace(
- `// export const dynamic = 'REPLACE_ME'`,
- `export const dynamic = '${dynamicValue}'`
- ),
- ],
- ]),
- '/static'
- )
-
- try {
- await session.assertHasRedbox()
- expect(await session.getRedboxDescription()).toContain(
- `Route /static with \`dynamic = "${dynamicValue}"\` couldn't be rendered statically because it used \`unstable_after\``
- )
- expect(getLogs()).toHaveLength(0)
- } finally {
- await cleanup()
- }
- }
- )
-
- it('errors at compile time when used in a client module', async () => {
- const { session, cleanup } = await sandbox(
- next,
- new Map([
- [
- 'app/invalid-in-client/page.js',
- (await next.readFile('app/invalid-in-client/page.js')).replace(
- `// 'use client'`,
- `'use client'`
- ),
- ],
- ]),
- '/invalid-in-client'
- )
- try {
- await session.assertHasRedbox()
- expect(await session.getRedboxSource(true)).toMatch(
- /You're importing a component that needs "?unstable_after"?\. That only works in a Server Component but one of its parents is marked with "use client", so it's a Client Component\./
- )
- expect(getLogs()).toHaveLength(0)
- } finally {
- await cleanup()
- }
- })
- })
- }
})
function promiseWithResolvers() {
diff --git a/test/e2e/app-dir/next-after-app/middleware.js b/test/e2e/app-dir/next-after-app/middleware.js
index 6a1c60397228b..f18a04fb0943f 100644
--- a/test/e2e/app-dir/next-after-app/middleware.js
+++ b/test/e2e/app-dir/next-after-app/middleware.js
@@ -6,7 +6,12 @@ export function middleware(
/** @type {import ('next/server').NextRequest} */ request
) {
const url = new URL(request.url)
- if (url.pathname.startsWith('/middleware/redirect-source')) {
+
+ const match = url.pathname.match(
+ /^(?\/[^/]+?)\/middleware\/redirect-source/
+ )
+ if (match) {
+ const prefix = match.groups.prefix
const requestId = url.searchParams.get('requestId')
after(() => {
cliLog({
@@ -15,10 +20,12 @@ export function middleware(
cookies: { testCookie: cookies().get('testCookie')?.value },
})
})
- return NextResponse.redirect(new URL('/middleware/redirect', request.url))
+ return NextResponse.redirect(
+ new URL(prefix + '/middleware/redirect', request.url)
+ )
}
}
export const config = {
- matcher: '/middleware/:path*',
+ matcher: ['/:prefix/middleware/:path*'],
}
diff --git a/test/e2e/app-dir/next-after-pages/index.test.ts b/test/e2e/app-dir/next-after-pages/index.test.ts
index ac7b5100b089b..960656a036abc 100644
--- a/test/e2e/app-dir/next-after-pages/index.test.ts
+++ b/test/e2e/app-dir/next-after-pages/index.test.ts
@@ -1,23 +1,14 @@
/* eslint-env jest */
import { nextTestSetup, isNextDev } from 'e2e-utils'
import { assertHasRedbox, getRedboxSource, retry } from 'next-test-utils'
-import * as fs from 'fs'
-import * as path from 'path'
-import * as os from 'os'
import * as Log from './utils/log'
// using unstable_after is a compile-time error in build mode.
const _describe = isNextDev ? describe : describe.skip
_describe('unstable_after() - pages', () => {
- const logFileDir = fs.mkdtempSync(path.join(os.tmpdir(), 'logs-'))
- const logFile = path.join(logFileDir, 'logs.jsonl')
-
const { next } = nextTestSetup({
files: __dirname,
- env: {
- PERSISTENT_LOG_FILE: logFile,
- },
})
let currentCliOutputIndex = 0
diff --git a/test/lib/development-sandbox.ts b/test/lib/development-sandbox.ts
index af73eeb79cae5..1462f49a39293 100644
--- a/test/lib/development-sandbox.ts
+++ b/test/lib/development-sandbox.ts
@@ -33,7 +33,7 @@ export function waitForHydration(browser: BrowserInterface): Promise {
export async function sandbox(
next: NextInstance,
- initialFiles?: Map,
+ initialFiles?: Map string)>,
initialUrl: string = '/',
webDriverOptions: any = undefined
) {