From 53201b178fb20d295d2065329533e354f1bb4d86 Mon Sep 17 00:00:00 2001 From: Janka Uryga Date: Fri, 30 Aug 2024 17:01:25 +0200 Subject: [PATCH] [after] duplicate test pages for edge instead of next.patch (#69477) `unstable_after` needs to be tested in both nodejs and edge runtimes. Previously, we set the runtime via `next.patch()`, but this proved error-prone (e.g. because `sandbox()` call would override this patch, and not actually run anything in edge...) This PR makes this more static by splitting the test pages for `unstable_after` into `/nodejs` and `/edge`, each with a corresponding `export const runtime = "..."` in its root layout. The pages in `/edge` mirror the structure of `/nodejs` and reexport components from it (which makes the tests easier to update than simply duplicating everything). note that "use client" directives and route segment configs cannot be reexported and need to be duplicated. --- .../app/invalid-in-client/page.js | 2 +- .../app/invalid-in-dynamic-error}/page.js | 5 +- .../invalid-in-dynamic-force-static/page.js | 11 ++ .../app/layout.js | 10 + .../index.test.ts | 50 +++++ .../next.config.js | 6 + .../next-after-app-invalid-usage/utils/log.js | 16 ++ .../app/edge/[id]/dynamic/page.js | 1 + .../next-after-app/app/edge/[id]/layout.js | 1 + .../app/edge/[id]/setting-cookies/page.js | 1 + .../app/edge/[id]/with-action/page.js | 1 + .../app/edge/[id]/with-metadata/page.js | 4 + .../next-after-app/app/edge/delay/page.js | 3 + .../edge/interrupted/calls-not-found/page.js | 1 + .../edge/interrupted/calls-redirect/page.js | 5 + .../edge/interrupted/redirect-target/page.js | 1 + .../app/edge/interrupted/throws-error/page.js | 1 + .../app-dir/next-after-app/app/edge/layout.js | 3 + .../app/edge/middleware/redirect/page.js | 1 + .../app/edge/nested-after/page.js | 1 + .../app/edge/provided-request-context/page.js | 1 + .../next-after-app/app/edge/route/route.js | 4 + .../app/interrupted/calls-redirect/page.js | 12 -- .../app/{ => nodejs}/[id]/dynamic/page.js | 2 +- .../app/{ => nodejs}/[id]/layout.js | 2 +- .../{ => nodejs}/[id]/setting-cookies/page.js | 0 .../app/{ => nodejs}/[id]/with-action/page.js | 2 +- .../{ => nodejs}/[id]/with-metadata/page.js | 2 +- .../app/{ => nodejs}/delay/page.js | 2 +- .../interrupted/calls-not-found/page.js | 2 +- .../nodejs/interrupted/calls-redirect/page.js | 19 ++ .../interrupted/redirect-target/page.js | 2 +- .../interrupted/throws-error/page.js | 2 +- .../next-after-app/app/{ => nodejs}/layout.js | 3 +- .../{ => nodejs}/middleware/redirect/page.js | 0 .../app/{ => nodejs}/nested-after/page.js | 2 +- .../provided-request-context/page.js | 2 +- .../app/{ => nodejs}/route/route.js | 6 +- test/e2e/app-dir/next-after-app/index.test.ts | 174 +++++------------- test/e2e/app-dir/next-after-app/middleware.js | 13 +- .../app-dir/next-after-pages/index.test.ts | 9 - test/lib/development-sandbox.ts | 2 +- 42 files changed, 213 insertions(+), 174 deletions(-) rename test/{e2e/app-dir/next-after-app => development/app-dir/next-after-app-invalid-usage}/app/invalid-in-client/page.js (94%) rename test/{e2e/app-dir/next-after-app/app/static => development/app-dir/next-after-app-invalid-usage/app/invalid-in-dynamic-error}/page.js (66%) create mode 100644 test/development/app-dir/next-after-app-invalid-usage/app/invalid-in-dynamic-force-static/page.js create mode 100644 test/development/app-dir/next-after-app-invalid-usage/app/layout.js create mode 100644 test/development/app-dir/next-after-app-invalid-usage/index.test.ts create mode 100644 test/development/app-dir/next-after-app-invalid-usage/next.config.js create mode 100644 test/development/app-dir/next-after-app-invalid-usage/utils/log.js create mode 100644 test/e2e/app-dir/next-after-app/app/edge/[id]/dynamic/page.js create mode 100644 test/e2e/app-dir/next-after-app/app/edge/[id]/layout.js create mode 100644 test/e2e/app-dir/next-after-app/app/edge/[id]/setting-cookies/page.js create mode 100644 test/e2e/app-dir/next-after-app/app/edge/[id]/with-action/page.js create mode 100644 test/e2e/app-dir/next-after-app/app/edge/[id]/with-metadata/page.js create mode 100644 test/e2e/app-dir/next-after-app/app/edge/delay/page.js create mode 100644 test/e2e/app-dir/next-after-app/app/edge/interrupted/calls-not-found/page.js create mode 100644 test/e2e/app-dir/next-after-app/app/edge/interrupted/calls-redirect/page.js create mode 100644 test/e2e/app-dir/next-after-app/app/edge/interrupted/redirect-target/page.js create mode 100644 test/e2e/app-dir/next-after-app/app/edge/interrupted/throws-error/page.js create mode 100644 test/e2e/app-dir/next-after-app/app/edge/layout.js create mode 100644 test/e2e/app-dir/next-after-app/app/edge/middleware/redirect/page.js create mode 100644 test/e2e/app-dir/next-after-app/app/edge/nested-after/page.js create mode 100644 test/e2e/app-dir/next-after-app/app/edge/provided-request-context/page.js create mode 100644 test/e2e/app-dir/next-after-app/app/edge/route/route.js delete mode 100644 test/e2e/app-dir/next-after-app/app/interrupted/calls-redirect/page.js rename test/e2e/app-dir/next-after-app/app/{ => nodejs}/[id]/dynamic/page.js (93%) rename test/e2e/app-dir/next-after-app/app/{ => nodejs}/[id]/layout.js (81%) rename test/e2e/app-dir/next-after-app/app/{ => nodejs}/[id]/setting-cookies/page.js (100%) rename test/e2e/app-dir/next-after-app/app/{ => nodejs}/[id]/with-action/page.js (95%) rename test/e2e/app-dir/next-after-app/app/{ => nodejs}/[id]/with-metadata/page.js (87%) rename test/e2e/app-dir/next-after-app/app/{ => nodejs}/delay/page.js (94%) rename test/e2e/app-dir/next-after-app/app/{ => nodejs}/interrupted/calls-not-found/page.js (83%) create mode 100644 test/e2e/app-dir/next-after-app/app/nodejs/interrupted/calls-redirect/page.js rename test/e2e/app-dir/next-after-app/app/{ => nodejs}/interrupted/redirect-target/page.js (81%) rename test/e2e/app-dir/next-after-app/app/{ => nodejs}/interrupted/throws-error/page.js (81%) rename test/e2e/app-dir/next-after-app/app/{ => nodejs}/layout.js (74%) rename test/e2e/app-dir/next-after-app/app/{ => nodejs}/middleware/redirect/page.js (100%) rename test/e2e/app-dir/next-after-app/app/{ => nodejs}/nested-after/page.js (97%) rename test/e2e/app-dir/next-after-app/app/{ => nodejs}/provided-request-context/page.js (83%) rename test/e2e/app-dir/next-after-app/app/{ => nodejs}/route/route.js (72%) 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 ) {