From e29a96a9eea4821af9fecd2edbd73e45e513e80d Mon Sep 17 00:00:00 2001 From: Joseph Date: Tue, 15 Sep 2026 10:41:17 +0200 Subject: [PATCH 01/14] docs: add an intro paragraph to the glossary (#98654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The glossary page goes straight from frontmatter to the first entry, `# A` β€”Β  the word "glossary" appears nowhere in its body. We index docs for search from the rendered content, and the page title is stored as non-searchable metadata, so searching "glossary" on nextjs.org does not return the glossary. This adds a two-sentence intro, which gives the page a body chunk carrying its own name. ```mdx The Next.js documentation uses a recurring set of terms for routing, rendering, and caching. This glossary defines them. ``` Also reads better than a page that opens directly to content. --------- Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: Aurora Scharff <66901228+aurorascharff@users.noreply.github.com> --- docs/01-app/04-glossary.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/01-app/04-glossary.mdx b/docs/01-app/04-glossary.mdx index a11297f10470..a7be2ebf104c 100644 --- a/docs/01-app/04-glossary.mdx +++ b/docs/01-app/04-glossary.mdx @@ -4,6 +4,8 @@ nav_title: Glossary description: A glossary of common terms used in Next.js. --- +This glossary defines common terms used throughout the Next.js documentation, including terms related to routing, rendering, and caching. + # A ## App Router From 05c20cd1fb273bba06be0b5350598bc737481659 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Tue, 15 Sep 2026 11:59:08 +0200 Subject: [PATCH 02/14] test: remove unsupported deployment ID builder cases (#98668) ### What? Removes the legacy Vercel builder rows from the immutable-assets deployment ID determinism test. Direct `.next` output and Vercel adapter output remain covered, including both the standard and Cache Components fixtures. ### Why? Current `vercel@latest` rejects `_next/static/immutable` output when the Next.js adapter is disabled. Disabling immutable assets is not a valid replacement for these cases: it adds the deployment ID to asset URLs, which intentionally changes content-hashed CSS filenames between builds. This intentionally removes the two legacy-builder deploy-mode cases rather than skipping or weakening their determinism assertions. The current legacy builder no longer supports the immutable-output contract the test verifies. ### How? The Build Output API matrix now exercises only adapter mode, and `NEXT_ENABLE_ADAPTER=1` is applied unconditionally. The shared two-build comparison and all assertions for the retained direct and adapter cases are unchanged. Builder-only mode branching and timeout handling are removed. ### Verification - `pnpm prettier --with-node-modules --ignore-path .prettierignore --check test/production/deterministic-build/deployment-id.test.ts` - `pnpm eslint --config eslint.config.mjs test/production/deterministic-build/deployment-id.test.ts` - `pnpm test-start-turbo test/production/deterministic-build/deployment-id.test.ts` (3 tests passed) Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> --- .../deterministic-build/deployment-id.test.ts | 60 ++++++++----------- 1 file changed, 24 insertions(+), 36 deletions(-) diff --git a/test/production/deterministic-build/deployment-id.test.ts b/test/production/deterministic-build/deployment-id.test.ts index 5d1f80ef9ed1..4577ae43d6a8 100644 --- a/test/production/deterministic-build/deployment-id.test.ts +++ b/test/production/deterministic-build/deployment-id.test.ts @@ -222,11 +222,9 @@ async function runTest( }) describe.each([ - { test: 'standard', mode: 'builder' } as const, - { test: 'standard', mode: 'adapter' } as const, - { test: 'cacheComponents', mode: 'builder' } as const, - { test: 'cacheComponents', mode: 'adapter' } as const, - ])('build output API - $test $mode', ({ test, mode }) => { + { test: 'standard' } as const, + { test: 'cacheComponents' } as const, + ])('build output API - $test adapter', ({ test }) => { const { next } = nextTestSetup({ files: { // A mock file to be able to run `vercel build` without logging in @@ -243,43 +241,33 @@ async function runTest( // We use NEXT_TEST_PREFER_OFFLINE, so just declaring `vercel: latest` as a dependency still // doesn't force the latest version. buildCommand: 'pnpm dlx vercel@latest build', - env: - mode === 'adapter' - ? { - NEXT_ENABLE_ADAPTER: '1', - } - : undefined, + env: { + NEXT_ENABLE_ADAPTER: '1', + }, skipStart: true, disableAutoSkewProtection: true, }) - it( - 'should produce identical build outputs even when changing deployment id', - async () => { - let { run1, run2 } = await runTest(next, readFilesBuilder) + it('should produce identical build outputs even when changing deployment id', async () => { + let { run1, run2 } = await runTest(next, readFilesBuilder) - expect(run1.size).toBeGreaterThan(0) - expect([...run1.keys()]).toEqual([...run2.keys()]) + expect(run1.size).toBeGreaterThan(0) + expect([...run1.keys()]).toEqual([...run2.keys()]) - if (test === 'standard') { - expect([...run1.keys()]).toIncludeAllMembers([ - '.vercel/output/functions/app-page.func/.vc-config.json', - '.vercel/output/functions/app-page.rsc.func/.vc-config.json', - '.vercel/output/functions/app-route.func/.vc-config.json', - '.vercel/output/functions/app-route.rsc.func/.vc-config.json', - '.vercel/output/functions/pages-dynamic.func/.vc-config.json', - '.vercel/output/functions/pages-static-gsp.func/.vc-config.json', - ]) - expect([...run1.keys()]).toSatisfyAny((k) => - k.includes('middleware.func') - ) - } - }, - // The builder mode can take a bit longer, so we increase the timeout - // for these tests. The adapter mode should be faster, so we leave it as - // the default. - mode === 'builder' ? 120_000 : undefined - ) + if (test === 'standard') { + expect([...run1.keys()]).toIncludeAllMembers([ + '.vercel/output/functions/app-page.func/.vc-config.json', + '.vercel/output/functions/app-page.rsc.func/.vc-config.json', + '.vercel/output/functions/app-route.func/.vc-config.json', + '.vercel/output/functions/app-route.rsc.func/.vc-config.json', + '.vercel/output/functions/pages-dynamic.func/.vc-config.json', + '.vercel/output/functions/pages-static-gsp.func/.vc-config.json', + ]) + expect([...run1.keys()]).toSatisfyAny((k) => + k.includes('middleware.func') + ) + } + }) }) } ) From 5979942c73748de5fc5f43c3cbed740c2bed2b2f Mon Sep 17 00:00:00 2001 From: Benjamin Staneck Date: Tue, 15 Sep 2026 12:52:19 +0200 Subject: [PATCH 03/14] Pages Router: key the client router filter prefetch marker by the `as` path (#98182) ### What? `Router.prefetch()` in the Pages Router evaluates the client router filter (`_bfl`) against the `as` path, but stored the resulting `{ __appRouter: true }` marker in `router.components` under the `href` pathname. `Router.change()` looked the marker up with the `href` pathname as well. This PR keys the marker by the normalized `as` pathname and effective locale, preserves existing cache entries, and checks for markers before and after route resolution. ### Why? `href` and `as` are the same URL for most links, so the mismatch was invisible. They differ for the "route as modal" pattern (`examples/with-route-as-modal`): `href` stays on the current route (`router.pathname` + query) and `as` shows the pretty URL. When the filter matches `as`, including a false positive, the marker could replace the cached route info of the page the user is currently on: - On a static route (`/`), the `change()` guard fired for later shallow navigation and hard-navigated. - On a dynamic route, the marker was stored under the pattern (`/players/[name]`) but looked up with the concrete path (`/players/alice`). The guard could miss it, and shallow navigation could render without page props, causing missing content or an application error. A prefetch must also preserve already-loaded route info when its key matches the current route. This can happen when the page was reached through a rewrite and its canonical URL is prefetched. Hash-only navigation renders that cached entry directly, so preserving it lets the normal client-side hash update retain the page props and update `router.asPath`. Fixes #98180 ### How? - `getAppRouterMarkerKey(router, as, locale)` parses the pathname, normalizes the trailing slash, and preserves the effective locale. It expects a path without `basePath` and does not strip that prefix again. An explicit locale prefix takes precedence over the supplied locale. - The helper returns `null` for non-local URLs, leaving their existing navigation handling unchanged. - `prefetch()` stores the marker under that key only when the corresponding cache entry is absent. It does not replace loaded route info. - `change()` checks both the normalized `as` key and the `href` route. It checks the resolved route again after config rewrites, before `getRouteInfo()` reads the cache. - Non-shallow navigations continue to consult the client router filter directly. Hash-only navigation retains its normal client-side path. The complementary change in #98650 (adopts #98187) adds defensive checks when `getRouteInfo()` reads marker entries. ### Tests The e2e suites run in production (`next start`) and deploy modes. `router.prefetch()` is a no-op in development. `test/e2e/app-dir/pages-prefetch-as-app-route` covers: - Hard navigation to an App Router destination after prefetching a link whose `href` and `as` differ. - Shallow navigation on static and dynamic Pages Router routes without a reload or lost server-provided props. - Hard navigation when the `href` route holds a marker and `as` differs, or a config rewrite resolves to a marked route. - Client-side hash navigation after an awaited prefetch of the current route's canonical URL, with preserved props and an updated `router.asPath`. `test/e2e/app-dir/pages-prefetch-as-app-route-base-path` covers an internal `/docs` route with `basePath: '/docs'`, including navigation to `/docs/docs` and an unaffected shallow update on the index page. `test/e2e/app-dir/pages-prefetch-as-app-route-i18n` covers a French-only redirect prefetch without forcing an unrelated English navigation to reload. Marker-dependent tests wait for the specific key they need, rather than any marker. The fix was also verified at runtime against the reproduction in https://github.com/Stanzilla/next-pages-router-prefetch-bloom-filter-repro by patching the compiled `router.js` and re-running the scenario. The e2e suite was not run locally. Disclosure: this change and its description were prepared with AI assistance (Claude Code) on behalf of the author. --- packages/next/src/shared/lib/router/router.ts | 99 ++++++++++++- .../app/docs/page.js | 3 + .../app/layout.js | 7 + .../next.config.js | 6 + ...es-prefetch-as-app-route-base-path.test.ts | 53 +++++++ .../pages/index.js | 23 +++ .../app/dashboard/page.js | 3 + .../app/layout.js | 7 + .../next.config.js | 23 +++ .../pages-prefetch-as-app-route-i18n.test.ts | 34 +++++ .../pages/index.js | 19 +++ .../pages/legacy.js | 3 + .../app/dashboard/page.js | 3 + .../pages-prefetch-as-app-route/app/layout.js | 7 + .../app/modal/[id]/page.js | 3 + .../next.config.js | 13 ++ .../pages-prefetch-as-app-route.test.ts | 138 ++++++++++++++++++ .../pages/blog/[slug].js | 37 +++++ .../pages/index.js | 50 +++++++ .../pages/modal.js | 39 +++++ 20 files changed, 564 insertions(+), 6 deletions(-) create mode 100644 test/e2e/app-dir/pages-prefetch-as-app-route-base-path/app/docs/page.js create mode 100644 test/e2e/app-dir/pages-prefetch-as-app-route-base-path/app/layout.js create mode 100644 test/e2e/app-dir/pages-prefetch-as-app-route-base-path/next.config.js create mode 100644 test/e2e/app-dir/pages-prefetch-as-app-route-base-path/pages-prefetch-as-app-route-base-path.test.ts create mode 100644 test/e2e/app-dir/pages-prefetch-as-app-route-base-path/pages/index.js create mode 100644 test/e2e/app-dir/pages-prefetch-as-app-route-i18n/app/dashboard/page.js create mode 100644 test/e2e/app-dir/pages-prefetch-as-app-route-i18n/app/layout.js create mode 100644 test/e2e/app-dir/pages-prefetch-as-app-route-i18n/next.config.js create mode 100644 test/e2e/app-dir/pages-prefetch-as-app-route-i18n/pages-prefetch-as-app-route-i18n.test.ts create mode 100644 test/e2e/app-dir/pages-prefetch-as-app-route-i18n/pages/index.js create mode 100644 test/e2e/app-dir/pages-prefetch-as-app-route-i18n/pages/legacy.js create mode 100644 test/e2e/app-dir/pages-prefetch-as-app-route/app/dashboard/page.js create mode 100644 test/e2e/app-dir/pages-prefetch-as-app-route/app/layout.js create mode 100644 test/e2e/app-dir/pages-prefetch-as-app-route/app/modal/[id]/page.js create mode 100644 test/e2e/app-dir/pages-prefetch-as-app-route/next.config.js create mode 100644 test/e2e/app-dir/pages-prefetch-as-app-route/pages-prefetch-as-app-route.test.ts create mode 100644 test/e2e/app-dir/pages-prefetch-as-app-route/pages/blog/[slug].js create mode 100644 test/e2e/app-dir/pages-prefetch-as-app-route/pages/index.js create mode 100644 test/e2e/app-dir/pages-prefetch-as-app-route/pages/modal.js diff --git a/packages/next/src/shared/lib/router/router.ts b/packages/next/src/shared/lib/router/router.ts index 75c19d1b160b..36ef19d610bb 100644 --- a/packages/next/src/shared/lib/router/router.ts +++ b/packages/next/src/shared/lib/router/router.ts @@ -152,6 +152,55 @@ function prepareUrlAs(router: NextRouter, url: Url, as?: Url) { } } +/** + * Key under which `prefetch()` stores the `{ __appRouter: true }` marker in + * `router.components` and under which `change()` looks it up again. + * + * The client router filter (`_bfl`) is evaluated against the `as` path, so the + * marker has to be keyed by the `as` path as well. Keying it by the `href` + * pathname is only equivalent while `href` and `as` are the same URL. When + * they differ (for example the "route as modal" pattern where `href` stays on + * the current page and `as` shows a pretty URL) a filter match on `as` would + * otherwise mark the pages route behind `href` as an App Router path and + * break every later navigation to that route. + * + * `as` is expected without the `basePath`, which is what both call sites + * hold: `prefetch()` receives it that way from `next/link` and `change()` + * strips it before computing `cleanedAs`. Stripping it again here would turn + * a route that merely starts with the `basePath` (for example the app route + * `/docs` with `basePath: '/docs'`) into `/` and poison the index page. + * + * The key also carries the effective locale, mirroring how `_bfl` evaluates + * `addLocale(as, locale)` against the filter. A match for a French-only + * redirect at `/fr/legacy` must not mark the English `/legacy` page. + * + * Returns `null` when `as` is not a local URL (for example `mailto:` or a + * different origin). Such an `as` can never be an App Router path, and + * `change()` reports it as an invalid `href`/`as` pair further down. + */ +function getAppRouterMarkerKey( + router: Router, + as: string, + locale: string | false | undefined +): string | null { + if (!isLocalURL(as)) { + return null + } + + let { pathname } = parseRelativeUrl(as) + let effectiveLocale = locale || router.locale + + if (process.env.__NEXT_I18N_SUPPORT) { + const localePathResult = normalizeLocalePath(pathname, router.locales) + pathname = localePathResult.pathname + effectiveLocale = localePathResult.detectedLocale || effectiveLocale + } + + return removeTrailingSlash( + addLocale(removeTrailingSlash(pathname), effectiveLocale) + ) +} + function resolveDynamicRoute(pathname: string, pages: string[]) { const cleanPathname = removeTrailingSlash(denormalizePagePath(pathname)) if (cleanPathname === '/404' || cleanPathname === '/_error') { @@ -1378,7 +1427,6 @@ export default class Router implements BaseRouter { // If the url change is only related to a hash change // We should not proceed. We should only change the state. - if (!isQueryUpdating && this.onlyAHashChange(cleanedAs) && !localeChange) { nextState.asPath = cleanedAs Router.events.emit('hashChangeStart', as, routeProps) @@ -1446,9 +1494,21 @@ export default class Router implements BaseRouter { let route = removeTrailingSlash(pathname) const parsedAsPathname = as.startsWith('/') && parseRelativeUrl(as).pathname - // if we detected the path as app route during prefetching - // trigger hard navigation - if ((this.components[pathname] as any)?.__appRouter) { + // if we detected the `as` path as app route during prefetching + // trigger hard navigation. The marker is keyed by the `as` path, but a + // marker can also sit under the href route when both paths share a + // pages route (`pages/modal.js` next to `app/modal/[id]/page.js`). + // Check both so `getRouteInfo()` never reads a marker as component data. + const appRouterMarkerKey = getAppRouterMarkerKey( + this, + cleanedAs, + nextState.locale + ) + if ( + (appRouterMarkerKey !== null && + (this.components[appRouterMarkerKey] as any)?.__appRouter) || + (this.components[route] as any)?.__appRouter + ) { handleHardNavigation({ url: as, router: this }) return new Promise(() => {}) } @@ -1563,6 +1623,15 @@ export default class Router implements BaseRouter { resolvedAs = removeLocale(removeBasePath(resolvedAs), nextState.locale) route = removeTrailingSlash(pathname) + + // A config rewrite can resolve `href` to another pages route after the + // marker guard above ran. That route can hold a marker from an earlier + // prefetch, so check it again before `getRouteInfo()` reads the cache. + if ((this.components[route] as any)?.__appRouter) { + handleHardNavigation({ url: as, router: this }) + return new Promise(() => {}) + } + let routeMatch: Params | false = false if (isDynamicRoute(route)) { @@ -2416,7 +2485,6 @@ export default class Router implements BaseRouter { return } let parsed = parseRelativeUrl(url) - const urlPathname = parsed.pathname let { pathname, query } = parsed const originalPathname = pathname @@ -2554,7 +2622,26 @@ export default class Router implements BaseRouter { const route = removeTrailingSlash(pathname) if (await this._bfl(asPath, resolvedAs, options.locale, true)) { - this.components[urlPathname] = { __appRouter: true } as any + const appRouterMarkerKey = getAppRouterMarkerKey( + this, + asPath, + options.locale + ) + // A loaded pages route in the cache is ground truth: the page behind + // this key exists and was rendered. This is the case when the current + // page was reached through a rewrite and the canonical URL is + // prefetched, or when a prefetch is still in flight while a navigation + // stores the same route. Replacing that entry with the marker would + // break hash-only changes and shallow navigations, which render the + // cached entry of the current route directly. Non-shallow navigations + // consult the client router filter themselves, so they do not depend + // on the marker to hard navigate. + if ( + appRouterMarkerKey !== null && + this.components[appRouterMarkerKey] === undefined + ) { + this.components[appRouterMarkerKey] = { __appRouter: true } as any + } } await Promise.all([ diff --git a/test/e2e/app-dir/pages-prefetch-as-app-route-base-path/app/docs/page.js b/test/e2e/app-dir/pages-prefetch-as-app-route-base-path/app/docs/page.js new file mode 100644 index 000000000000..9eab4c03c5d4 --- /dev/null +++ b/test/e2e/app-dir/pages-prefetch-as-app-route-base-path/app/docs/page.js @@ -0,0 +1,3 @@ +export default function Page() { + return

hello from app/docs/page

+} diff --git a/test/e2e/app-dir/pages-prefetch-as-app-route-base-path/app/layout.js b/test/e2e/app-dir/pages-prefetch-as-app-route-base-path/app/layout.js new file mode 100644 index 000000000000..a3a86a5ca1e1 --- /dev/null +++ b/test/e2e/app-dir/pages-prefetch-as-app-route-base-path/app/layout.js @@ -0,0 +1,7 @@ +export default function Root({ children }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/pages-prefetch-as-app-route-base-path/next.config.js b/test/e2e/app-dir/pages-prefetch-as-app-route-base-path/next.config.js new file mode 100644 index 000000000000..c5cb6537a120 --- /dev/null +++ b/test/e2e/app-dir/pages-prefetch-as-app-route-base-path/next.config.js @@ -0,0 +1,6 @@ +/** + * @type import('next').NextConfig + */ +module.exports = { + basePath: '/docs', +} diff --git a/test/e2e/app-dir/pages-prefetch-as-app-route-base-path/pages-prefetch-as-app-route-base-path.test.ts b/test/e2e/app-dir/pages-prefetch-as-app-route-base-path/pages-prefetch-as-app-route-base-path.test.ts new file mode 100644 index 000000000000..fbd74dec8089 --- /dev/null +++ b/test/e2e/app-dir/pages-prefetch-as-app-route-base-path/pages-prefetch-as-app-route-base-path.test.ts @@ -0,0 +1,53 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('pages router - prefetch of an app route that starts with the basePath', () => { + const { next, isNextDev } = nextTestSetup({ + files: __dirname, + }) + + // `router.prefetch()` is a no-op in development, so the client router filter + // marker is only ever written by production builds. + ;(isNextDev ? describe.skip : describe)('production mode', () => { + async function hoverAppLink( + browser: Awaited> + ) { + await browser.eval('window.beforeNav = 1') + await browser.elementById('app-link').moveTo() + + await retry(async () => { + expect( + await browser.eval( + "window.next.router.components['/docs']?.__appRouter" + ) + ).toBe(true) + }) + } + + it('should hard navigate to the app route when the link is clicked', async () => { + const browser = await next.browser('/docs') + await hoverAppLink(browser) + + await browser.elementById('app-link').click() + await browser.waitForElementByCss('#app-page') + + expect(await browser.eval('window.beforeNav')).toBeUndefined() + expect(await browser.eval('location.pathname')).toBe('/docs/docs') + }) + + it('should keep shallow navigation working on the index page after prefetching the link', async () => { + const browser = await next.browser('/docs') + await hoverAppLink(browser) + + await browser.elementById('tab-b').click() + await retry(async () => { + expect(await browser.elementById('tab').text()).toBe('b') + }) + + // a hard navigation would have reset the flag + expect(await browser.eval('window.beforeNav')).toBe(1) + expect(await browser.eval('location.pathname')).toBe('/docs') + expect(await browser.eval('location.search')).toBe('?tab=b') + }) + }) +}) diff --git a/test/e2e/app-dir/pages-prefetch-as-app-route-base-path/pages/index.js b/test/e2e/app-dir/pages-prefetch-as-app-route-base-path/pages/index.js new file mode 100644 index 000000000000..83021237bafe --- /dev/null +++ b/test/e2e/app-dir/pages-prefetch-as-app-route-base-path/pages/index.js @@ -0,0 +1,23 @@ +import Link from 'next/link' +import { useRouter } from 'next/router' + +export default function Page() { + const router = useRouter() + return ( + <> +

hello from pages/index

+

{router.query.tab || 'a'}

+ + tab b + + {/* + The app route `/docs` starts with the `basePath` `/docs`, so it is + served at `/docs/docs`. `next/link` hands the router the path without + the `basePath`, so stripping it again would turn `/docs` into `/`. + */} + + to app route + + + ) +} diff --git a/test/e2e/app-dir/pages-prefetch-as-app-route-i18n/app/dashboard/page.js b/test/e2e/app-dir/pages-prefetch-as-app-route-i18n/app/dashboard/page.js new file mode 100644 index 000000000000..91a4d3e78a17 --- /dev/null +++ b/test/e2e/app-dir/pages-prefetch-as-app-route-i18n/app/dashboard/page.js @@ -0,0 +1,3 @@ +export default function Page() { + return

hello from app/dashboard/page

+} diff --git a/test/e2e/app-dir/pages-prefetch-as-app-route-i18n/app/layout.js b/test/e2e/app-dir/pages-prefetch-as-app-route-i18n/app/layout.js new file mode 100644 index 000000000000..a3a86a5ca1e1 --- /dev/null +++ b/test/e2e/app-dir/pages-prefetch-as-app-route-i18n/app/layout.js @@ -0,0 +1,7 @@ +export default function Root({ children }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/pages-prefetch-as-app-route-i18n/next.config.js b/test/e2e/app-dir/pages-prefetch-as-app-route-i18n/next.config.js new file mode 100644 index 000000000000..ad2146546431 --- /dev/null +++ b/test/e2e/app-dir/pages-prefetch-as-app-route-i18n/next.config.js @@ -0,0 +1,23 @@ +/** + * @type import('next').NextConfig + */ +module.exports = { + i18n: { + locales: ['en', 'fr'], + defaultLocale: 'en', + }, + experimental: { + clientRouterFilterRedirects: true, + }, + async redirects() { + return [ + { + // French-only redirect: the English `/legacy` stays a pages route + source: '/fr/legacy', + destination: '/fr', + permanent: false, + locale: false, + }, + ] + }, +} diff --git a/test/e2e/app-dir/pages-prefetch-as-app-route-i18n/pages-prefetch-as-app-route-i18n.test.ts b/test/e2e/app-dir/pages-prefetch-as-app-route-i18n/pages-prefetch-as-app-route-i18n.test.ts new file mode 100644 index 000000000000..f3e107834b18 --- /dev/null +++ b/test/e2e/app-dir/pages-prefetch-as-app-route-i18n/pages-prefetch-as-app-route-i18n.test.ts @@ -0,0 +1,34 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('pages router - prefetch marker keeps the effective locale', () => { + const { next, isNextDev } = nextTestSetup({ + files: __dirname, + }) + + // `router.prefetch()` is a no-op in development, so the client router filter + // marker is only ever written by production builds. + ;(isNextDev ? describe.skip : describe)('production mode', () => { + it('should keep the English navigation client-side after prefetching the French-only redirect', async () => { + const browser = await next.browser('/') + await browser.eval('window.beforeNav = 1') + await browser.elementById('fr-legacy-link').moveTo() + + // wait until the prefetch has matched `/fr/legacy` and stored its marker + await retry(async () => { + expect( + await browser.eval( + "window.next.router.components['/fr/legacy']?.__appRouter" + ) + ).toBe(true) + }) + + await browser.elementById('en-legacy-link').click() + await browser.waitForElementByCss('#legacy-page') + + // a hard navigation would have reset the flag + expect(await browser.eval('window.beforeNav')).toBe(1) + expect(await browser.eval('location.pathname')).toBe('/legacy') + }) + }) +}) diff --git a/test/e2e/app-dir/pages-prefetch-as-app-route-i18n/pages/index.js b/test/e2e/app-dir/pages-prefetch-as-app-route-i18n/pages/index.js new file mode 100644 index 000000000000..a50f56b89212 --- /dev/null +++ b/test/e2e/app-dir/pages-prefetch-as-app-route-i18n/pages/index.js @@ -0,0 +1,19 @@ +import Link from 'next/link' + +export default function Page() { + return ( + <> +

hello from pages/index

+ {/* + Prefetching this link evaluates the client router filter against + `/fr/legacy`, which matches the French-only redirect. + */} + + legacy (fr) + + + legacy (en) + + + ) +} diff --git a/test/e2e/app-dir/pages-prefetch-as-app-route-i18n/pages/legacy.js b/test/e2e/app-dir/pages-prefetch-as-app-route-i18n/pages/legacy.js new file mode 100644 index 000000000000..a57d7b0c98ea --- /dev/null +++ b/test/e2e/app-dir/pages-prefetch-as-app-route-i18n/pages/legacy.js @@ -0,0 +1,3 @@ +export default function Page() { + return

hello from pages/legacy

+} diff --git a/test/e2e/app-dir/pages-prefetch-as-app-route/app/dashboard/page.js b/test/e2e/app-dir/pages-prefetch-as-app-route/app/dashboard/page.js new file mode 100644 index 000000000000..91a4d3e78a17 --- /dev/null +++ b/test/e2e/app-dir/pages-prefetch-as-app-route/app/dashboard/page.js @@ -0,0 +1,3 @@ +export default function Page() { + return

hello from app/dashboard/page

+} diff --git a/test/e2e/app-dir/pages-prefetch-as-app-route/app/layout.js b/test/e2e/app-dir/pages-prefetch-as-app-route/app/layout.js new file mode 100644 index 000000000000..a3a86a5ca1e1 --- /dev/null +++ b/test/e2e/app-dir/pages-prefetch-as-app-route/app/layout.js @@ -0,0 +1,7 @@ +export default function Root({ children }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/pages-prefetch-as-app-route/app/modal/[id]/page.js b/test/e2e/app-dir/pages-prefetch-as-app-route/app/modal/[id]/page.js new file mode 100644 index 000000000000..50d237460fdc --- /dev/null +++ b/test/e2e/app-dir/pages-prefetch-as-app-route/app/modal/[id]/page.js @@ -0,0 +1,3 @@ +export default function Page() { + return

hello from app/modal/[id]/page

+} diff --git a/test/e2e/app-dir/pages-prefetch-as-app-route/next.config.js b/test/e2e/app-dir/pages-prefetch-as-app-route/next.config.js new file mode 100644 index 000000000000..347f5190dc93 --- /dev/null +++ b/test/e2e/app-dir/pages-prefetch-as-app-route/next.config.js @@ -0,0 +1,13 @@ +/** + * @type import('next').NextConfig + */ +module.exports = { + async rewrites() { + return [ + { + source: '/pretty', + destination: '/modal', + }, + ] + }, +} diff --git a/test/e2e/app-dir/pages-prefetch-as-app-route/pages-prefetch-as-app-route.test.ts b/test/e2e/app-dir/pages-prefetch-as-app-route/pages-prefetch-as-app-route.test.ts new file mode 100644 index 000000000000..adfd0645dd8a --- /dev/null +++ b/test/e2e/app-dir/pages-prefetch-as-app-route/pages-prefetch-as-app-route.test.ts @@ -0,0 +1,138 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('pages router - prefetch with `as` pointing at an app route', () => { + const { next, isNextDev } = nextTestSetup({ + files: __dirname, + }) + + // `router.prefetch()` is a no-op in development, so the client router filter + // marker is only ever written by production builds. + ;(isNextDev ? describe.skip : describe)('production mode', () => { + async function hoverLink( + browser: Awaited>, + id: string, + markerKey: string + ) { + await browser.eval('window.beforeNav = 1') + await browser.elementById(id).moveTo() + + // wait until the prefetch has consulted the client router filter and + // stored the marker this test depends on + await retry(async () => { + expect( + await browser.eval( + `window.next.router.components[${JSON.stringify(markerKey)}]?.__appRouter` + ) + ).toBe(true) + }) + } + + it('should hard navigate to the app route when the link is clicked', async () => { + const browser = await next.browser('/') + await hoverLink(browser, 'app-link', '/dashboard') + + await browser.elementById('app-link').click() + await browser.waitForElementByCss('#app-page') + + expect(await browser.eval('window.beforeNav')).toBeUndefined() + expect(await browser.eval('location.pathname')).toBe('/dashboard') + }) + + it('should keep shallow navigation working on the static pages route after prefetching the link', async () => { + const browser = await next.browser('/') + await hoverLink(browser, 'app-link', '/dashboard') + + await browser.elementById('tab-b').click() + await retry(async () => { + expect(await browser.elementById('tab').text()).toBe('b') + }) + + // a hard navigation would have reset the flag + expect(await browser.eval('window.beforeNav')).toBe(1) + expect(await browser.eval('location.search')).toBe('?tab=b') + }) + + it('should keep shallow navigation working on the dynamic pages route after prefetching the link', async () => { + const browser = await next.browser('/blog/first') + await hoverLink(browser, 'app-link', '/dashboard') + + await browser.elementById('tab-b').click() + await retry(async () => { + expect(await browser.elementById('tab').text()).toBe('b') + }) + + expect(await browser.eval('window.beforeNav')).toBe(1) + expect(await browser.elementById('pages-page').text()).toBe( + 'hello from pages/blog/[slug]' + ) + // the props from getServerSideProps must survive the shallow navigation + expect(await browser.elementById('slug').text()).toBe('first') + expect(await browser.elementById('tab-b').getAttribute('href')).toBe( + '/blog/first?tab=b' + ) + }) + + it('should hard navigate when the href route was flagged by the prefetch and `as` differs', async () => { + const browser = await next.browser('/') + // `/modal` is a pages route and the static prefix of `app/modal/[id]` + await hoverLink(browser, 'modal-link', '/modal') + + // href is the flagged `/modal`, `as` is `/pretty` which the filter does + // not know. This must fall back to a hard navigation instead of reading + // the marker as component data. + await browser.elementById('push-pretty').click() + await browser.waitForElementByCss('#modal-page') + + expect(await browser.eval('window.beforeNav')).toBeUndefined() + expect(await browser.eval('location.pathname')).toBe('/pretty') + }) + + it('should hard navigate when a config rewrite resolves to the flagged route', async () => { + const browser = await next.browser('/') + await hoverLink(browser, 'modal-link', '/modal') + + // `/pretty` holds no marker, but the config rewrite resolves it to + // `/modal` after the first marker guard ran. + await browser.elementById('push-pretty-rewrite').click() + await browser.waitForElementByCss('#modal-page') + + expect(await browser.eval('window.beforeNav')).toBeUndefined() + expect(await browser.eval('location.pathname')).toBe('/pretty') + expect(await browser.elementById('modal-page').text()).toBe( + 'hello from pages/modal' + ) + }) + + it('should keep the hash-only change client side when the prefetch flagged the current route', async () => { + // loaded through the config rewrite: the route is `/modal`, the URL is + // `/pretty`, so the filter does not skip `/modal` as the current path + const browser = await next.browser('/pretty') + await browser.eval('window.beforeNav = 1') + + // the filter matches `/modal`, but the cache entry of the current + // route must survive the prefetch + await browser.elementById('prefetch-canonical').click() + await retry(async () => { + expect(await browser.elementById('prefetch-state').text()).toBe('done') + }) + + // a hash-only change does not call `getRouteInfo()`; it renders the + // cache entry of the current route directly and stays client side + await browser.elementById('hash-link').click() + await retry(async () => { + expect(await browser.eval('location.hash')).toBe('#section') + expect(await browser.eval('window.next.router.asPath')).toBe( + '/pretty#section' + ) + }) + + expect(await browser.eval('window.beforeNav')).toBe(1) + // the props from getServerSideProps must still be rendered + expect(await browser.elementById('modal-page').text()).toBe( + 'hello from pages/modal' + ) + expect(await browser.eval('location.pathname')).toBe('/pretty') + }) + }) +}) diff --git a/test/e2e/app-dir/pages-prefetch-as-app-route/pages/blog/[slug].js b/test/e2e/app-dir/pages-prefetch-as-app-route/pages/blog/[slug].js new file mode 100644 index 000000000000..a5d2842dabc4 --- /dev/null +++ b/test/e2e/app-dir/pages-prefetch-as-app-route/pages/blog/[slug].js @@ -0,0 +1,37 @@ +import Link from 'next/link' +import { useRouter } from 'next/router' + +export function getServerSideProps({ params }) { + return { props: { slug: params.slug } } +} + +export default function Page({ slug }) { + const router = useRouter() + return ( + <> +

hello from pages/blog/[slug]

+ {/* server-provided prop: it must survive a shallow navigation */} +

{slug}

+ {/* read from the router: a shallow navigation does not re-run getServerSideProps */} +

{router.query.tab || 'a'}

+ + tab b + + {/* + "route as modal": href is the current dynamic route pattern with an + extra query param, `as` is an app route. + */} + + to app route + + + ) +} diff --git a/test/e2e/app-dir/pages-prefetch-as-app-route/pages/index.js b/test/e2e/app-dir/pages-prefetch-as-app-route/pages/index.js new file mode 100644 index 000000000000..3619f20704db --- /dev/null +++ b/test/e2e/app-dir/pages-prefetch-as-app-route/pages/index.js @@ -0,0 +1,50 @@ +import Link from 'next/link' +import { useRouter } from 'next/router' + +export default function Page() { + const router = useRouter() + return ( + <> +

hello from pages/index

+

{router.query.tab || 'a'}

+ + tab b + + {/* + href stays on the current pages route, `as` is an app route. + Hovering it prefetches `href`/`as` and runs the client router filter + against `as`. + */} + + to app route + + {/* + `/modal` is a pages route and also the static prefix of the app route + `app/modal/[id]`, so the dynamic client router filter flags it. + */} + + to modal + + + + + ) +} diff --git a/test/e2e/app-dir/pages-prefetch-as-app-route/pages/modal.js b/test/e2e/app-dir/pages-prefetch-as-app-route/pages/modal.js new file mode 100644 index 000000000000..e2a5ccee151a --- /dev/null +++ b/test/e2e/app-dir/pages-prefetch-as-app-route/pages/modal.js @@ -0,0 +1,39 @@ +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useState } from 'react' + +export function getServerSideProps() { + return { props: { title: 'hello from pages/modal' } } +} + +// Served at `/modal` and, through the config rewrite, at `/pretty`. +export default function Page({ title }) { + const router = useRouter() + const [prefetchState, setPrefetchState] = useState('idle') + + return ( + <> + {/* server-provided prop: it is lost when a marker is rendered */} + + {/* + When this page is loaded at `/pretty`, prefetching the canonical URL + evaluates the client router filter against `/modal`, the route of the + current page. The button reports when the prefetch has finished. + */} + +

{prefetchState}

+ + to section + +

section

+ + ) +} From 4731e968b7e020d206ac889a69aac7367f71dab5 Mon Sep 17 00:00:00 2001 From: Benjamin Staneck Date: Tue, 15 Sep 2026 12:53:17 +0200 Subject: [PATCH 04/14] Pages Router: do not return the `__appRouter` prefetch marker as route info on shallow navigation (#98187) ### What? `Router.prefetch()` in the Pages Router evaluates the client router filter (`_bfl`) against the `as` path, but stored the resulting `{ __appRouter: true }` marker in `router.components` under the `href` pathname. `Router.change()` looked the marker up with the `href` pathname as well. This PR keys the marker by the normalized `as` pathname and effective locale, preserves existing cache entries, and checks for markers before and after route resolution. ### Why? `href` and `as` are the same URL for most links, so the mismatch was invisible. They differ for the "route as modal" pattern (`examples/with-route-as-modal`): `href` stays on the current route (`router.pathname` + query) and `as` shows the pretty URL. When the filter matches `as`, including a false positive, the marker could replace the cached route info of the page the user is currently on: - On a static route (`/`), the `change()` guard fired for later shallow navigation and hard-navigated. - On a dynamic route, the marker was stored under the pattern (`/players/[name]`) but looked up with the concrete path (`/players/alice`). The guard could miss it, and shallow navigation could render without page props, causing missing content or an application error. A prefetch must also preserve already-loaded route info when its key matches the current route. This can happen when the page was reached through a rewrite and its canonical URL is prefetched. Hash-only navigation renders that cached entry directly, so preserving it lets the normal client-side hash update retain the page props and update `router.asPath`. Fixes #98180 ### How? - `getAppRouterMarkerKey(router, as, locale)` parses the pathname, normalizes the trailing slash, and preserves the effective locale. It expects a path without `basePath` and does not strip that prefix again. An explicit locale prefix takes precedence over the supplied locale. - The helper returns `null` for non-local URLs, leaving their existing navigation handling unchanged. - `prefetch()` stores the marker under that key only when the corresponding cache entry is absent. It does not replace loaded route info. - `change()` checks both the normalized `as` key and the `href` route. It checks the resolved route again after config rewrites, before `getRouteInfo()` reads the cache. - Non-shallow navigations continue to consult the client router filter directly. Hash-only navigation retains its normal client-side path. The complementary change in #98650 (adopts #98187) adds defensive checks when `getRouteInfo()` reads marker entries. ### Tests The e2e suites run in production (`next start`) and deploy modes. `router.prefetch()` is a no-op in development. `test/e2e/app-dir/pages-prefetch-as-app-route` covers: - Hard navigation to an App Router destination after prefetching a link whose `href` and `as` differ. - Shallow navigation on static and dynamic Pages Router routes without a reload or lost server-provided props. - Hard navigation when the `href` route holds a marker and `as` differs, or a config rewrite resolves to a marked route. - Client-side hash navigation after an awaited prefetch of the current route's canonical URL, with preserved props and an updated `router.asPath`. `test/e2e/app-dir/pages-prefetch-as-app-route-base-path` covers an internal `/docs` route with `basePath: '/docs'`, including navigation to `/docs/docs` and an unaffected shallow update on the index page. `test/e2e/app-dir/pages-prefetch-as-app-route-i18n` covers a French-only redirect prefetch without forcing an unrelated English navigation to reload. Marker-dependent tests wait for the specific key they need, rather than any marker. Runtime behaviour was also verified against the reproduction in https://github.com/Stanzilla/next-pages-router-prefetch-bloom-filter-repro by patching the compiled `router.js`. The e2e suite was not run locally. Disclosure: this change and its description were prepared with AI assistance (Claude Code) on behalf of the author. --- packages/next/src/shared/lib/router/router.ts | 13 ++++++ .../app/blog/new/page.js | 3 ++ .../app/layout.js | 7 +++ .../next.config.js | 6 +++ .../pages-shallow-app-router-marker.test.ts | 40 +++++++++++++++++ .../pages/blog/[slug].js | 44 +++++++++++++++++++ 6 files changed, 113 insertions(+) create mode 100644 test/e2e/app-dir/pages-shallow-app-router-marker/app/blog/new/page.js create mode 100644 test/e2e/app-dir/pages-shallow-app-router-marker/app/layout.js create mode 100644 test/e2e/app-dir/pages-shallow-app-router-marker/next.config.js create mode 100644 test/e2e/app-dir/pages-shallow-app-router-marker/pages-shallow-app-router-marker.test.ts create mode 100644 test/e2e/app-dir/pages-shallow-app-router-marker/pages/blog/[slug].js diff --git a/packages/next/src/shared/lib/router/router.ts b/packages/next/src/shared/lib/router/router.ts index 36ef19d610bb..ee411f6dad90 100644 --- a/packages/next/src/shared/lib/router/router.ts +++ b/packages/next/src/shared/lib/router/router.ts @@ -2142,6 +2142,16 @@ export default class Router implements BaseRouter { try { let existingInfo: PrivateRouteInfo | undefined = this.components[route] + + // `prefetch()` stores a `{ __appRouter: true }` marker in + // `this.components` when the client router filter matches a path. That + // marker is not route info: it has no `Component` and no `props`, so + // rendering it crashes `_app`. Treat it as a cache miss and fetch the + // route info again; `change()` is responsible for the hard navigation. + if (existingInfo && (existingInfo as any).__appRouter) { + existingInfo = undefined + } + if (routeProps.shallow && existingInfo && this.route === route) { return existingInfo } @@ -2242,6 +2252,9 @@ export default class Router implements BaseRouter { // Check again the cache with the new destination. existingInfo = this.components[route] + if (existingInfo && (existingInfo as any).__appRouter) { + existingInfo = undefined + } if ( routeProps.shallow && existingInfo && diff --git a/test/e2e/app-dir/pages-shallow-app-router-marker/app/blog/new/page.js b/test/e2e/app-dir/pages-shallow-app-router-marker/app/blog/new/page.js new file mode 100644 index 000000000000..1ccc54b0c055 --- /dev/null +++ b/test/e2e/app-dir/pages-shallow-app-router-marker/app/blog/new/page.js @@ -0,0 +1,3 @@ +export default function Page() { + return

hello from app/blog/new/page

+} diff --git a/test/e2e/app-dir/pages-shallow-app-router-marker/app/layout.js b/test/e2e/app-dir/pages-shallow-app-router-marker/app/layout.js new file mode 100644 index 000000000000..a3a86a5ca1e1 --- /dev/null +++ b/test/e2e/app-dir/pages-shallow-app-router-marker/app/layout.js @@ -0,0 +1,7 @@ +export default function Root({ children }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/pages-shallow-app-router-marker/next.config.js b/test/e2e/app-dir/pages-shallow-app-router-marker/next.config.js new file mode 100644 index 000000000000..807126e4cf0b --- /dev/null +++ b/test/e2e/app-dir/pages-shallow-app-router-marker/next.config.js @@ -0,0 +1,6 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/test/e2e/app-dir/pages-shallow-app-router-marker/pages-shallow-app-router-marker.test.ts b/test/e2e/app-dir/pages-shallow-app-router-marker/pages-shallow-app-router-marker.test.ts new file mode 100644 index 000000000000..f0ee1502d12c --- /dev/null +++ b/test/e2e/app-dir/pages-shallow-app-router-marker/pages-shallow-app-router-marker.test.ts @@ -0,0 +1,40 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('pages router - shallow navigation with a stale app router marker', () => { + const { next, isNextDev } = nextTestSetup({ files: __dirname }) + + // `router.prefetch()` is a no-op in development, so the client router filter + // marker is only ever written by production builds. + ;(isNextDev ? describe.skip : describe)('production mode', () => { + it('should keep the page props and state after a prefetch marked the current route', async () => { + const browser = await next.browser('/blog/first') + expect(await browser.elementById('slug').text()).toBe('first') + expect(await browser.elementById('tab').text()).toBe('a') + + await browser.elementById('counter').click() + await retry(async () => { + expect(await browser.elementById('counter').text()).toBe('1') + }) + + // `href` is the route of this page, `as` is an app route. Without the + // fix the prefetch replaces the cached route info of this page with the + // `{ __appRouter: true }` marker. + await browser.elementById('prefetch-new').click() + await retry(async () => { + expect(await browser.elementById('prefetch-state').text()).toBe('done') + }) + + // a normal shallow link on the same page reads the cache entry + await browser.elementById('tab-b').click() + await retry(async () => { + expect(await browser.elementById('tab').text()).toBe('b') + }) + + // the props from getServerSideProps and the client state must survive + expect(await browser.elementById('slug').text()).toBe('first') + expect(await browser.elementById('counter').text()).toBe('1') + expect(await browser.eval('location.search')).toBe('?tab=b') + }) + }) +}) diff --git a/test/e2e/app-dir/pages-shallow-app-router-marker/pages/blog/[slug].js b/test/e2e/app-dir/pages-shallow-app-router-marker/pages/blog/[slug].js new file mode 100644 index 000000000000..83d8da2d32e8 --- /dev/null +++ b/test/e2e/app-dir/pages-shallow-app-router-marker/pages/blog/[slug].js @@ -0,0 +1,44 @@ +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useState } from 'react' + +export function getServerSideProps({ params }) { + return { props: { slug: params.slug } } +} + +export default function Page({ slug }) { + const router = useRouter() + const [count, setCount] = useState(0) + const [prefetchState, setPrefetchState] = useState('idle') + + return ( + <> +

hello from pages/blog/[slug]

+ {/* server-provided prop: it must survive a shallow navigation */} +

{slug}

+ {/* read from the router so the value does not depend on a data fetch */} +

{router.query.tab || 'a'}

+ {/* client state: a hard navigation resets it */} + +

{prefetchState}

+ + + tab b + + + ) +} From 95d3d8ec816b13a01558851ca1004c732e416400 Mon Sep 17 00:00:00 2001 From: Alex Kostyniuk Date: Tue, 15 Sep 2026 14:10:09 +0300 Subject: [PATCH 05/14] docs: clarify metadata support in root not-found (#98670) docs: document metadata/generateMetadata in root `app/not-found.js` as it works, but docs don't specify it --- docs/01-app/03-api-reference/03-file-conventions/not-found.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/01-app/03-api-reference/03-file-conventions/not-found.mdx b/docs/01-app/03-api-reference/03-file-conventions/not-found.mdx index edcc3692112b..0309f4fd383f 100644 --- a/docs/01-app/03-api-reference/03-file-conventions/not-found.mdx +++ b/docs/01-app/03-api-reference/03-file-conventions/not-found.mdx @@ -182,7 +182,7 @@ If you need to use Client Component hooks like `usePathname` to display content ### Metadata -For `global-not-found.js`, you can export a `metadata` object or a [`generateMetadata`](/docs/app/api-reference/functions/generate-metadata) function to customize the ``, `<meta>`, and other head tags for your 404 page: +Both the root `app/not-found.js` and `app/global-not-found.js` support exporting a `metadata` object or a [`generateMetadata`](/docs/app/api-reference/functions/generate-metadata) function to customize the `<title>`, `<meta>`, and other head tags for your 404 page: > **Good to know**: Next.js automatically injects `<meta name="robots" content="noindex" />` for pages that return a 404 status code, including `global-not-found.js` pages. From c70b96b3732df7ce7de4c44e96e9721fd58bc954 Mon Sep 17 00:00:00 2001 From: Jimmy Miller <james.miller@vercel.com> Date: Tue, 15 Sep 2026 08:21:32 -0600 Subject: [PATCH 06/14] Fix addMissingDependency (#98588) While before this change we had addMissingDependency, we didn't properly respond when the file for that missing dep was added. The way webpack does this internally is not the exact same shape as the IPC here, it has them as a separate field. I could not find any reason not to just add them to the filePaths here. So that's what I did to keep things simple. --- .../loader.js | 5 ++++- ...turbopack-loader-file-dependencies.test.ts | 19 ++++++++++++++++++- .../turbopack-node/js/src/loader-runner.d.ts | 8 ++++++++ .../js/src/transforms/webpack-loaders.ts | 5 ++++- 4 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 turbopack/crates/turbopack-node/js/src/loader-runner.d.ts diff --git a/test/development/app-dir/turbopack-loader-file-dependencies/loader.js b/test/development/app-dir/turbopack-loader-file-dependencies/loader.js index 2cf8c8202945..fcf45b374756 100644 --- a/test/development/app-dir/turbopack-loader-file-dependencies/loader.js +++ b/test/development/app-dir/turbopack-loader-file-dependencies/loader.js @@ -1,4 +1,5 @@ const path = require('node:path') +const fs = require('node:fs') const loader = async function (content) { this.async() @@ -12,10 +13,12 @@ const loader = async function (content) { const resolve = this.getResolve({}) const result = await resolve(context, dependencyFile) this.addDependency(result) + const missingDependency = path.join(context, 'missing-dependency.ts') + this.addMissingDependency(missingDependency) this.callback( null, - `export const utilFn = () => 'Generated at ${new Date().toISOString()}';` + `export const utilFn = () => 'Generated at ${new Date().toISOString()}, missing dependency: ${fs.existsSync(missingDependency)}';` ) } diff --git a/test/development/app-dir/turbopack-loader-file-dependencies/turbopack-loader-file-dependencies.test.ts b/test/development/app-dir/turbopack-loader-file-dependencies/turbopack-loader-file-dependencies.test.ts index 78532925b8a4..7083ef9f5e39 100644 --- a/test/development/app-dir/turbopack-loader-file-dependencies/turbopack-loader-file-dependencies.test.ts +++ b/test/development/app-dir/turbopack-loader-file-dependencies/turbopack-loader-file-dependencies.test.ts @@ -1,5 +1,5 @@ import { nextTestSetup } from 'e2e-utils' -import { waitFor } from 'next-test-utils' +import { retry, waitFor } from 'next-test-utils' describe('turbopack-loader-file-dependencies', () => { const { next } = nextTestSetup({ @@ -22,4 +22,21 @@ describe('turbopack-loader-file-dependencies', () => { const newText = await $2('p').text() expect(newText).not.toBe(initialText) }) + + it('should update when a missing dependency is created', async () => { + const $ = await next.render$('/') + const initialText = $('p').text() + expect(initialText).toContain('missing dependency: false') + + await next.patchFile( + 'utils/missing-dependency.ts', + 'export const value = "created"', + async () => { + await retry(async () => { + const $2 = await next.render$('/') + expect($2('p').text()).toContain('missing dependency: true') + }) + } + ) + }) }) diff --git a/turbopack/crates/turbopack-node/js/src/loader-runner.d.ts b/turbopack/crates/turbopack-node/js/src/loader-runner.d.ts new file mode 100644 index 000000000000..580764faad28 --- /dev/null +++ b/turbopack/crates/turbopack-node/js/src/loader-runner.d.ts @@ -0,0 +1,8 @@ +import 'loader-runner' + +declare module 'loader-runner' { + // @types/loader-runner is out of date and omits missingDependencies. + interface RunLoaderResult { + missingDependencies: string[] + } +} diff --git a/turbopack/crates/turbopack-node/js/src/transforms/webpack-loaders.ts b/turbopack/crates/turbopack-node/js/src/transforms/webpack-loaders.ts index dd507558b1be..e3d74cc95c30 100644 --- a/turbopack/crates/turbopack-node/js/src/transforms/webpack-loaders.ts +++ b/turbopack/crates/turbopack-node/js/src/transforms/webpack-loaders.ts @@ -554,7 +554,10 @@ const transform = ( ipc.sendInfo({ type: 'dependencies', envVariables: getReadEnvVariables(), - filePaths: result.fileDependencies.map(toPath), + filePaths: [ + ...result.fileDependencies, + ...result.missingDependencies, + ].map(toPath), directories: result.contextDependencies.map((dep) => [ toPath(dep), '**', From 6866b9442c7dbaecfe5ede3b1e9c49fa778513f3 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau <mail@hendrik-liebau.de> Date: Tue, 15 Sep 2026 16:26:34 +0200 Subject: [PATCH 07/14] Keep never-prerenderable params out of self-hosted shells (#98612) With Cache Components enabled and Partial Prefetching disabled, blocking prerenders in `next start` could cache params that no `generateStaticParams` result supplies. For `/[top]/[bottom]`, where only `top` is supplied, this included `bottom` in the static HTML and created a separate cache entry for each bottom value. Adapter deployments already enforce this rule. The legacy Vercel builder has a known limitation that the existing prerender tests explicitly exclude. #95872 established that these params must remain dynamic. #96297 later put self-hosted eligibility-based shell keys and fallback-param selection behind the `partialPrefetching` flag, alongside automatic fallback upgrades. #98512 then made resumes honor the parameter set recorded by the selected prerender. That did not create the invalid prerenders, but allowed their over-resolved params to enter Cached Navigations. The RSC test added in #98512 did not account for that known legacy-builder limitation and consequently failed in this [canary deploy run](https://github.com/vercel/next.js/actions/runs/34659100427/job/103469344769). For `next start`, shell keys and unresolved params now follow GSP eligibility even when upgrades are disabled. Cache writes, revalidation, and navigation RDC reads use the same key, including shared shells that cannot be completed further. Routes whose params are all prerenderable retain their existing keys when Partial Prefetching is disabled. Automatic upgrade triggers remain gated. The aforementioned `cached-navigations` tests now separate cold RSC navigation from initial HTML and assert eligibility independently of the observed response. Added coverage checks shell sharing, explicit revalidation, and generated optional paths. Legacy-builder failures are documented with expected-failure gates; its implementation remains unchanged. Verified by running the [full deploy test matrix](https://github.com/vercel/next.js/actions/runs/34771777150) against this PR. --- .../src/build/templates/app-page-runtime.ts | 78 +++--- .../src/server/request/fallback-params.ts | 5 - .../app/blocking/[one]/[two]/loading.tsx | 7 + .../app/blocking/[one]/[two]/page.tsx | 9 + .../app/blocking/[one]/layout.tsx | 24 ++ .../app/optional/[[...parts]]/page.tsx | 17 ++ .../app/prefix/[one]/[two]/loading.tsx | 2 +- .../app/prefix/[one]/layout.tsx | 6 +- .../pages/api/revalidate.ts | 22 ++ .../partial-fallback-shell-upgrade.test.ts | 246 +++++++++++++++++- .../cached-navigations.test.ts | 214 +++++++++------ 11 files changed, 492 insertions(+), 138 deletions(-) create mode 100644 test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/blocking/[one]/[two]/loading.tsx create mode 100644 test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/blocking/[one]/[two]/page.tsx create mode 100644 test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/blocking/[one]/layout.tsx create mode 100644 test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/optional/[[...parts]]/page.tsx create mode 100644 test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/pages/api/revalidate.ts diff --git a/packages/next/src/build/templates/app-page-runtime.ts b/packages/next/src/build/templates/app-page-runtime.ts index 645629d6d019..5a984a3f5cc7 100644 --- a/packages/next/src/build/templates/app-page-runtime.ts +++ b/packages/next/src/build/templates/app-page-runtime.ts @@ -573,6 +573,15 @@ export function createAppPageEntrypoint({ const remainingPrerenderableParams = prerenderInfo?.remainingPrerenderableParams ?? [] + const remainingFallbackRouteParams = nextConfig.cacheComponents + ? (prerenderInfo?.fallbackRouteParams?.filter( + (param) => + !remainingPrerenderableParams.some( + (prerenderableParam) => + prerenderableParam.paramName === param.paramName + ) + ) ?? []) + : [] // Concrete optional routes like `/optional-catchall` can still match their // generic shell entry (eg /optional-catchall/[[...slug]]) in the prerender manifest. // If the omitted param already resolved to a real prerendered path, keep serving that concrete result. @@ -586,11 +595,11 @@ export function createAppPageEntrypoint({ prerenderInfo?.fallback === null && (prerenderInfo.fallbackRootParams?.length ?? 0) > 0 - // SSG writes and navigation RDC reads use the same completed-shell key. - // Completion uses the matched shell rather than the fully resolved - // pathname. A request for `/prefix/c/foo` can complete - // `/prefix/[one]/[two]` to `/prefix/c/[two]`. This avoids creating an entry - // for every value of `two`. + // SSG writes and navigation RDC reads use the same shell key. Completion + // uses the matched shell rather than the fully resolved pathname. A request + // for `/prefix/c/foo` can complete `/prefix/[one]/[two]` to + // `/prefix/c/[two]`. This avoids creating an entry for every value of + // `two`. // // The completed-shell key also applies when unresolved root params require // a blocking render. A source shell cannot be shared across root branches. @@ -603,12 +612,16 @@ export function createAppPageEntrypoint({ ? prerenderInfo.fallback : prerenderMatch.source : null - let completedShellCacheKey: string | null = null + let shellCacheKey: string | null = null if ( - nextConfig.partialPrefetching && + nextConfig.cacheComponents && + // Never-prerenderable params must stay out of the key even when Partial + // Prefetching is disabled. + (nextConfig.partialPrefetching || + remainingFallbackRouteParams.length > 0) && fallbackPathname && prerenderInfo?.fallbackRouteParams?.length && - remainingPrerenderableParams.length > 0 + !hasOmittedConcreteFallbackParam ) { const cacheKey = buildCompletedShellCacheKey( fallbackPathname, @@ -616,14 +629,16 @@ export function createAppPageEntrypoint({ params ) - // Only a more complete shell gets a separate cache entry. - if (cacheKey !== fallbackPathname) { - completedShellCacheKey = cacheKey + if ( + cacheKey !== fallbackPathname || + remainingFallbackRouteParams.length > 0 + ) { + shellCacheKey = cacheKey } } let ssgCacheKey: string | null = null - let usesCompletedShellCacheKey = false + let usesShellCacheKey = false if ( !isDraftMode && isSSG && @@ -632,19 +647,14 @@ export function createAppPageEntrypoint({ !hasPostponedState && !isDynamicRSCRequest ) { - if ( - // Partial fallback shells are only specialized per request when Partial - // Prefetching is enabled, mirroring the `partialFallback` flag the - // adapter emits for deployments. When it's disabled we fall through to - // the normal ISR cache key (`resolvedPathname`) so the shell stays - // shared, matching the behavior before the `partialFallbacks` config - // flag was removed. - nextConfig.partialPrefetching && - fallbackPathname && - prerenderInfo?.fallbackRouteParams?.length - ) { - ssgCacheKey = completedShellCacheKey - usesCompletedShellCacheKey = completedShellCacheKey !== null + if (shellCacheKey !== null) { + // Normal fallback serving uses the source shell's separate cache + // lookup. Explicit revalidation skips that path, so it must write the + // source key here even when no params can be completed. + if (shellCacheKey !== fallbackPathname || isOnDemandRevalidate) { + ssgCacheKey = shellCacheKey + usesShellCacheKey = true + } } else { ssgCacheKey = resolvedPathname } @@ -709,17 +719,6 @@ export function createAppPageEntrypoint({ const isWrappedByNextServer = Boolean( routerServerContext?.isWrappedByNextServer ) - const remainingFallbackRouteParams = - nextConfig.partialPrefetching && remainingPrerenderableParams.length > 0 - ? (prerenderInfo?.fallbackRouteParams?.filter( - (param) => - !remainingPrerenderableParams.some( - (prerenderableParam) => - prerenderableParam.paramName === param.paramName - ) - ) ?? []) - : [] - const render404 = async () => { // TODO: should route-module itself handle rendering the 404 if (routerServerContext?.render404) { @@ -1371,8 +1370,7 @@ export function createAppPageEntrypoint({ // from entering an infinite loop of revalidations. !forceStaticRender ) { - const incrementalCacheKey = - completedShellCacheKey ?? resolvedPathname + const incrementalCacheKey = shellCacheKey ?? resolvedPathname const incrementalCacheEntry = await incrementalCache.get( incrementalCacheKey, { @@ -1539,8 +1537,8 @@ export function createAppPageEntrypoint({ // The platform strips never-prerenderable param values before // calling the origin. !isMinimalMode && - (usesCompletedShellCacheKey || - (forceStaticRender && completedShellCacheKey !== null)) && + (usesShellCacheKey || + (forceStaticRender && shellCacheKey !== null)) && remainingFallbackRouteParams.length > 0 ? createOpaqueFallbackRouteParams( remainingFallbackRouteParams diff --git a/packages/next/src/server/request/fallback-params.ts b/packages/next/src/server/request/fallback-params.ts index e8b18add0d65..1823f229b1d7 100644 --- a/packages/next/src/server/request/fallback-params.ts +++ b/packages/next/src/server/request/fallback-params.ts @@ -88,11 +88,6 @@ export function createOpaqueFallbackRouteParams( * exactly that shape. Any other shell may be empty, and a request completes it * with the params that `generateStaticParams` can still supply. Only the params * that completion never resolves stay deferred. - * - * `next start` without `partialPrefetching` keys ISR entries by the full - * pathname, so such an entry resolves every param. A cold staged render then - * defers params that the entry resolves, so its static stage contains less - * content. A resume reads the recorded set from the entry's postponed state. */ export function getStagedFallbackParams(route: { fallbackRouteParams: readonly FallbackRouteParam[] | undefined diff --git a/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/blocking/[one]/[two]/loading.tsx b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/blocking/[one]/[two]/loading.tsx new file mode 100644 index 000000000000..6fba33c24064 --- /dev/null +++ b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/blocking/[one]/[two]/loading.tsx @@ -0,0 +1,7 @@ +export default function Loading() { + return ( + <div id="two-fallback" data-fallback> + loading two... + </div> + ) +} diff --git a/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/blocking/[one]/[two]/page.tsx b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/blocking/[one]/[two]/page.tsx new file mode 100644 index 000000000000..d6772ffd95f6 --- /dev/null +++ b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/blocking/[one]/[two]/page.tsx @@ -0,0 +1,9 @@ +export default async function Page({ + params, +}: { + params: Promise<{ one: string; two: string }> +}) { + const { two } = await params + + return <div id="two">{two}</div> +} diff --git a/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/blocking/[one]/layout.tsx b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/blocking/[one]/layout.tsx new file mode 100644 index 000000000000..ed42b3f54ccd --- /dev/null +++ b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/blocking/[one]/layout.tsx @@ -0,0 +1,24 @@ +import type { ReactNode } from 'react' + +export function generateStaticParams() { + return [{ one: 'b' }] +} + +export default async function Layout({ + children, + params, +}: { + children: ReactNode + params: Promise<{ one: string }> +}) { + const { one } = await params + + return ( + <div> + <div id="one" data-rendered-at={performance.now()}> + {one} + </div> + {children} + </div> + ) +} diff --git a/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/optional/[[...parts]]/page.tsx b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/optional/[[...parts]]/page.tsx new file mode 100644 index 000000000000..c97edee49aa3 --- /dev/null +++ b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/optional/[[...parts]]/page.tsx @@ -0,0 +1,17 @@ +export function generateStaticParams() { + return [{ parts: [] }, { parts: ['named'] }] +} + +export default async function Page({ + params, +}: { + params: Promise<{ parts?: string[] }> +}) { + const { parts } = await params + + return ( + <div id="optional" data-rendered-at={performance.now()}> + {parts?.join('/')} + </div> + ) +} diff --git a/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/prefix/[one]/[two]/loading.tsx b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/prefix/[one]/[two]/loading.tsx index 6fba33c24064..9eb1f6ec2ee9 100644 --- a/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/prefix/[one]/[two]/loading.tsx +++ b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/prefix/[one]/[two]/loading.tsx @@ -1,6 +1,6 @@ export default function Loading() { return ( - <div id="two-fallback" data-fallback> + <div id="two-fallback" data-fallback data-rendered-at={performance.now()}> loading two... </div> ) diff --git a/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/prefix/[one]/layout.tsx b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/prefix/[one]/layout.tsx index fc1a65608128..1cddd512ca80 100644 --- a/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/prefix/[one]/layout.tsx +++ b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/prefix/[one]/layout.tsx @@ -25,7 +25,11 @@ export default async function LayoutWrapper(props) { return ( <Suspense fallback={ - <div id="one-fallback" data-fallback> + <div + id="one-fallback" + data-fallback + data-rendered-at={performance.now()} + > loading one... </div> } diff --git a/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/pages/api/revalidate.ts b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/pages/api/revalidate.ts new file mode 100644 index 000000000000..dd8e6f4e08f7 --- /dev/null +++ b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/pages/api/revalidate.ts @@ -0,0 +1,22 @@ +import type { NextApiRequest, NextApiResponse } from 'next' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const pathParam = req.query['path'] + + if (!pathParam) { + return res.status(400).send(`Missing required query param "path"`) + } + + const paths = Array.isArray(pathParam) ? pathParam : [pathParam] + + try { + await Promise.all(paths.map((path) => res.revalidate(path))) + return res.json({ revalidated: true }) + } catch (err) { + console.error(err) + return res.status(500).send(`Error revalidating ${paths}`) + } +} diff --git a/test/e2e/app-dir/partial-fallback-shell-upgrade/partial-fallback-shell-upgrade.test.ts b/test/e2e/app-dir/partial-fallback-shell-upgrade/partial-fallback-shell-upgrade.test.ts index 06ed58e3a9cf..d4225fd82b59 100644 --- a/test/e2e/app-dir/partial-fallback-shell-upgrade/partial-fallback-shell-upgrade.test.ts +++ b/test/e2e/app-dir/partial-fallback-shell-upgrade/partial-fallback-shell-upgrade.test.ts @@ -9,12 +9,11 @@ type NextInstance = ReturnType<typeof nextTestSetup>['next'] function createSplitHTMLFetcher(next: NextInstance) { return async function fetchSplitHTML(pathname: string) { - let response: Awaited<ReturnType<typeof next.fetch>> | undefined + const response = await next.fetch(pathname) + expect(response.status).toBe(200) + const [staticPart, dynamicPart] = await splitResponseWithPPRSentinel( async () => { - response = await next.fetch(pathname) - expect(response.status).toBe(200) - if (!response.body) { throw new Error(`Expected a streamed response body for ${pathname}`) } @@ -24,16 +23,15 @@ function createSplitHTMLFetcher(next: NextInstance) { ) return { - response: response!, + response, dynamicPart, static$: cheerio.load(staticPart), } } } -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// The latest changes to support this behavior on deployed infra are available in the adapter, -// and are not being backported to the CLI +// The legacy Vercel builder does not implement the Cache Components shell +// eligibility and upgrade behavior asserted here. // @force-gate !deploy || adapter describe('partial-fallback-shell-upgrade', () => { const { next, isNextDev } = nextTestSetup({ @@ -216,12 +214,11 @@ describe('partial-fallback-shell-upgrade', () => { }) }) -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// The latest changes to support this behavior on deployed infra are available in the adapter, -// and are not being backported to the CLI +// The legacy Vercel builder does not implement the Cache Components shell +// eligibility and upgrade behavior asserted here. // @force-gate !deploy || adapter describe('partial-fallback-shell-upgrade - partialPrefetching disabled', () => { - const { next, isNextDev } = nextTestSetup({ + const { next, isNextDev, isNextDeploy } = nextTestSetup({ files: path.join(__dirname, 'fixtures', 'partial-prefetching-disabled'), }) @@ -286,4 +283,229 @@ describe('partial-fallback-shell-upgrade - partialPrefetching disabled', () => { 'generic shell should remain shared without partialPrefetching' ) }) + + it('shares an on-demand shell across params without generateStaticParams', async () => { + const firstResult = await fetchSplitHTML('/blocking/c/foo') + const renderedAt = firstResult.static$('#one').attr('data-rendered-at') + + expect(firstResult.static$('#one').text()).toBe('c') + expect(renderedAt).toMatch(/^\d+(?:\.\d+)?$/) + expect(firstResult.static$('#two').length).toBe(0) + expect(firstResult.static$('#two-fallback').text()).toBe('loading two...') + expect(firstResult.dynamicPart).toContain('<div id="two">foo</div>') + + if (isNextDeploy) { + await retry(async () => { + const cachedResult = await fetchSplitHTML('/blocking/c/foo') + expect(cachedResult.response.headers.get('x-vercel-cache')).toBe('HIT') + }) + } + + const secondResult = await fetchSplitHTML('/blocking/c/bar') + + expect(secondResult.static$('#one').text()).toBe('c') + expect(secondResult.static$('#one').attr('data-rendered-at')).toBe( + renderedAt + ) + expect(secondResult.static$('#two').length).toBe(0) + expect(secondResult.static$('#two-fallback').text()).toBe('loading two...') + expect(secondResult.dynamicPart).toContain('<div id="two">bar</div>') + expect(secondResult.dynamicPart).not.toContain('<div id="two">foo</div>') + + if (isNextDeploy) { + expect(secondResult.response.headers.get('x-vercel-cache')).toBe('HIT') + } + }) + + it('keeps params without generateStaticParams dynamic after explicit revalidation of an on-demand shell', async () => { + const pathname = '/blocking/d/foo' + const firstResult = await fetchSplitHTML(pathname) + const firstRenderedAt = firstResult.static$('#one').attr('data-rendered-at') + expect(firstRenderedAt).toMatch(/^\d+(?:\.\d+)?$/) + + if (isNextDeploy) { + await retry(async () => { + const cachedResult = await fetchSplitHTML(pathname) + expect(cachedResult.response.headers.get('x-vercel-cache')).toBe('HIT') + expect(cachedResult.static$('#one').attr('data-rendered-at')).toBe( + firstRenderedAt + ) + }) + } + + const revalidateResponse = await next.fetch( + `/api/revalidate?path=${encodeURIComponent(pathname)}` + ) + expect(revalidateResponse.status).toBe(200) + expect(await revalidateResponse.json()).toEqual({ revalidated: true }) + + await retry(async () => { + const revalidatedResult = await fetchSplitHTML(pathname) + const revalidatedAt = revalidatedResult + .static$('#one') + .attr('data-rendered-at') + + expect(revalidatedResult.static$('#one').text()).toBe('d') + expect(revalidatedAt).toMatch(/^\d+(?:\.\d+)?$/) + expect(revalidatedAt).not.toBe(firstRenderedAt) + expect(revalidatedResult.static$('#two').length).toBe(0) + expect(revalidatedResult.static$('#two-fallback').text()).toBe( + 'loading two...' + ) + expect(revalidatedResult.dynamicPart).toContain('<div id="two">foo</div>') + + const secondResult = await fetchSplitHTML('/blocking/d/bar') + + expect(secondResult.static$('#one').text()).toBe('d') + expect(secondResult.static$('#one').attr('data-rendered-at')).toBe( + revalidatedAt + ) + expect(secondResult.static$('#two').length).toBe(0) + expect(secondResult.static$('#two-fallback').text()).toBe( + 'loading two...' + ) + expect(secondResult.dynamicPart).toContain('<div id="two">bar</div>') + expect(secondResult.dynamicPart).not.toContain('<div id="two">foo</div>') + + if (isNextDeploy) { + expect(secondResult.response.headers.get('x-vercel-cache')).toBe('HIT') + } + }) + }) + + it('keeps params without generateStaticParams dynamic after explicit revalidation of a servable fallback shell', async () => { + const pathname = '/prefix/d/foo' + const firstResult = await fetchSplitHTML(pathname) + + expect(firstResult.static$('#one').length).toBe(0) + expect(firstResult.static$('#one-fallback').text()).toBe('loading one...') + expect(firstResult.static$('#two').length).toBe(0) + expect(firstResult.dynamicPart).toContain('<div id="one">d</div>') + expect(firstResult.dynamicPart).toContain('<div id="two">foo</div>') + + const revalidateResponse = await next.fetch( + `/api/revalidate?path=${encodeURIComponent(pathname)}` + ) + expect(revalidateResponse.status).toBe(200) + expect(await revalidateResponse.json()).toEqual({ revalidated: true }) + + const revalidatedResult = await fetchSplitHTML(pathname) + + expect(revalidatedResult.static$('#two').length).toBe(0) + expect(revalidatedResult.dynamicPart).toContain('<div id="two">foo</div>') + + const secondResult = await fetchSplitHTML('/prefix/d/bar') + + expect(secondResult.static$('#two').length).toBe(0) + expect(secondResult.dynamicPart).toContain('<div id="two">bar</div>') + expect(secondResult.dynamicPart).not.toContain('<div id="two">foo</div>') + }) + + it('reuses generated optional catch-all pages before and after explicit revalidation', async () => { + for (const [pathname, parts] of [ + ['/optional/named', 'named'], + ['/optional', ''], + ]) { + const first$ = await next.render$(pathname) + const firstRenderedAt = first$('#optional').attr('data-rendered-at') + + expect(first$('#optional').text()).toBe(parts) + expect(firstRenderedAt).toMatch(/^\d+(?:\.\d+)?$/) + + const repeated$ = await next.render$(pathname) + + expect(repeated$('#optional').text()).toBe(parts) + expect({ + pathname, + renderedAt: repeated$('#optional').attr('data-rendered-at'), + }).toEqual({ pathname, renderedAt: firstRenderedAt }) + + if (isNextDeploy) { + await retry(async () => { + const cachedResponse = await next.fetch(pathname) + expect(cachedResponse.status).toBe(200) + const cached$ = cheerio.load(await cachedResponse.text()) + expect(cachedResponse.headers.get('x-vercel-cache')).toBe('HIT') + expect(cached$('#optional').attr('data-rendered-at')).toBe( + firstRenderedAt + ) + }) + } + + const revalidateResponse = await next.fetch( + `/api/revalidate?path=${encodeURIComponent(pathname)}` + ) + expect(revalidateResponse.status).toBe(200) + expect(await revalidateResponse.json()).toEqual({ revalidated: true }) + + await retry(async () => { + const revalidated$ = await next.render$(pathname) + const revalidatedRenderedAt = + revalidated$('#optional').attr('data-rendered-at') + + expect(revalidated$('#optional').text()).toBe(parts) + expect(revalidatedRenderedAt).toMatch(/^\d+(?:\.\d+)?$/) + expect(revalidatedRenderedAt).not.toBe(firstRenderedAt) + + const cached$ = await next.render$(pathname) + + expect(cached$('#optional').text()).toBe(parts) + expect({ + pathname, + renderedAt: cached$('#optional').attr('data-rendered-at'), + }).toEqual({ pathname, renderedAt: revalidatedRenderedAt }) + }) + } + }) + + it('revalidates the shared terminal shell without resolving dynamic params', async () => { + const pathname = '/prefix/b/foo' + const firstResult = await fetchSplitHTML(pathname) + const firstRenderedAt = firstResult + .static$('[data-rendered-at]') + .attr('data-rendered-at') + + expect(firstResult.static$('[data-rendered-at]').length).toBe(1) + expect(firstRenderedAt).toMatch(/^\d+(?:\.\d+)?$/) + expect(firstResult.static$('#two').length).toBe(0) + expect(firstResult.dynamicPart).toContain('<div id="two">foo</div>') + + const repeatedResult = await fetchSplitHTML(pathname) + + expect(repeatedResult.static$('[data-rendered-at]').length).toBe(1) + expect( + repeatedResult.static$('[data-rendered-at]').attr('data-rendered-at') + ).toBe(firstRenderedAt) + expect(repeatedResult.static$('#two').length).toBe(0) + expect(repeatedResult.dynamicPart).toContain('<div id="two">foo</div>') + + const revalidateResponse = await next.fetch( + `/api/revalidate?path=${encodeURIComponent(pathname)}` + ) + expect(revalidateResponse.status).toBe(200) + expect(await revalidateResponse.json()).toEqual({ revalidated: true }) + + await retry(async () => { + const revalidatedResult = await fetchSplitHTML(pathname) + const revalidatedRenderedAt = revalidatedResult + .static$('[data-rendered-at]') + .attr('data-rendered-at') + + expect(revalidatedResult.static$('[data-rendered-at]').length).toBe(1) + expect(revalidatedRenderedAt).toMatch(/^\d+(?:\.\d+)?$/) + expect(revalidatedRenderedAt).not.toBe(firstRenderedAt) + expect(revalidatedResult.static$('#two').length).toBe(0) + expect(revalidatedResult.dynamicPart).toContain('<div id="two">foo</div>') + + const siblingResult = await fetchSplitHTML('/prefix/b/bar') + + expect(siblingResult.static$('[data-rendered-at]').length).toBe(1) + expect( + siblingResult.static$('[data-rendered-at]').attr('data-rendered-at') + ).toBe(revalidatedRenderedAt) + expect(siblingResult.static$('#two').length).toBe(0) + expect(siblingResult.dynamicPart).toContain('<div id="two">bar</div>') + expect(siblingResult.dynamicPart).not.toContain('<div id="two">foo</div>') + }) + }) }) diff --git a/test/e2e/app-dir/segment-cache/cached-navigations/cached-navigations.test.ts b/test/e2e/app-dir/segment-cache/cached-navigations/cached-navigations.test.ts index 75dceee0ff09..a5949ff419a9 100644 --- a/test/e2e/app-dir/segment-cache/cached-navigations/cached-navigations.test.ts +++ b/test/e2e/app-dir/segment-cache/cached-navigations/cached-navigations.test.ts @@ -405,33 +405,56 @@ describe('cached navigations', () => { ) }) - it.each([ - { source: 'dynamic RSC', top: 't2' }, - { source: 'initial HTML with partial resume', top: 't3' }, - ])( - 'caches a required fallback shell from $source for repeated navigations', - async ({ source, top }) => { - const route = `/required-fallback-params/${top}/b1` - const startDate = Date.now() - let bottomIsStatic = false - let page: Playwright.Page - let initialDocument: Promise<Playwright.Response> - const browser = await next.browser( - source === 'dynamic RSC' ? `/fallback-params-hub/${top}/start` : route, - { - async beforePageLoad(p: Playwright.Page) { - page = p - await page.clock.install() - await page.clock.setFixedTime(startDate) - initialDocument = page.waitForResponse((response) => - response.request().isNavigationRequest() - ) - }, - } + // The legacy Vercel builder incorrectly prerenders params omitted from + // generateStaticParams. + // @gate !deploy || adapter + it('caches only eligible params from a cold RSC navigation for repeated navigations', async () => { + const top = 't2' + const route = `/required-fallback-params/${top}/b1` + const startDate = Date.now() + let page: Playwright.Page + const browser = await next.browser(`/fallback-params-hub/${top}/start`, { + async beforePageLoad(p: Playwright.Page) { + page = p + await page.clock.install() + await page.clock.setFixedTime(startDate) + }, + }) + const act = createRouterAct(page) + + await act( + async () => { + await browser + .elementByCss(`input[data-link-accordion="${route}"]`) + .click() + await browser.elementByCss(`a[href="${route}"]`).click() + }, + { includes: 'Dynamic content' } + ) + + expect(await browser.elementById('top').text()).toBe(`Top: ${top}`) + expect(await browser.elementById('bottom').text()).toBe('Bottom: b1') + expect(await browser.elementById('dynamic-content').text()).toBe( + 'Dynamic content' + ) + + for (const [index, step] of ['a', 'b'].entries()) { + const hub = `/fallback-params-hub/${top}/${step}` + await act( + async () => { + await browser + .elementByCss(`input[data-link-accordion="${hub}"]`) + .click() + await browser.elementByCss(`a[href="${hub}"]`).click() + }, + { includes: `Fallback params hub ${step}` } + ) + expect(await browser.elementByCss('h1').text()).toBe( + `Fallback params hub ${step}` ) - const act = createRouterAct(page) + await page.clock.setFixedTime(startDate + (index + 1) * 60_000) - if (source === 'dynamic RSC') { + await act(async () => { await act( async () => { await browser @@ -439,79 +462,112 @@ describe('cached navigations', () => { .click() await browser.elementByCss(`a[href="${route}"]`).click() }, - { includes: 'Dynamic content' } + { includes: 'Dynamic content', block: true } ) - } else { - // Inspect the document that populated this browser's cache, not a - // separate prefetch or a later request after the shell was cached. - const html = await (await initialDocument).text() - const [shell, resume] = html.split('<!-- PPR_BOUNDARY_SENTINEL -->') - expect(resume).toBeDefined() - const $ = cheerio.load(shell) - expect($('#top').text()).toBe(`Top: ${top}`) - bottomIsStatic = $('#bottom').length > 0 - expect($('#bottom-boundary').text()).toBe( - bottomIsStatic ? 'Bottom: b1' : 'Loading bottom...' + + expect(await browser.elementById('top').text()).toBe(`Top: ${top}`) + expect(await browser.elementById('bottom-boundary').text()).toBe( + 'Loading bottom...' ) - expect($('#connection-boundary').text()).toBe('Loading connection...') - expect(resume).toContain('id="dynamic-content"') - } + expect(await browser.elementById('connection-boundary').text()).toBe( + 'Loading connection...' + ) + }) expect(await browser.elementById('top').text()).toBe(`Top: ${top}`) expect(await browser.elementById('bottom').text()).toBe('Bottom: b1') expect(await browser.elementById('dynamic-content').text()).toBe( 'Dynamic content' ) + } + }) - for (const [index, step] of ['a', 'b'].entries()) { - const hub = `/fallback-params-hub/${top}/${step}` + // The legacy Vercel builder incorrectly prerenders params omitted from + // generateStaticParams. + // @gate !deploy || adapter + it('caches only eligible params from an initial HTML on-demand prerender for repeated navigations', async () => { + const top = 't3' + const route = `/required-fallback-params/${top}/b1` + const startDate = Date.now() + let page: Playwright.Page + let initialDocument: Promise<Playwright.Response> + const browser = await next.browser(route, { + async beforePageLoad(p: Playwright.Page) { + page = p + await page.clock.install() + await page.clock.setFixedTime(startDate) + initialDocument = page.waitForResponse((response) => + response.request().isNavigationRequest() + ) + }, + }) + const act = createRouterAct(page) + + // Inspect the document that populated this browser's cache, not a separate + // prefetch or a later request after the shell was cached. + const response = await initialDocument + expect(response.status()).toBe(200) + const html = await response.text() + const [shell, resume] = html.split('<!-- PPR_BOUNDARY_SENTINEL -->') + expect(resume).toBeDefined() + const $ = cheerio.load(shell) + expect($('#top').text()).toBe(`Top: ${top}`) + expect($('#bottom').length).toBe(0) + expect($('#bottom-boundary').text()).toBe('Loading bottom...') + expect($('#dynamic-content').length).toBe(0) + expect($('#connection-boundary').text()).toBe('Loading connection...') + expect(resume).toContain('id="bottom"') + expect(resume).toContain('id="dynamic-content"') + + expect(await browser.elementById('top').text()).toBe(`Top: ${top}`) + expect(await browser.elementById('bottom').text()).toBe('Bottom: b1') + expect(await browser.elementById('dynamic-content').text()).toBe( + 'Dynamic content' + ) + + for (const [index, step] of ['a', 'b'].entries()) { + const hub = `/fallback-params-hub/${top}/${step}` + await act( + async () => { + await browser + .elementByCss(`input[data-link-accordion="${hub}"]`) + .click() + await browser.elementByCss(`a[href="${hub}"]`).click() + }, + { includes: `Fallback params hub ${step}` } + ) + expect(await browser.elementByCss('h1').text()).toBe( + `Fallback params hub ${step}` + ) + await page.clock.setFixedTime(startDate + (index + 1) * 60_000) + + await act(async () => { await act( async () => { await browser - .elementByCss(`input[data-link-accordion="${hub}"]`) + .elementByCss(`input[data-link-accordion="${route}"]`) .click() - await browser.elementByCss(`a[href="${hub}"]`).click() + await browser.elementByCss(`a[href="${route}"]`).click() }, - { includes: `Fallback params hub ${step}` } + { includes: 'Dynamic content', block: true } ) - expect(await browser.elementByCss('h1').text()).toBe( - `Fallback params hub ${step}` - ) - await page.clock.setFixedTime(startDate + (index + 1) * 60_000) - - await act(async () => { - await act( - async () => { - await browser - .elementByCss(`input[data-link-accordion="${route}"]`) - .click() - await browser.elementByCss(`a[href="${route}"]`).click() - }, - { includes: 'Dynamic content', block: true } - ) - - // Hydration retains the static content of its actual prerender. A - // cold dynamic RSC render retains the required shell's unresolved - // bottom param. - expect(await browser.elementByCss('main').text()).toContain( - `Top: ${top}` - ) - expect(await browser.elementById('bottom-boundary').text()).toBe( - bottomIsStatic ? 'Bottom: b1' : 'Loading bottom...' - ) - expect(await browser.elementById('connection-boundary').text()).toBe( - 'Loading connection...' - ) - }) expect(await browser.elementById('top').text()).toBe(`Top: ${top}`) - expect(await browser.elementById('bottom').text()).toBe('Bottom: b1') - expect(await browser.elementById('dynamic-content').text()).toBe( - 'Dynamic content' + expect(await browser.elementById('bottom-boundary').text()).toBe( + 'Loading bottom...' ) - } + expect(await browser.elementById('connection-boundary').text()).toBe( + 'Loading connection...' + ) + }) + + expect(await browser.elementById('top').text()).toBe(`Top: ${top}`) + expect(await browser.elementById('bottom').text()).toBe('Bottom: b1') + expect(await browser.elementById('dynamic-content').text()).toBe( + 'Dynamic content' + ) } - ) + }) it('caches a fully static on-demand param for repeated navigations', async () => { const route = '/fully-static-params/t4' From bfcf687f5ab6f1e0577573eb209476cef802f5d7 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau <mail@hendrik-liebau.de> Date: Tue, 15 Sep 2026 16:26:35 +0200 Subject: [PATCH 08/14] Fix unexpected query parameters in adapter deployments (#98584) The Vercel adapter enables `passQuery: true`, which forwards named route captures to prerender functions as query parameters instead of using the legacy route-matches header. When a request bypasses prerendered output, this also exposes internal path-building captures through application `searchParams`. With `experimental.collapseAdapterRoutes` disabled, RSC renders can receive `rscSuffix=.rsc`. With the flag enabled, document and Server Action renders can also receive `rscSuffix=`, and collapsed fallback shells can expose values such as `shellPrefix=en`. These additions can overwrite user query parameters and cause unnecessary navigation retries. We fix this in `handleBuildComplete`, where Next.js generates these path-only helper captures. Unnamed captures and numbered destination references preserve the rewrite behavior without requiring adapters to recognize and filter Next.js's internal helper capture names. When using an adapter, we now emit unnamed captures for these path-only values. For example, `(?<shellPrefix>de|en)` and its destination reference `$shellPrefix` become `(de|en)` and `$1` when no locale capture precedes the group. Likewise, `(?<rscSuffix>...)` and `$rscSuffix` become `(...)` and a positional reference such as `$2` when one capture precedes the suffix. The generator accounts for parameter and locale captures and escapes literal base paths. Destination substitution runs once, preserving captured text and correctly distinguishing references such as `$1` and `$10`. Cache-bypass rules, route counts, and route ordering remain unchanged. User-supplied `rscSuffix` and `shellPrefix` remain valid query parameters. Standard `next dev` and `next start` routing, and Vercel's legacy deployment path, do not use these generated adapter routes, and thus are not affected. Verified by running the [full deploy test matrix](https://github.com/vercel/next.js/actions/runs/34835432358) against this PR. --- packages/next-routing/src/destination.ts | 54 +++++-- .../next/src/build/adapter/build-complete.ts | 51 +++++-- .../src/build/adapter/fallback-shell-runs.ts | 2 +- .../adapter-route-i18n.test.ts | 42 ++++++ test/e2e/adapter-route-i18n/next.config.ts | 11 ++ .../pages/legacy/[slug].tsx | 38 +++++ .../adapter-route-navigation.test.ts | 139 ++++++++++++++++++ .../app/[team]/[region]/hub/page.tsx | 33 +++++ .../app/[team]/[region]/layout.tsx | 39 +++++ .../[seventh]/[eighth]/[ninth]/page.tsx | 33 +++++ .../components/link-accordion.tsx | 21 +++ .../adapter-route-navigation/next.config.ts | 15 ++ .../adapter-rsc-query-leak.test.ts | 129 ++++++++++++++++ .../app/[locale]/layout.tsx | 9 ++ .../app/[locale]/posts/[slug]/page.tsx | 1 + .../adapter-rsc-query-leak/app/actions.ts | 8 + .../app/article/[slug]/page.tsx | 34 +++++ .../adapter-rsc-query-leak/app/draft/route.ts | 10 ++ .../adapter-rsc-query-leak/app/layout.tsx | 51 +++++++ .../adapter-rsc-query-leak/next.config.ts | 16 ++ .../next.config.js | 6 - ...-routes-and-interception-from-root.test.ts | 108 +++++++------- .../dynamic-routes-cache-components.test.ts | 32 ++-- .../dynamic-routes-legacy.test.ts | 20 +-- .../dynamic-routes-no-root-params.test.ts | 22 +-- ...namic-routes-shell-prefixes-single.test.ts | 8 +- .../dynamic-routes-shell-prefixes.test.ts | 10 +- 27 files changed, 810 insertions(+), 132 deletions(-) create mode 100644 test/e2e/adapter-route-i18n/adapter-route-i18n.test.ts create mode 100644 test/e2e/adapter-route-i18n/next.config.ts create mode 100644 test/e2e/adapter-route-i18n/pages/legacy/[slug].tsx create mode 100644 test/e2e/app-dir/adapter-route-navigation/adapter-route-navigation.test.ts create mode 100644 test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/hub/page.tsx create mode 100644 test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/layout.tsx create mode 100644 test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/many/[first]/[second]/[third]/[fourth]/[fifth]/[sixth]/[seventh]/[eighth]/[ninth]/page.tsx create mode 100644 test/e2e/app-dir/adapter-route-navigation/components/link-accordion.tsx create mode 100644 test/e2e/app-dir/adapter-route-navigation/next.config.ts create mode 100644 test/e2e/app-dir/adapter-rsc-query-leak/adapter-rsc-query-leak.test.ts create mode 100644 test/e2e/app-dir/adapter-rsc-query-leak/app/[locale]/layout.tsx create mode 100644 test/e2e/app-dir/adapter-rsc-query-leak/app/[locale]/posts/[slug]/page.tsx create mode 100644 test/e2e/app-dir/adapter-rsc-query-leak/app/actions.ts create mode 100644 test/e2e/app-dir/adapter-rsc-query-leak/app/article/[slug]/page.tsx create mode 100644 test/e2e/app-dir/adapter-rsc-query-leak/app/draft/route.ts create mode 100644 test/e2e/app-dir/adapter-rsc-query-leak/app/layout.tsx create mode 100644 test/e2e/app-dir/adapter-rsc-query-leak/next.config.ts delete mode 100644 test/e2e/app-dir/parallel-routes-and-interception-from-root/next.config.js diff --git a/packages/next-routing/src/destination.ts b/packages/next-routing/src/destination.ts index bfbabe98e472..6b434714d848 100644 --- a/packages/next-routing/src/destination.ts +++ b/packages/next-routing/src/destination.ts @@ -7,30 +7,54 @@ export function replaceDestination( regexMatches: RegExpMatchArray | null, hasCaptures: Record<string, string> ): string { - let result = destination + const captureKeys = Object.keys(hasCaptures) - // Replace numbered captures from regex ($1, $2, etc.) if (regexMatches) { - // Replace numbered groups (skip index 0 which is the full match) - for (let i = 1; i < regexMatches.length; i++) { - const value = regexMatches[i] ?? '' - result = result.replace(new RegExp(`\\$${i}`, 'g'), value) + for (let index = 1; index < regexMatches.length; index++) { + captureKeys.push(String(index)) } - - // Replace named groups ($name) if (regexMatches.groups) { - for (const [name, value] of Object.entries(regexMatches.groups)) { - result = result.replace(new RegExp(`\\$${name}`, 'g'), value ?? '') - } + captureKeys.push(...Object.keys(regexMatches.groups)) } } - // Replace named captures from has conditions - for (const [name, value] of Object.entries(hasCaptures)) { - result = result.replace(new RegExp(`\\$${name}`, 'g'), value) + if (captureKeys.length === 0) { + return destination } - return result + const capturePattern = captureKeys + .sort((first, second) => second.length - first.length) + .map((key) => { + const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + return /^\d+$/.test(key) ? `${escapedKey}(?!\\d)` : escapedKey + }) + .join('|') + + // Replace placeholders once. Captured paths can contain literal text such as + // $2 or $d$segment that must not become another substitution. + return destination.replace( + new RegExp(`\\$(${capturePattern})`, 'g'), + (placeholder, key: string) => { + if (regexMatches) { + const index = Number(key) + if ( + Number.isInteger(index) && + index > 0 && + index < regexMatches.length && + String(index) === key + ) { + return regexMatches[index] ?? '' + } + if (regexMatches.groups && Object.hasOwn(regexMatches.groups, key)) { + return regexMatches.groups[key] ?? '' + } + } + if (Object.hasOwn(hasCaptures, key)) { + return hasCaptures[key] + } + return placeholder + } + ) } /** diff --git a/packages/next/src/build/adapter/build-complete.ts b/packages/next/src/build/adapter/build-complete.ts index 38925acdd3d6..ec5af1cdd40a 100644 --- a/packages/next/src/build/adapter/build-complete.ts +++ b/packages/next/src/build/adapter/build-complete.ts @@ -2110,6 +2110,10 @@ export async function handleBuildComplete({ (page) => prerenderManifest.dynamicRoutes[page]?.fallback === false ) : undefined + const escapedBasePath = + config.basePath && config.basePath !== '/' + ? escapeStringRegexp(path.posix.join('/', config.basePath)) + : '' for (const route of routesManifest.dynamicRoutes) { // An earlier entry in this loop serves this shell. @@ -2133,6 +2137,10 @@ export async function handleBuildComplete({ // An entry for a whole run of shells matches every prefix in that run. // The destination copies the prefix that matched. // + // The prefix and RSC suffix use unnamed captures. Adapters can forward + // named captures to the application query when they bypass prerendered + // output. + // // This replacement runs on the pattern for the page, and `sourceRegex` // below prefixes the result with the base path and the locale group. That // order is deliberate. The search text anchors at `^`, and here that @@ -2143,18 +2151,24 @@ export async function handleBuildComplete({ const pagePattern = fallbackShellRun ? routeRegex.namedRegex.replace( `^/${escapeStringRegexp(fallbackShellRun.prefixes[0])}/`, - `^/(?<shellPrefix>${fallbackShellRun.prefixes - .map((prefix) => escapeStringRegexp(prefix)) - .join('|')})/` + () => + `^/(${fallbackShellRun.prefixes + .map((prefix) => escapeStringRegexp(prefix)) + .join('|')})/` ) : routeRegex.namedRegex const pagePath = fallbackShellRun - ? path.posix.join('/', '$shellPrefix', fallbackShellRun.tail) + ? path.posix.join( + '/', + shouldLocalize ? '$2' : '$1', + fallbackShellRun.tail + ) : route.page const sourceRegex = pagePattern.replace( '^', - `^${config.basePath && config.basePath !== '/' ? path.posix.join('/', config.basePath || '') : ''}[/]?${shouldLocalize ? '(?<nextLocale>[^/]{1,})' : ''}` + () => + `^${escapedBasePath}[/]?${shouldLocalize ? '(?<nextLocale>[^/]{1,})' : ''}` ) const destination = path.posix.join( @@ -2163,6 +2177,13 @@ export async function handleBuildComplete({ shouldLocalize ? '/$nextLocale' : '', pagePath ) + getDestinationQuery(route.routeKeys) + // Count capture names, not parameter names. An interception route can + // capture the same parameter with both nxtP and nxtI names. + const suffixCaptureIndex = + Object.keys(routeRegex.routeKeys).length + + (shouldLocalize ? 1 : 0) + + (fallbackShellRun ? 1 : 0) + + 1 const hasAppPages = Boolean(appPageKeys && appPageKeys.length > 0) @@ -2196,15 +2217,18 @@ export async function handleBuildComplete({ // An optional group is unsafe here. An adapter, or the router that // consumes its output, can resolve the placeholders in a destination // from the match result rather than from the pattern. A group that does - // not match is then absent from that result, and the literal text - // `$rscSuffix` stays in the destination. + // not match is then absent from that result, and the destination + // placeholder stays unresolved. dynamicRoutes.push({ source: pagePath, sourceRegex: sourceRegex.replace( new RegExp(escapeStringRegexp('(?:/)?$')), - '(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$' + '(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$' + ), + destination: destination.replace( + /($|\?)/, + (separator) => `$${suffixCaptureIndex}${separator}` ), - destination: destination?.replace(/($|\?)/, '$rscSuffix$1'), has: plainHas, missing: undefined, }) @@ -2218,9 +2242,12 @@ export async function handleBuildComplete({ source: pagePath + '.rsc', sourceRegex: sourceRegex.replace( new RegExp(escapeStringRegexp('(?:/)?$')), - '(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc)(?:/)?$' + '(\\.rsc|\\.segments/.+\\.segment\\.rsc)(?:/)?$' + ), + destination: destination.replace( + /($|\?)/, + (separator) => `$${suffixCaptureIndex}${separator}` ), - destination: destination?.replace(/($|\?)/, '$rscSuffix$1'), has: suffixedHas, missing: undefined, }) @@ -2246,7 +2273,7 @@ export async function handleBuildComplete({ source: route.page, sourceRegex: segmentRoute.source.replace( '^', - `^${config.basePath && config.basePath !== '/' ? path.posix.join('/', config.basePath || '') : ''}[/]?` + () => `^${escapedBasePath}[/]?` ), destination: path.posix.join( '/', diff --git a/packages/next/src/build/adapter/fallback-shell-runs.ts b/packages/next/src/build/adapter/fallback-shell-runs.ts index 89942220a6e5..d93351c29dfe 100644 --- a/packages/next/src/build/adapter/fallback-shell-runs.ts +++ b/packages/next/src/build/adapter/fallback-shell-runs.ts @@ -157,7 +157,7 @@ function splitShellPage( * with shells for `acme/en`, `acme/de` and `globex/en`, that pattern holds: * * ``` - * (?<shellPrefix>acme/en|acme/de|globex/en) + * (acme/en|acme/de|globex/en) * ``` * * A pattern that offered a choice per param instead, such as diff --git a/test/e2e/adapter-route-i18n/adapter-route-i18n.test.ts b/test/e2e/adapter-route-i18n/adapter-route-i18n.test.ts new file mode 100644 index 000000000000..dee95d205f6e --- /dev/null +++ b/test/e2e/adapter-route-i18n/adapter-route-i18n.test.ts @@ -0,0 +1,42 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('adapter-route-i18n', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it.each([ + { prefix: '', locale: 'en' }, + { prefix: '/fr', locale: 'fr' }, + ])( + 'renders the $locale locale with a base path', + async ({ prefix, locale }) => { + const browser = await next.browser( + `/base${prefix}/legacy/one?term=kept`, + { permissions: [] } + ) + expect(await browser.elementById(`${locale}-one`).text()).toBe( + `${locale}:one` + ) + expect(JSON.parse(await browser.elementById('query').text())).toEqual({ + slug: 'one', + term: 'kept', + }) + } + ) + + it('navigates to an explicit locale without reloading the document', async () => { + const browser = await next.browser('/base/legacy/one', { + permissions: [], + }) + await browser.eval('window.__testDocument = "retained"') + await browser.elementById('next-page').click() + expect(await browser.elementById('fr-two').text()).toBe('fr:two') + expect(JSON.parse(await browser.elementById('query').text())).toEqual({ + slug: 'two', + term: 'next', + }) + expect(new URL(await browser.url()).pathname).toBe('/base/fr/legacy/two') + expect(await browser.eval('window.__testDocument')).toBe('retained') + }) +}) diff --git a/test/e2e/adapter-route-i18n/next.config.ts b/test/e2e/adapter-route-i18n/next.config.ts new file mode 100644 index 000000000000..729c70b0d3e6 --- /dev/null +++ b/test/e2e/adapter-route-i18n/next.config.ts @@ -0,0 +1,11 @@ +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + basePath: '/base', + cacheComponents: false, + partialPrefetching: false, + experimental: { cachedNavigations: false }, + i18n: { locales: ['en', 'fr'], defaultLocale: 'en', localeDetection: false }, +} + +export default nextConfig diff --git a/test/e2e/adapter-route-i18n/pages/legacy/[slug].tsx b/test/e2e/adapter-route-i18n/pages/legacy/[slug].tsx new file mode 100644 index 000000000000..203fbc3b05a1 --- /dev/null +++ b/test/e2e/adapter-route-i18n/pages/legacy/[slug].tsx @@ -0,0 +1,38 @@ +import Link from 'next/link' +import type { GetServerSideProps, InferGetServerSidePropsType } from 'next' +import type { ParsedUrlQuery } from 'querystring' + +export const getServerSideProps: GetServerSideProps<{ + slug: string + locale: string + query: ParsedUrlQuery +}> = async ({ params, locale, query }) => { + if (typeof params?.slug !== 'string') { + return { notFound: true } + } + if (locale === undefined) { + throw new Error('Expected a configured locale') + } + return { props: { slug: params.slug, locale, query } } +} + +export default function Page({ + slug, + locale, + query, +}: InferGetServerSidePropsType<typeof getServerSideProps>) { + return ( + <> + <p id={`${locale}-${slug}`}>{`${locale}:${slug}`}</p> + <pre id="query">{JSON.stringify(query)}</pre> + <Link + id="next-page" + href="/legacy/two?term=next" + locale="fr" + prefetch={false} + > + French page + </Link> + </> + ) +} diff --git a/test/e2e/app-dir/adapter-route-navigation/adapter-route-navigation.test.ts b/test/e2e/app-dir/adapter-route-navigation/adapter-route-navigation.test.ts new file mode 100644 index 000000000000..549cca61ac9d --- /dev/null +++ b/test/e2e/app-dir/adapter-route-navigation/adapter-route-navigation.test.ts @@ -0,0 +1,139 @@ +import { nextTestSetup } from 'e2e-utils' +import { createRouterAct } from 'router-act' + +const basePath = '/base' +const routePath = '/many/one/two/three/four/five/six/seven/eight' +const rootParameters = [ + { team: 'acme', region: 'east' }, + { team: 'cash$2', region: 'east' }, + { team: 'cash$3', region: 'east' }, + { team: 'sparse', region: 'west' }, +] +const parameters = { + first: 'one', + second: 'two', + third: 'three', + fourth: 'four', + fifth: 'five', + sixth: 'six', + seventh: 'seven', + eighth: 'eight', + ninth: 'nine', +} + +describe.each([false, true])( + 'adapter route navigation, collapseAdapterRoutes: %s', + (collapseAdapterRoutes) => { + const { next } = nextTestSetup({ + files: __dirname, + env: { TEST_COLLAPSE_ADAPTER_ROUTES: String(collapseAdapterRoutes) }, + }) + + async function startBrowser(pathname: string) { + let act: ReturnType<typeof createRouterAct> | undefined + const browser = await next.browser(`${basePath}${pathname}`, { + permissions: [], + beforePageLoad(page) { + act = createRouterAct(page, { includeAppShellRequests: true }) + }, + }) + if (act === undefined) { + throw new Error('Router act was not initialized') + } + await browser.eval('window.__testDocument = "retained"') + return { browser, act } + } + + it.each(rootParameters)( + 'renders every parameter on a direct visit within $team/$region', + async ({ team, region }) => { + const { browser } = await startBrowser( + `/${team}/${region}${routePath}/nine?term=kept` + ) + expect( + JSON.parse(await browser.elementById('parameters').text()) + ).toEqual({ + team: encodeURIComponent(team), + region: encodeURIComponent(region), + ...parameters, + }) + expect(JSON.parse(await browser.elementById('query').text())).toEqual({ + term: 'kept', + }) + } + ) + + it.each(rootParameters)( + 'navigates within $team/$region without prefetching', + async ({ team, region }) => { + const prefix = `/${team}/${region}` + // App Router passes encoded parameter values to components. + const expectedParameters = { + team: encodeURIComponent(team), + region: encodeURIComponent(region), + ...parameters, + } + const { browser, act } = await startBrowser(`${prefix}/hub`) + expect(await browser.elementById('root-params').text()).toBe( + `${expectedParameters.team}:${expectedParameters.region}` + ) + const href = `/${expectedParameters.team}/${expectedParameters.region}${routePath}/nine?term=kept` + await act( + async () => { + await browser.elementByCss(`a[href="${basePath}${href}"]`).click() + }, + { includes: 'Article nine' } + ) + expect( + JSON.parse(await browser.elementById('parameters').text()) + ).toEqual(expectedParameters) + expect(JSON.parse(await browser.elementById('query').text())).toEqual({ + term: 'kept', + }) + expect(decodeURIComponent(new URL(await browser.url()).pathname)).toBe( + `${basePath}${prefix}${routePath}/nine` + ) + expect(await browser.eval('window.__testDocument')).toBe('retained') + } + ) + + // @force-gate prefetching + it('prefetches page content before navigation', async () => { + const { browser, act } = await startBrowser('/acme/east/hub') + const href = `/acme/east${routePath}/prefetched?term=kept` + await act( + async () => { + await browser + .elementByCss(`input[data-link-accordion="${href}"]`) + .click() + }, + { includes: 'Many parameters' } + ) + await act( + async () => { + await browser.elementByCss(`a[href="${basePath}${href}"]`).click() + }, + { includes: 'Article prefetched' } + ) + expect( + JSON.parse(await browser.elementById('parameters').text()) + ).toEqual({ + team: 'acme', + region: 'east', + ...parameters, + ninth: 'prefetched', + }) + expect(JSON.parse(await browser.elementById('query').text())).toEqual({ + term: 'kept', + }) + expect(await browser.eval('window.__testDocument')).toBe('retained') + }) + + it('returns 404 outside the base path', async () => { + for (const invalidBasePath of ['/base-other', '/bas', '/baseX']) { + const response = await next.fetch(`${invalidBasePath}/acme/east/hub`) + expect(response.status).toBe(404) + } + }) + } +) diff --git a/test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/hub/page.tsx b/test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/hub/page.tsx new file mode 100644 index 000000000000..9ed98ed0ed9f --- /dev/null +++ b/test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/hub/page.tsx @@ -0,0 +1,33 @@ +import { Suspense } from 'react' +import Link from 'next/link' +import { LinkAccordion } from '../../../../components/link-accordion' + +async function Links({ + params, +}: { + params: Promise<{ team: string; region: string }> +}) { + const { team, region } = await params + const prefix = `/${team}/${region}/many/one/two/three/four/five/six/seven/eight` + + return ( + <> + <Link href={`${prefix}/nine?term=kept`} prefetch={false}> + Navigate without prefetching + </Link> + <LinkAccordion href={`${prefix}/prefetched?term=kept`} /> + </> + ) +} + +export default function Page({ + params, +}: { + params: Promise<{ team: string; region: string }> +}) { + return ( + <Suspense fallback={<p>Loading links</p>}> + <Links params={params} /> + </Suspense> + ) +} diff --git a/test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/layout.tsx b/test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/layout.tsx new file mode 100644 index 000000000000..d5eee133695b --- /dev/null +++ b/test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/layout.tsx @@ -0,0 +1,39 @@ +import { Suspense, type ReactNode } from 'react' + +export function generateStaticParams() { + return [ + { team: 'acme', region: 'east' }, + { team: 'acme', region: 'west' }, + { team: 'sparse', region: 'east' }, + { team: 'cash$2', region: 'east' }, + { team: 'cash$3', region: 'east' }, + ] +} + +async function RootParams({ + params, +}: { + params: Promise<{ team: string; region: string }> +}) { + const { team, region } = await params + return <p id="root-params">{`${team}:${region}`}</p> +} + +export default function Root({ + children, + params, +}: { + children: ReactNode + params: Promise<{ team: string; region: string }> +}) { + return ( + <html> + <body> + <Suspense fallback={<p>Loading root parameters</p>}> + <RootParams params={params} /> + </Suspense> + {children} + </body> + </html> + ) +} diff --git a/test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/many/[first]/[second]/[third]/[fourth]/[fifth]/[sixth]/[seventh]/[eighth]/[ninth]/page.tsx b/test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/many/[first]/[second]/[third]/[fourth]/[fifth]/[sixth]/[seventh]/[eighth]/[ninth]/page.tsx new file mode 100644 index 000000000000..99f8c007c75e --- /dev/null +++ b/test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/many/[first]/[second]/[third]/[fourth]/[fifth]/[sixth]/[seventh]/[eighth]/[ninth]/page.tsx @@ -0,0 +1,33 @@ +import { Suspense } from 'react' + +// This page tests routing, not an instant-navigation guarantee. +export const instant = false + +type Props = { + params: Promise<Record<string, string>> + searchParams: Promise<Record<string, string | string[] | undefined>> +} + +async function Content({ params, searchParams }: Props) { + const parameters = await params + const query = await searchParams + + return ( + <> + <p id="article">{`Article ${parameters.ninth}`}</p> + <pre id="parameters">{JSON.stringify(parameters)}</pre> + <pre id="query">{JSON.stringify(query)}</pre> + </> + ) +} + +export default function Page(props: Props) { + return ( + <> + <h1>Many parameters</h1> + <Suspense fallback={<p>Loading article</p>}> + <Content {...props} /> + </Suspense> + </> + ) +} diff --git a/test/e2e/app-dir/adapter-route-navigation/components/link-accordion.tsx b/test/e2e/app-dir/adapter-route-navigation/components/link-accordion.tsx new file mode 100644 index 000000000000..3878ec8c7881 --- /dev/null +++ b/test/e2e/app-dir/adapter-route-navigation/components/link-accordion.tsx @@ -0,0 +1,21 @@ +'use client' + +import Link from 'next/link' +import { useState } from 'react' + +export function LinkAccordion({ href }: { href: string }) { + const [visible, setVisible] = useState(false) + + return ( + <> + <input + type="checkbox" + aria-label={`Show ${href}`} + data-link-accordion={href} + checked={visible} + onChange={() => setVisible(!visible)} + /> + {visible ? <Link href={href}>Open article</Link> : null} + </> + ) +} diff --git a/test/e2e/app-dir/adapter-route-navigation/next.config.ts b/test/e2e/app-dir/adapter-route-navigation/next.config.ts new file mode 100644 index 000000000000..a9f4e2b78c61 --- /dev/null +++ b/test/e2e/app-dir/adapter-route-navigation/next.config.ts @@ -0,0 +1,15 @@ +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + basePath: '/base', + cacheComponents: true, + partialPrefetching: false, + experimental: { + cachedNavigations: true, + optimisticRouting: true, + prefetchInlining: false, + collapseAdapterRoutes: process.env.TEST_COLLAPSE_ADAPTER_ROUTES === 'true', + }, +} + +export default nextConfig diff --git a/test/e2e/app-dir/adapter-rsc-query-leak/adapter-rsc-query-leak.test.ts b/test/e2e/app-dir/adapter-rsc-query-leak/adapter-rsc-query-leak.test.ts new file mode 100644 index 000000000000..8178331318c7 --- /dev/null +++ b/test/e2e/app-dir/adapter-rsc-query-leak/adapter-rsc-query-leak.test.ts @@ -0,0 +1,129 @@ +import { nextTestSetup } from 'e2e-utils' +import { createRouterAct } from 'router-act' + +describe.each([false, true])( + 'adapter query parameters, collapseAdapterRoutes: %s', + (collapseAdapterRoutes) => { + const { next } = nextTestSetup({ + files: __dirname, + env: { + TEST_COLLAPSE_ADAPTER_ROUTES: String(collapseAdapterRoutes), + }, + }) + + async function startBrowser(url: string) { + let act: ReturnType<typeof createRouterAct> | undefined + const browser = await next.browser(url, { + permissions: [], + beforePageLoad(page) { + act = createRouterAct(page, { includeAppShellRequests: true }) + }, + }) + if (act === undefined) { + throw new Error('Router act was not initialized') + } + return { browser, act } + } + + describe.each([ + { name: 'no search parameters', search: '', searchParams: {} }, + { + name: 'user search parameters', + search: + '?rscSuffix=first&rscSuffix=second&shellPrefix=user&term=example', + searchParams: { + rscSuffix: ['first', 'second'], + shellPrefix: 'user', + term: 'example', + }, + }, + ])('$name', ({ search, searchParams }) => { + it.each([false, true])( + 'preserves search parameters in the document, draft mode: %s', + async (draft) => { + const { browser } = await startBrowser( + `${draft ? '/draft' : '/article/one'}${search}` + ) + expect(await browser.elementById('article').text()).toBe( + `${draft ? 'Draft' : 'Published'} article one` + ) + expect(JSON.parse(await browser.elementById('query').text())).toEqual( + searchParams + ) + } + ) + + it.each([false, true])( + 'preserves search parameters without retrying navigation, draft mode: %s', + async (draft) => { + const { browser, act } = await startBrowser( + draft ? '/draft' : '/article/one' + ) + const href = `/article/two${search}` + await act( + async () => { + await browser.elementByCss(`a[href="${href}"]`).click() + }, + { includes: `${draft ? 'Draft' : 'Published'} article two` } + ) + expect(await browser.elementById('article').text()).toBe( + `${draft ? 'Draft' : 'Published'} article two` + ) + expect(JSON.parse(await browser.elementById('query').text())).toEqual( + searchParams + ) + expect(new URL(await browser.url()).pathname).toBe('/article/two') + } + ) + + it('preserves search parameters after a non-draft Server Action', async () => { + const { browser, act } = await startBrowser(`/article/one${search}`) + await act(async () => { + await browser.elementById('update-cookie').click() + }) + expect(await browser.elementById('shared-render').text()).toBe( + 'Cookie updated' + ) + expect(await browser.elementById('article').text()).toBe( + 'Published article one' + ) + expect(JSON.parse(await browser.elementById('query').text())).toEqual( + searchParams + ) + }) + + it.each(['en', 'de'])( + 'preserves search parameters in a fallback shell document for %s', + async (locale) => { + const { browser } = await startBrowser( + `/${locale}/posts/one${search}` + ) + expect(await browser.elementById('article').text()).toBe( + 'Published article one' + ) + expect(JSON.parse(await browser.elementById('query').text())).toEqual( + searchParams + ) + } + ) + + it('preserves search parameters when navigating through a fallback shell', async () => { + const { browser, act } = await startBrowser('/en/posts/one') + const href = `/en/posts/two${search}` + await act( + async () => { + await browser.elementByCss(`a[href="${href}"]`).click() + }, + { includes: 'Published article two' } + ) + expect(await browser.elementById('article').text()).toBe( + 'Published article two' + ) + expect(JSON.parse(await browser.elementById('query').text())).toEqual( + searchParams + ) + expect(new URL(await browser.url()).pathname).toBe('/en/posts/two') + }) + }) + } +) diff --git a/test/e2e/app-dir/adapter-rsc-query-leak/app/[locale]/layout.tsx b/test/e2e/app-dir/adapter-rsc-query-leak/app/[locale]/layout.tsx new file mode 100644 index 000000000000..91388b20dc29 --- /dev/null +++ b/test/e2e/app-dir/adapter-rsc-query-leak/app/[locale]/layout.tsx @@ -0,0 +1,9 @@ +import type { ReactNode } from 'react' + +export function generateStaticParams() { + return [{ locale: 'en' }, { locale: 'de' }] +} + +export default function Layout({ children }: { children: ReactNode }) { + return children +} diff --git a/test/e2e/app-dir/adapter-rsc-query-leak/app/[locale]/posts/[slug]/page.tsx b/test/e2e/app-dir/adapter-rsc-query-leak/app/[locale]/posts/[slug]/page.tsx new file mode 100644 index 000000000000..8a4c914ac14b --- /dev/null +++ b/test/e2e/app-dir/adapter-rsc-query-leak/app/[locale]/posts/[slug]/page.tsx @@ -0,0 +1 @@ +export { default } from '../../../article/[slug]/page' diff --git a/test/e2e/app-dir/adapter-rsc-query-leak/app/actions.ts b/test/e2e/app-dir/adapter-rsc-query-leak/app/actions.ts new file mode 100644 index 000000000000..9ea81ee2a12c --- /dev/null +++ b/test/e2e/app-dir/adapter-rsc-query-leak/app/actions.ts @@ -0,0 +1,8 @@ +'use server' + +import { cookies } from 'next/headers' + +export async function updateCookie() { + const cookieStore = await cookies() + cookieStore.set('query-test', 'set') +} diff --git a/test/e2e/app-dir/adapter-rsc-query-leak/app/article/[slug]/page.tsx b/test/e2e/app-dir/adapter-rsc-query-leak/app/article/[slug]/page.tsx new file mode 100644 index 000000000000..555c3e1ba319 --- /dev/null +++ b/test/e2e/app-dir/adapter-rsc-query-leak/app/article/[slug]/page.tsx @@ -0,0 +1,34 @@ +import { Suspense } from 'react' +import { draftMode } from 'next/headers' +import { connection } from 'next/server' + +type Props = { + params: Promise<{ slug: string }> + searchParams: Promise<Record<string, string | string[] | undefined>> +} + +export function generateStaticParams() { + return [{ slug: 'built' }] +} + +async function Content({ params, searchParams }: Props) { + await connection() + const { slug } = await params + const query = await searchParams + const { isEnabled } = await draftMode() + + return ( + <> + <p id="article">{`${isEnabled ? 'Draft' : 'Published'} article ${slug}`}</p> + <p id="query">{JSON.stringify(query)}</p> + </> + ) +} + +export default function Page(props: Props) { + return ( + <Suspense fallback={<p>Loading article</p>}> + <Content {...props} /> + </Suspense> + ) +} diff --git a/test/e2e/app-dir/adapter-rsc-query-leak/app/draft/route.ts b/test/e2e/app-dir/adapter-rsc-query-leak/app/draft/route.ts new file mode 100644 index 000000000000..e7f2bd16310a --- /dev/null +++ b/test/e2e/app-dir/adapter-rsc-query-leak/app/draft/route.ts @@ -0,0 +1,10 @@ +import { draftMode } from 'next/headers' +import { NextResponse, type NextRequest } from 'next/server' + +export async function GET(request: NextRequest) { + const draft = await draftMode() + draft.enable() + const destination = new URL('/article/one', request.url) + destination.search = request.nextUrl.search + return NextResponse.redirect(destination) +} diff --git a/test/e2e/app-dir/adapter-rsc-query-leak/app/layout.tsx b/test/e2e/app-dir/adapter-rsc-query-leak/app/layout.tsx new file mode 100644 index 000000000000..dae2a8746756 --- /dev/null +++ b/test/e2e/app-dir/adapter-rsc-query-leak/app/layout.tsx @@ -0,0 +1,51 @@ +import { Suspense, type ReactNode } from 'react' +import Link from 'next/link' +import { cookies } from 'next/headers' +import { connection } from 'next/server' +import { updateCookie } from './actions' + +async function SharedContent() { + await connection() + const cookieStore = await cookies() + return ( + <p id="shared-render"> + {cookieStore.has('query-test') ? 'Cookie updated' : 'Cookie absent'} + </p> + ) +} + +export default function Root({ children }: { children: ReactNode }) { + return ( + <html> + <body> + <Suspense fallback={<p>Loading shared content</p>}> + <SharedContent /> + </Suspense> + <nav> + <Link href="/article/two" prefetch={false}> + Article two + </Link> + <Link + href="/article/two?rscSuffix=first&rscSuffix=second&shellPrefix=user&term=example" + prefetch={false} + > + Article two with search parameters + </Link> + <Link href="/en/posts/two" prefetch={false}> + English article two + </Link> + <Link + href="/en/posts/two?rscSuffix=first&rscSuffix=second&shellPrefix=user&term=example" + prefetch={false} + > + English article two with search parameters + </Link> + </nav> + <form action={updateCookie}> + <button id="update-cookie">Update an ordinary cookie</button> + </form> + {children} + </body> + </html> + ) +} diff --git a/test/e2e/app-dir/adapter-rsc-query-leak/next.config.ts b/test/e2e/app-dir/adapter-rsc-query-leak/next.config.ts new file mode 100644 index 000000000000..86b2e93e41b5 --- /dev/null +++ b/test/e2e/app-dir/adapter-rsc-query-leak/next.config.ts @@ -0,0 +1,16 @@ +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + cacheComponents: true, + partialPrefetching: false, + // Exercise the prerender bypass with ordinary browser requests, not only + // draft mode. + htmlLimitedBots: /.*/, + experimental: { + cachedNavigations: true, + optimisticRouting: true, + collapseAdapterRoutes: process.env.TEST_COLLAPSE_ADAPTER_ROUTES === 'true', + }, +} + +export default nextConfig diff --git a/test/e2e/app-dir/parallel-routes-and-interception-from-root/next.config.js b/test/e2e/app-dir/parallel-routes-and-interception-from-root/next.config.js deleted file mode 100644 index 807126e4cf0b..000000000000 --- a/test/e2e/app-dir/parallel-routes-and-interception-from-root/next.config.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * @type {import('next').NextConfig} - */ -const nextConfig = {} - -module.exports = nextConfig diff --git a/test/e2e/app-dir/parallel-routes-and-interception-from-root/parallel-routes-and-interception-from-root.test.ts b/test/e2e/app-dir/parallel-routes-and-interception-from-root/parallel-routes-and-interception-from-root.test.ts index 5fedd25f98eb..76fbabc835e8 100644 --- a/test/e2e/app-dir/parallel-routes-and-interception-from-root/parallel-routes-and-interception-from-root.test.ts +++ b/test/e2e/app-dir/parallel-routes-and-interception-from-root/parallel-routes-and-interception-from-root.test.ts @@ -1,57 +1,61 @@ import { nextTestSetup } from 'e2e-utils' import { retry } from 'next-test-utils' -describe('parallel-routes-and-interception-from-root', () => { - const { next, isNextDeploy } = nextTestSetup({ - files: __dirname, - }) - - it('should interpolate [locale] in "/[locale]/example/(...)[locale]/intercepted"', async () => { - const browser = await next.browser('/en/example') - - expect(await browser.elementByCss('h1').text()).toBe('Example Page') - expect(await browser.elementByCss('p').text()).toBe('Locale: en') - - if (!isNextDeploy) { - expect(next.cliOutput).toInclude('RootLayout rendered, locale: en') - } - - // Referenced by commented out assertion below, see TODO message - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const cliOutputLength = next.cliOutput.length - - await browser.elementByCss('a').click() - - await retry(async () => { - expect(await browser.elementByCss('h2').text()).toBe( - 'Page intercepted from root' - ) +describe.each([false, true])( + 'parallel-routes-and-interception-from-root, collapseAdapterRoutes: %s', + (collapseAdapterRoutes) => { + const { next, isNextDeploy } = nextTestSetup({ + files: __dirname, + nextConfig: { experimental: { collapseAdapterRoutes } }, }) - // Ensure that the locale is still correctly rendered in the root layout. - expect(await browser.elementByCss('p').text()).toBe('Locale: en') - - // ...and that the root layout was not rerendered. - if (!isNextDeploy) { - // FIXME: This assertion is temporarily disabled. Clicking the link should - // not re-render the root layout. This is happening because the response - // includes extra search params in the page segment that shouldn't be - // there: "__PAGE__?{\"locale":\"en\"}" instead of "__PAGE__". On the - // surface, it looks like the route params are accidentally being treated - // as search params. - // - // This assertion used to pass despite the mismatch, because client was - // more permissive about validating the tree when receiving a dynamic - // response from the server. But now we intentionally compare all the - // segments, including the search params. - // - // Regardless, we need to fix whatever's causing the params to be treated - // as search params. - // - // Correct behavior: - // expect(next.cliOutput.slice(cliOutputLength)).not.toInclude( - // 'RootLayout rendered, locale: en' - // ) - } - }) -}) + it('should interpolate [locale] in "/[locale]/example/(...)[locale]/intercepted"', async () => { + const browser = await next.browser('/en/example') + + expect(await browser.elementByCss('h1').text()).toBe('Example Page') + expect(await browser.elementByCss('p').text()).toBe('Locale: en') + + if (!isNextDeploy) { + expect(next.cliOutput).toInclude('RootLayout rendered, locale: en') + } + + // Referenced by commented out assertion below, see TODO message + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const cliOutputLength = next.cliOutput.length + + await browser.elementByCss('a').click() + + await retry(async () => { + expect(await browser.elementByCss('h2').text()).toBe( + 'Page intercepted from root' + ) + }) + + // Ensure that the locale is still correctly rendered in the root layout. + expect(await browser.elementByCss('p').text()).toBe('Locale: en') + + // ...and that the root layout was not rerendered. + if (!isNextDeploy) { + // FIXME: This assertion is temporarily disabled. Clicking the link should + // not re-render the root layout. This is happening because the response + // includes extra search params in the page segment that shouldn't be + // there: "__PAGE__?{\"locale":\"en\"}" instead of "__PAGE__". On the + // surface, it looks like the route params are accidentally being treated + // as search params. + // + // This assertion used to pass despite the mismatch, because client was + // more permissive about validating the tree when receiving a dynamic + // response from the server. But now we intentionally compare all the + // segments, including the search params. + // + // Regardless, we need to fix whatever's causing the params to be treated + // as search params. + // + // Correct behavior: + // expect(next.cliOutput.slice(cliOutputLength)).not.toInclude( + // 'RootLayout rendered, locale: en' + // ) + } + }) + } +) diff --git a/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-cache-components.test.ts b/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-cache-components.test.ts index 6999f9be4de9..68d9080aacb0 100644 --- a/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-cache-components.test.ts +++ b/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-cache-components.test.ts @@ -44,32 +44,32 @@ describe('adapter dynamic routes (cache components)', () => { "7 entries /[lang] - ^[/]?/(?<nxtPlang>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /[lang]$rscSuffix?nxtPlang=$nxtPlang + ^[/]?/(?<nxtPlang>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /[lang]$2?nxtPlang=$nxtPlang - /$shellPrefix/fallback-shell/[slug] - ^[/]?/(?<shellPrefix>de|en)/fallback\\-shell/(?<nxtPslug>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /$shellPrefix/fallback-shell/[slug]$rscSuffix?nxtPslug=$nxtPslug + /$1/fallback-shell/[slug] + ^[/]?/(de|en)/fallback\\-shell/(?<nxtPslug>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /$1/fallback-shell/[slug]$3?nxtPslug=$nxtPslug /[lang]/fallback-shell/[slug] - ^[/]?/(?<nxtPlang>[^/]+?)/fallback\\-shell/(?<nxtPslug>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /[lang]/fallback-shell/[slug]$rscSuffix?nxtPlang=$nxtPlang&nxtPslug=$nxtPslug + ^[/]?/(?<nxtPlang>[^/]+?)/fallback\\-shell/(?<nxtPslug>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /[lang]/fallback-shell/[slug]$3?nxtPlang=$nxtPlang&nxtPslug=$nxtPslug /[lang]/ppr - ^[/]?/(?<nxtPlang>[^/]+?)/ppr(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /[lang]/ppr$rscSuffix?nxtPlang=$nxtPlang + ^[/]?/(?<nxtPlang>[^/]+?)/ppr(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /[lang]/ppr$2?nxtPlang=$nxtPlang /[lang]/static - ^[/]?/(?<nxtPlang>[^/]+?)/static(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /[lang]/static$rscSuffix?nxtPlang=$nxtPlang + ^[/]?/(?<nxtPlang>[^/]+?)/static(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /[lang]/static$2?nxtPlang=$nxtPlang - /$shellPrefix/[slug] - ^[/]?/(?<shellPrefix>de|en)/(?<nxtPslug>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /$shellPrefix/[slug]$rscSuffix?nxtPslug=$nxtPslug + /$1/[slug] + ^[/]?/(de|en)/(?<nxtPslug>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /$1/[slug]$3?nxtPslug=$nxtPslug /[lang]/[slug] - ^[/]?/(?<nxtPlang>[^/]+?)/(?<nxtPslug>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /[lang]/[slug]$rscSuffix?nxtPlang=$nxtPlang&nxtPslug=$nxtPslug" + ^[/]?/(?<nxtPlang>[^/]+?)/(?<nxtPslug>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /[lang]/[slug]$3?nxtPlang=$nxtPlang&nxtPslug=$nxtPslug" `) }) }) diff --git a/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-legacy.test.ts b/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-legacy.test.ts index 445ec8127724..ed1cae823dae 100644 --- a/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-legacy.test.ts +++ b/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-legacy.test.ts @@ -56,24 +56,24 @@ describe('adapter dynamic routes (legacy)', () => { -> /static-two /blog/[slug] - ^[/]?/blog/(?<nxtPslug>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /blog/[slug]$rscSuffix?nxtPslug=$nxtPslug + ^[/]?/blog/(?<nxtPslug>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /blog/[slug]$2?nxtPslug=$nxtPslug /docs/[lang]/accounts - ^[/]?/docs/(?<nxtPlang>[^/]+?)/accounts(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /docs/[lang]/accounts$rscSuffix?nxtPlang=$nxtPlang + ^[/]?/docs/(?<nxtPlang>[^/]+?)/accounts(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /docs/[lang]/accounts$2?nxtPlang=$nxtPlang /docs/[lang]/functions - ^[/]?/docs/(?<nxtPlang>[^/]+?)/functions(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /docs/[lang]/functions$rscSuffix?nxtPlang=$nxtPlang + ^[/]?/docs/(?<nxtPlang>[^/]+?)/functions(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /docs/[lang]/functions$2?nxtPlang=$nxtPlang /docs/[lang]/guide - ^[/]?/docs/(?<nxtPlang>[^/]+?)/guide(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /docs/[lang]/guide$rscSuffix?nxtPlang=$nxtPlang + ^[/]?/docs/(?<nxtPlang>[^/]+?)/guide(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /docs/[lang]/guide$2?nxtPlang=$nxtPlang /legacy/[id].rsc - ^[/]?/legacy/(?<nxtPid>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc)(?:/)?$ - -> /legacy/[id]$rscSuffix?nxtPid=$nxtPid + ^[/]?/legacy/(?<nxtPid>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc)(?:/)?$ + -> /legacy/[id]$2?nxtPid=$nxtPid /legacy/[id] ^[/]?/legacy/(?<nxtPid>[^/]+?)(?:/)?$ diff --git a/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-no-root-params.test.ts b/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-no-root-params.test.ts index 2cc553a0d35c..971c25febbbd 100644 --- a/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-no-root-params.test.ts +++ b/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-no-root-params.test.ts @@ -44,25 +44,25 @@ describe('adapter dynamic routes (no root params)', () => { .toMatchInlineSnapshot(` "5 entries - /$shellPrefix/posts/[id] - ^[/]?/(?<shellPrefix>acme\\.one\\-two,three/de|acme\\.one\\-two,three/en)/posts/(?<nxtPid>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /$shellPrefix/posts/[id]$rscSuffix?nxtPid=$nxtPid + /$1/posts/[id] + ^[/]?/(acme\\.one\\-two,three/de|acme\\.one\\-two,three/en)/posts/(?<nxtPid>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /$1/posts/[id]$3?nxtPid=$nxtPid /acme.one-two,three/[locale]/posts/[id] - ^[/]?/acme\\.one\\-two,three/(?<nxtPlocale>[^/]+?)/posts/(?<nxtPid>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /acme.one-two,three/[locale]/posts/[id]$rscSuffix?nxtPlocale=$nxtPlocale&nxtPid=$nxtPid + ^[/]?/acme\\.one\\-two,three/(?<nxtPlocale>[^/]+?)/posts/(?<nxtPid>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /acme.one-two,three/[locale]/posts/[id]$3?nxtPlocale=$nxtPlocale&nxtPid=$nxtPid /sparse/en/posts/[id] - ^[/]?/sparse/en/posts/(?<nxtPid>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /sparse/en/posts/[id]$rscSuffix?nxtPid=$nxtPid + ^[/]?/sparse/en/posts/(?<nxtPid>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /sparse/en/posts/[id]$2?nxtPid=$nxtPid /sparse/[locale]/posts/[id] - ^[/]?/sparse/(?<nxtPlocale>[^/]+?)/posts/(?<nxtPid>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /sparse/[locale]/posts/[id]$rscSuffix?nxtPlocale=$nxtPlocale&nxtPid=$nxtPid + ^[/]?/sparse/(?<nxtPlocale>[^/]+?)/posts/(?<nxtPid>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /sparse/[locale]/posts/[id]$3?nxtPlocale=$nxtPlocale&nxtPid=$nxtPid /[team]/[locale]/posts/[id] - ^[/]?/(?<nxtPteam>[^/]+?)/(?<nxtPlocale>[^/]+?)/posts/(?<nxtPid>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /[team]/[locale]/posts/[id]$rscSuffix?nxtPteam=$nxtPteam&nxtPlocale=$nxtPlocale&nxtPid=$nxtPid" + ^[/]?/(?<nxtPteam>[^/]+?)/(?<nxtPlocale>[^/]+?)/posts/(?<nxtPid>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /[team]/[locale]/posts/[id]$4?nxtPteam=$nxtPteam&nxtPlocale=$nxtPlocale&nxtPid=$nxtPid" `) }) }) diff --git a/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-shell-prefixes-single.test.ts b/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-shell-prefixes-single.test.ts index 2d7c58651208..73897325813c 100644 --- a/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-shell-prefixes-single.test.ts +++ b/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-shell-prefixes-single.test.ts @@ -29,12 +29,12 @@ describe('adapter dynamic routes (shell prefixes, one combination)', () => { "2 entries /acme.one-two,three/en/posts/[id] - ^[/]?/acme\\.one\\-two,three/en/posts/(?<nxtPid>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /acme.one-two,three/en/posts/[id]$rscSuffix?nxtPid=$nxtPid + ^[/]?/acme\\.one\\-two,three/en/posts/(?<nxtPid>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /acme.one-two,three/en/posts/[id]$2?nxtPid=$nxtPid /[team]/[locale]/posts/[id] - ^[/]?/(?<nxtPteam>[^/]+?)/(?<nxtPlocale>[^/]+?)/posts/(?<nxtPid>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /[team]/[locale]/posts/[id]$rscSuffix?nxtPteam=$nxtPteam&nxtPlocale=$nxtPlocale&nxtPid=$nxtPid" + ^[/]?/(?<nxtPteam>[^/]+?)/(?<nxtPlocale>[^/]+?)/posts/(?<nxtPid>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /[team]/[locale]/posts/[id]$4?nxtPteam=$nxtPteam&nxtPlocale=$nxtPlocale&nxtPid=$nxtPid" `) }) }) diff --git a/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-shell-prefixes.test.ts b/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-shell-prefixes.test.ts index 5135f84e59a3..664f358c4993 100644 --- a/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-shell-prefixes.test.ts +++ b/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-shell-prefixes.test.ts @@ -31,13 +31,13 @@ describe('adapter dynamic routes (shell prefixes)', () => { .toMatchInlineSnapshot(` "2 entries - /$shellPrefix/posts/[id] - ^[/]?/(?<shellPrefix>acme\\.one\\-two,three/de|acme\\.one\\-two,three/en|sparse/en)/posts/(?<nxtPid>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /$shellPrefix/posts/[id]$rscSuffix?nxtPid=$nxtPid + /$1/posts/[id] + ^[/]?/(acme\\.one\\-two,three/de|acme\\.one\\-two,three/en|sparse/en)/posts/(?<nxtPid>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /$1/posts/[id]$3?nxtPid=$nxtPid /[team]/[locale]/posts/[id] - ^[/]?/(?<nxtPteam>[^/]+?)/(?<nxtPlocale>[^/]+?)/posts/(?<nxtPid>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /[team]/[locale]/posts/[id]$rscSuffix?nxtPteam=$nxtPteam&nxtPlocale=$nxtPlocale&nxtPid=$nxtPid" + ^[/]?/(?<nxtPteam>[^/]+?)/(?<nxtPlocale>[^/]+?)/posts/(?<nxtPid>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /[team]/[locale]/posts/[id]$4?nxtPteam=$nxtPteam&nxtPlocale=$nxtPlocale&nxtPid=$nxtPid" `) }) }) From dee811ff1cc2cafa85adf312bd2c0d7382ea1250 Mon Sep 17 00:00:00 2001 From: Jamiboy Mohammad <jamiboym@gmail.com> Date: Tue, 15 Sep 2026 10:29:05 -0700 Subject: [PATCH 09/14] test: enable verified caching deploy tests (#98523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Enable the same 24 previously selected deployment-test scopes across 21 caching test files, now in a stack rooted on canary. Remove 24 `@force-gate !deploy` directives and their associated TODO comments, which are already present on canary. Other mode, bundler, middleware, and Cache Components exclusions remain in place. This preserves the selection with passing evidence from the previous deployment runs. No additional candidate scopes are enabled; excluded variants are not counted as deployment coverage. ## Verification - All selected test registration names and assertion bodies match the previous enabled revision, checked by AST comparison. - Verified that the canary diff contains only the inventoried exclusions and their obsolete skip plumbing; other exclusions are preserved. - Formatting and lint passed; 77 gate infrastructure unit tests passed. - Full local bootstrap was blocked by missing package-level dependencies in the temporary worktree. Fresh deployment execution on these rewritten commits remains to be verified in CI. <details> <summary>Preserved scope inventory (24)</summary> - ID 1: `test/e2e/app-dir/app-client-cache/client-cache.original.test.ts` β€” `describe('app dir client cache semantics (30s/5min)', () => { const { next, isNextDev } = nextTestSetup({ files: path.join(__dirname, 'fixtures', 'regular'), nextConfig: { experimental: { staleTimes: { dynamic: 30, static: 180 } }, }, }) if (isNextDev) { // dev doesn't support prefetch={true}, so this just performs a basic test to make sure data is reused for 30s it('should renew the 30s cache once the data is revalidated', async () => { let browser = await next.browser('/', browserConfigWithFixedTime) // navigate to prefetch-auto page await browser.elementByCss('[href="/1"]').click() await browser.waitForElementByCss('#random-number') let initialNumber = await browser.elementById('random-number').text() // Navigate back to the index, and then back to the prefetch-auto page await browser.elementByCss('[href="/"]').click() await browser.waitForElementByCss('[href="/1"]') await browser.eval(fastForwardTo, 5 * 1000) await browser.elementByCss('[href="/1"]').click() await browser.waitForElementByCss('#random-number') let newNumber = await browser.elementById('random-number').text() // the number should be the same, as we navigated within 30s. expect(newNumber).toBe(initialNumber) // Fast forward to expire the cache await browser.eval(fastForwardTo, 30 * 1000) // Navigate back to the index, and then back to the prefetch-auto page await browser.elementByCss('[href="/"]').click() await browser.waitForElementByCss('[href="/1"]') await browser.elementByCss('[href="/1"]').click() await browser.waitForElementByCss('#random-number') newNumber = await browser.elementById('random-number').text() // ~35s have passed, so the cache should be expired and the number should be different expect(newNumber).not.toBe(initialNumber) // once the number is updated, we should have a renewed 30s cache for this entry // store this new number so we can check that it stays the same initialNumber = newNumber await browser.eval(fastForwardTo, 5 * 1000) // Navigate back to the index, and then back to the prefetch-auto page await browser.elementByCss('[href="/"]').click() await browser.waitForElementByCss('[href="/1"]') await browser.elementByCss('[href="/1"]').click() await browser.waitForElementByCss('#random-number') newNumber = await browser.elementById('random-number').text() // the number should be the same, as we navigated within 30s (part 2). expect(newNumber).toBe(initialNumber) }) } else { describe('prefetch={true}', () => { let browser: Playwright beforeEach(async () => { browser = await next.browser('/', browserConfigWithFixedTime) }) it('should prefetch the full page', async () => { const { getRequests, clearRequests } = await createRequestsListener(browser) await retry(() => { expect( getRequests().some( ([url, didPartialPrefetch]) => getPathname(url) === '/0' && !didPartialPrefetch ) ).toBe(true) }) clearRequests() await browser.elementByCss('[href="/0?timeout=0"]').click() await browser.waitForElementByCss('#random-number') await retry(() => { const requests = getRequests() expect(requests.every(([url]) => getPathname(url) !== '/0')).toBe( true ) }) }) it('should re-use the cache for the full page, only for 5 mins', async () => { await browser.elementByCss('[href="/0?timeout=0"]').click() await browser.waitForElementByCss('#random-number') const randomNumber = await browser.elementById('random-number').text() await browser.elementByCss('[href="/"]').click() await browser.waitForElementByCss('[href="/0?timeout=0"]') await browser.elementByCss('[href="/0?timeout=0"]').click() await browser.waitForElementByCss('#random-number') const number = await browser.elementById('random-number').text() expect(number).toBe(randomNumber) await browser.eval(fastForwardTo, 5 * 60 * 1000) await browser.elementByCss('[href="/"]').click() await browser.waitForElementByCss('[href="/0?timeout=0"]') await browser.elementByCss('[href="/0?timeout=0"]').click() await browser.waitForElementByCss('#random-number') const newNumber = await browser.elementById('random-number').text() expect(newNumber).not.toBe(randomNumber) }) it('should prefetch again after 5 mins if the link is visible again', async () => { const { getRequests, clearRequests } = await createRequestsListener(browser) await retry(() => { expect( getRequests().some( ([url, didPartialPrefetch]) => getPathname(url) === '/0' && !didPartialPrefetch ) ).toBe(true) }) await browser.elementByCss('[href="/0?timeout=0"]').click() await browser.waitForElementByCss('#random-number') const randomNumber = await browser.elementById('random-number').text() await browser.eval(fastForwardTo, 5 * 60 * 1000) clearRequests() await browser.elementByCss('[href="/"]').click() await browser.waitForElementByCss('[href="/0?timeout=0"]') await retry(() => { expect( getRequests().some( ([url, didPartialPrefetch]) => getPathname(url) === '/0' && !didPartialPrefetch ) ).toBe(true) }) await browser.elementByCss('[href="/0?timeout=0"]').click() await browser.waitForElementByCss('#random-number') const number = await browser.elementById('random-number').text() expect(number).not.toBe(randomNumber) }) }) describe('prefetch={false}', () => { let browser: Playwright beforeEach(async () => { browser = await next.browser('/', browserConfigWithFixedTime) }) it('should not prefetch the page at all', async () => { const { getRequests } = await createRequestsListener(browser) await browser.elementByCss('[href="/2"]').click() await browser.waitForElementByCss('#random-number') await retry(() => { const requests = getRequests().filter( ([url]) => getPathname(url) === '/2' ) expect(requests.length).toBe(1) }) expect( getRequests().some( ([url, didPartialPrefetch]) => getPathname(url) === '/2' && didPartialPrefetch ) ).toBe(false) }) it('should re-use the cache only for 30 seconds', async () => { await browser.elementByCss('[href="/2"]').click() await browser.waitForElementByCss('#random-number') const randomNumber = await browser.elementById('random-number').text() await browser.elementByCss('[href="/"]').click() await browser.waitForElementByCss('[href="/2"]') await browser.elementByCss('[href="/2"]').click() await browser.waitForElementByCss('#random-number') const number = await browser.elementById('random-number').text() expect(number).toBe(randomNumber) await browser.eval(fastForwardTo, 30 * 1000) await browser.elementByCss('[href="/"]').click() await browser.waitForElementByCss('[href="/2"]') await browser.elementByCss('[href="/2"]').click() await browser.waitForElementByCss('#random-number') const newNumber = await browser.elementById('random-number').text() expect(newNumber).not.toBe(randomNumber) }) }) describe('prefetch={undefined} - default', () => { let browser: Playwright beforeEach(async () => { browser = await next.browser('/', browserConfigWithFixedTime) }) it('should prefetch partially a dynamic page', async () => { const { getRequests, clearRequests } = await createRequestsListener(browser) await retry(() => { expect( getRequests().some( ([url, didPartialPrefetch]) => getPathname(url) === '/1' && didPartialPrefetch ) ).toBe(true) }) clearRequests() await browser.elementByCss('[href="/1"]').click() await browser.waitForElementByCss('#random-number') await retry(() => { expect( getRequests().some( ([url, didPartialPrefetch]) => getPathname(url) === '/1' && !didPartialPrefetch ) ).toBe(true) }) }) it('should re-use the full cache for only 30 seconds', async () => { await browser.elementByCss('[href="/1"]').click() await browser.waitForElementByCss('#random-number') const randomNumber = await browser.elementById('random-number').text() await browser.elementByCss('[href="/"]').click() await browser.waitForElementByCss('[href="/1"]') await browser.elementByCss('[href="/1"]').click() await browser.waitForElementByCss('#random-number') const number = await browser.elementById('random-number').text() expect(number).toBe(randomNumber) await browser.eval(fastForwardTo, 5 * 1000) await browser.elementByCss('[href="/"]').click() await browser.waitForElementByCss('[href="/1"]') await browser.elementByCss('[href="/1"]').click() await browser.waitForElementByCss('#random-number') const newNumber = await browser.elementById('random-number').text() expect(newNumber).toBe(randomNumber) await browser.eval(fastForwardTo, 30 * 1000) await browser.elementByCss('[href="/"]').click() await browser.waitForElementByCss('[href="/1"]') await browser.elementByCss('[href="/1"]').click() await browser.waitForElementByCss('#random-number') const newNumber2 = await browser.elementById('random-number').text() expect(newNumber2).not.toBe(newNumber) }) it('should renew the 30s cache once the data is revalidated', async () => { // navigate to prefetch-auto page await browser.elementByCss('[href="/1"]').click() await browser.waitForElementByCss('#random-number') let initialNumber = await browser.elementById('random-number').text() // Navigate back to the index, and then back to the prefetch-auto page await browser.elementByCss('[href="/"]').click() await browser.waitForElementByCss('[href="/1"]') await browser.eval(fastForwardTo, 5 * 1000) await browser.elementByCss('[href="/1"]').click() await browser.waitForElementByCss('#random-number') let newNumber = await browser.elementById('random-number').text() // the number should be the same, as we navigated within 30s. expect(newNumber).toBe(initialNumber) // Fast forward to expire the cache await browser.eval(fastForwardTo, 30 * 1000) // Navigate back to the index, and then back to the prefetch-auto page await browser.elementByCss('[href="/"]').click() await browser.waitForElementByCss('[href="/1"]') await browser.elementByCss('[href="/1"]').click() await browser.waitForElementByCss('#random-number') newNumber = await browser.elementById('random-number').text() // ~35s have passed, so the cache should be expired and the number should be different expect(newNumber).not.toBe(initialNumber) // once the number is updated, we should have a renewed 30s cache for this entry // store this new number so we can check that it stays the same initialNumber = newNumber await browser.eval(fastForwardTo, 5 * 1000) // Navigate back to the index, and then back to the prefetch-auto page await browser.elementByCss('[href="/"]').click() await browser.waitForElementByCss('[href="/1"]') await browser.elementByCss('[href="/1"]').click() await browser.waitForElementByCss('#random-number') newNumber = await browser.elementById('random-number').text() // the number should be the same, as we navigated within 30s (part 2). expect(newNumber).toBe(initialNumber) }) it('should refetch below the fold after 30 seconds', async () => { await browser.elementByCss('[href="/1?timeout=1000"]').click() await browser.waitForElementByCss('#random-number') const randomNumber = await browser.elementById('random-number').text() await browser.elementByCss('[href="/"]').click() await browser.waitForElementByCss('[href="/1?timeout=1000"]') await browser.eval(fastForwardTo, 30 * 1000) await browser.elementByCss('[href="/1?timeout=1000"]').click() await browser.waitForElementByCss('#random-number') const newNumber = await browser.elementById('random-number').text() expect(newNumber).not.toBe(randomNumber) }) it('should refetch the full page after 5 mins', async () => { // Wait for initial prefetch to complete before clicking await browser.waitForIdleNetwork() const randomLoadingNumber = await browser .elementByCss('[href="/1?timeout=1000"]') .click() .waitForElementByCss('#loading') .text() const randomNumber = await browser .waitForElementByCss('#random-number') .text() await browser.eval(fastForwardTo, 5 * 60 * 1000) await browser .elementByCss('[href="/"]') .click() .waitForElementByCss('[href="/1?timeout=1000"]') // Wait for prefetch requests to complete before clicking, otherwise // clicking during an in-flight prefetch aborts it and skips loading state await browser.waitForIdleNetwork() const newLoadingNumber = await browser .elementByCss('[href="/1?timeout=1000"]') .click() .waitForElementByCss('#loading') .text() const newNumber = await browser .waitForElementByCss('#random-number') .text() expect(newLoadingNumber).not.toBe(randomLoadingNumber) expect(newNumber).not.toBe(randomNumber) }) it('should respect a loading boundary that returns `null`', async () => { await browser.elementByCss('[href="/null-loading"]').click() // the page content should disappear immediately await retry(async () => { expect( await browser.hasElementByCssSelector('[href="/null-loading"]') ).toBe(false) }) // the root layout should still be visible expect(await browser.hasElementByCssSelector('#root-layout')).toBe(true) // the dynamic content should eventually appear await browser.waitForElementByCss('#random-number') expect(await browser.hasElementByCssSelector('#random-number')).toBe( true ) }) }) it('should seed the prefetch cache with the fetched page data', async () => { const browser = await next.browser('/1', browserConfigWithFixedTime) await browser.waitForElementByCss('#random-number') const initialNumber = await browser.elementById('random-number').text() // Move forward a few seconds, navigate off the page and then back to it await browser.eval(fastForwardTo, 5 * 1000) await browser.elementByCss('[href="/"]').click() await browser.waitForElementByCss('[href="/1"]') await browser.waitForIdleNetwork() await browser.elementByCss('[href="/1"]').click() await browser.waitForElementByCss('#random-number') const newNumber = await browser.elementById('random-number').text() // The number should be the same as we've seeded it in the prefetch cache when we loaded the full page expect(newNumber).toBe(initialNumber) }) it('should renew the initial seeded data after expiration time', async () => { const browser = await next.browser( '/without-loading/1', browserConfigWithFixedTime ) await browser.waitForElementByCss('#random-number') const initialNumber = await browser.elementById('random-number').text() // Expire the cache await browser.eval(fastForwardTo, 30 * 1000) await browser.elementByCss('[href="/without-loading"]').click() await browser.waitForElementByCss('[href="/without-loading/1"]') await browser.elementByCss('[href="/without-loading/1"]').click() await browser.waitForElementByCss('#random-number') const newNumber = await browser.elementById('random-number').text() // The number should be different, as the seeded data has expired after 30s expect(newNumber).not.toBe(initialNumber) }) } })` - ID 4: `test/e2e/app-dir/app-custom-cache-handler/index.test.ts` β€” `describe('app-dir - custom-cache-handler - cjs', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, env: { CUSTOM_CACHE_HANDLER: 'cache-handler.js', }, }) runTests('cjs module exports', { next, isNextDev }) })` - ID 5: `test/e2e/app-dir/app-custom-cache-handler/index.test.ts` β€” `describe('app-dir - custom-cache-handler - cjs-default-export', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, env: { CUSTOM_CACHE_HANDLER: 'cache-handler-cjs-default-export.js', }, }) runTests('cjs default export', { next, isNextDev }) })` - ID 6: `test/e2e/app-dir/app-custom-cache-handler/index.test.ts` β€” `describe('app-dir - custom-cache-handler - esm', () => { const { next, isNextDev } = nextTestSetup({ files: { app: new FileRef(__dirname + '/app'), 'cache-handler-esm.js': new FileRef(__dirname + '/cache-handler-esm.js'), 'next.config.js': originalNextConfig.replace( 'module.exports = ', 'export default ' ), }, packageJson: { type: 'module', }, env: { CUSTOM_CACHE_HANDLER: 'cache-handler-esm.js', }, }) runTests('esm default export', { next, isNextDev }) })` - ID 7: `test/e2e/app-dir/app-custom-cache-handler/index.test.ts` β€” `describe('app-dir - custom-cache-handler - esm import.meta.resolve', () => { const { next, isNextDev } = nextTestSetup({ files: { app: new FileRef(__dirname + '/app'), 'cache-handler-esm.js': new FileRef(__dirname + '/cache-handler-esm.js'), 'next.config.js': importMetaResolveNextConfig, }, packageJson: { type: 'module', }, }) runTests('esm default export', { next, isNextDev }) })` - ID 9: `test/e2e/app-dir/app-prefetch/prefetching.stale-times.test.ts` β€” `describe('app dir - prefetching (custom staleTime)', () => { const { next, isNextDev } = nextTestSetup({ files: { app: new FileRef(join(__dirname, 'app')), }, nextConfig: { experimental: { staleTimes: { static: 30, // Minimum enforced by clientSegmentCache is 30 seconds dynamic: 5, }, }, }, }) if (isNextDev) { it('should skip next dev for now', () => {}) return } it('should not fetch again when a static page was prefetched when navigating to it twice', async () => { let act: ReturnType<typeof createRouterAct> const browser = await next.browser('/', { beforePageLoad(page) { act = createRouterAct(page) }, }) // Reveal the link to trigger prefetch and wait for it to complete const link = await act( async () => { const reveal = await browser.elementByCss('#accordion-to-static-page') await reveal.click() return browser.elementByCss('#to-static-page') }, { includes: 'Static Page [prefetch-sentinel]' } ) // Navigate to static page - should use prefetched data with no additional requests await act(async () => { await link.click() const staticPageText = await browser.elementByCss('#static-page').text() expect(staticPageText).toBe('Static Page [prefetch-sentinel]') }, 'no-requests') // Reveal the "to-home" link and navigate back // Note: Not using act() here because behavior differs between cache models. // With clientSegmentCache, revealing may trigger a prefetch. Without it, home is already // cached so no prefetch occurs. Either way, navigation works with cached data. const reveal = await browser.elementByCss('#accordion-to-home') await reveal.click() const homeLink = await browser.waitForElementByCss('#to-home') await homeLink.click() await browser.waitForElementByCss('#accordion-to-static-page') // Reveal the static page link again since accordion is hidden after navigation await browser.elementByCss('#accordion-to-static-page').click() await browser.waitForElementByCss('#to-static-page') // Navigate to static page again using the accordion - should still use cached data with no additional requests const staticPageText = await act(async () => { await browser.elementByCss('#to-static-page').click() return browser.elementByCss('#static-page').text() }, 'no-requests') expect(staticPageText).toBe('Static Page [prefetch-sentinel]') }) it('should fetch again when a static page was prefetched when navigating to it after the stale time has passed', async () => { let act: ReturnType<typeof createRouterAct> const timeController = createTimeController() const browser = await next.browser('/', { beforePageLoad(page) { act = createRouterAct(page) }, }) // Install time controller await timeController.install(browser) // Reveal the static-page link to trigger prefetch and wait for it to complete let link = await act( async () => { const reveal = await browser.elementByCss('#accordion-to-static-page') await reveal.click() return browser.elementByCss('#to-static-page') }, { includes: 'Static Page [prefetch-sentinel]' } ) // Navigate to static page - should use prefetched data with no additional requests await act(async () => { await link.click() await browser.waitForElementByCss('#static-page') }, 'no-requests') // Reveal the "to-home" link and navigate back const reveal = await browser.elementByCss('#accordion-to-home') await reveal.click() const homeLink = await browser.waitForElementByCss('#to-home') await homeLink.click() await browser.waitForElementByCss('#accordion-to-static-page') // Advance time past the stale time await timeController.advance(browser, 31000) // Reveal the static-page link to trigger prefetch and wait for it to complete link = await act( async () => { const reveal = await browser.elementByCss('#accordion-to-static-page') await reveal.click() return browser.elementByCss('#to-static-page') }, { includes: 'Static Page [prefetch-sentinel]' } ) // Navigate to static page - should use prefetched data with no additional requests await act(async () => { await link.click() await browser.waitForElementByCss('#static-page') }, 'no-requests') }) // FIXME: Flaky test - investigate and re-enable it.skip('should not re-fetch cached data when navigating back to a route group', async () => { let act: ReturnType<typeof createRouterAct> // Just installing so that the page doesn't automatically move past dynamic stale time createTimeController() const browser = await next.browser('/prefetch-auto-route-groups', { beforePageLoad(page) { act = createRouterAct(page) }, }) // Once the page has loaded, we expect a data fetch (initial page load) expect(await browser.elementById('count').text()).toBe('1') // Navigate to a sub-page - this will trigger a data fetch await act(async () => { await browser .elementByCss("[href='/prefetch-auto-route-groups/sub/foo']") .click() }) // Navigate back to the route group page - should use cached data with no additional fetch await act(async () => { await browser.elementByCss("[href='/prefetch-auto-route-groups']").click() // Confirm that the dashboard page is still rendering the stale fetch count, as it should be cached }, 'no-requests') expect(await browser.elementById('count').text()).toBe('1') // Navigate to a new sub-page - this will trigger another data fetch await act(async () => { await browser .elementByCss("[href='/prefetch-auto-route-groups/sub/bar']") .click() }) // Finally, go back to the route group page - should use cached data with no additional fetch await act(async () => { await browser.elementByCss("[href='/prefetch-auto-route-groups']").click() }, 'no-requests') // Confirm that the dashboard page is still rendering the stale fetch count, as it should be cached expect(await browser.elementById('count').text()).toBe('1') // Reload the page to get the accurate total number of fetches await browser.refresh() // The initial fetch, 2 sub-page fetches, and a final fetch when reloading the page expect(await browser.elementById('count').text()).toBe('4') }) it('should fetch again when the initially visited static page is visited after the stale time has passed', async () => { let act: ReturnType<typeof createRouterAct> const timeController = createTimeController() const browser = await next.browser('/static-page-no-prefetch', { beforePageLoad(page) { act = createRouterAct(page) }, }) // Install time controller await timeController.install(browser) // Wait for the page to load (initial navigation request happened during browser load) await browser.waitForElementByCss('#static-page-no-prefetch') // Reveal the home link and wait for prefetch to complete, then navigate const homeLink = await act( async () => { const reveal = await browser.elementByCss('#accordion-to-home') await reveal.click() return browser.elementByCss('#to-home') }, { includes: 'Home Page [prefetch-sentinel]' } ) // Navigate to home - no additional requests since we just prefetched await homeLink.click() await browser.waitForElementByCss('#accordion-to-static-page') // Advance time past the stale time await timeController.advance(browser, 31000) // Reveal the link to static-page-no-prefetch and wait for prefetch const link = await act( async () => { const reveal = await browser.elementByCss( '#accordion-to-static-page-no-prefetch' ) await reveal.click() return browser.elementByCss('#to-static-page-no-prefetch') }, { includes: 'Static Page No Prefetch [prefetch-sentinel]' } ) // Navigate back to static-page-no-prefetch - should use the fresh prefetch data const staticPageText = await act(async () => { await link.click() return browser.elementByCss('#static-page-no-prefetch').text() }, 'no-requests') expect(staticPageText).toBe('Static Page No Prefetch [prefetch-sentinel]') }) it('should renew the stale time after refetching expired RSC data', async () => { let act: ReturnType<typeof createRouterAct> const timeController = createTimeController() const browser = await next.browser('/', { beforePageLoad(page) { act = createRouterAct(page) }, }) // Install time controller await timeController.install(browser) // Reveal the static-page link to trigger prefetch and wait for it to complete let link = await act( async () => { const reveal = await browser.elementByCss('#accordion-to-static-page') await reveal.click() return browser.elementByCss('#to-static-page') }, { includes: 'Static Page [prefetch-sentinel]' } ) // Navigate to static page (should use prefetched data with no additional requests) await act(async () => { await link.click() await browser.waitForElementByCss('#static-page') }, 'no-requests') // Reveal the "to-home" link and navigate back // Note: Not using act() here because behavior differs between cache models. // With clientSegmentCache, revealing may trigger a prefetch. Without it, home is already // cached so no prefetch occurs. Either way, navigation works with cached data. const reveal = await browser.elementByCss('#accordion-to-home') await reveal.click() const homeLink = await browser.waitForElementByCss('#to-home') await homeLink.click() await browser.waitForElementByCss('#accordion-to-static-page') // Advance time past the stale time await timeController.advance(browser, 31000) // Reveal the static-page link to trigger prefetch and wait for it to complete link = await act( async () => { const reveal = await browser.elementByCss('#accordion-to-static-page') await reveal.click() return browser.elementByCss('#to-static-page') }, { includes: 'Static Page [prefetch-sentinel]' } ) // Navigate to static page again (should use freshly prefetched data with no additional requests) await act(async () => { await link.click() await browser.waitForElementByCss('#static-page') }, 'no-requests') // Go back to home (reveal the link and navigate) // Note: Not using act() here because behavior differs between cache models. const reveal2 = await browser.elementByCss('#accordion-to-home') await reveal2.click() const homeLink2 = await browser.waitForElementByCss('#to-home') await homeLink2.click() await browser.waitForElementByCss('#accordion-to-static-page') // Advance time but not past the stale time (20 seconds < 30 second stale time - should still be fresh) await timeController.advance(browser, 20000) // Reveal the static-page link to trigger prefetch (should use cached data, not refetch) link = await act(async () => { const reveal = await browser.elementByCss('#accordion-to-static-page') await reveal.click() return browser.elementByCss('#to-static-page') }, 'no-requests') // Navigate to static page again (should NOT refetch - stale time should be renewed) // If this assertion passes, it means the stale time was properly renewed after the refetch const staticPageText = await act(async () => { await link.click() return browser.elementByCss('#static-page').text() }, 'no-requests') expect(staticPageText).toBe('Static Page [prefetch-sentinel]') }) })` - ID 10: `test/e2e/app-dir/app-root-params-getters/use-cache.test.ts` β€” `describe('app-root-param-getters - cache dedup with root params', () => { const { next, isNextDev } = nextTestSetup({ files: join(__dirname, 'fixtures', 'use-cache-dedup'), }) it('should dedupe same root params and isolate different root params', async () => { // Three concurrent requests: ca/en, ca/fr, ca/fr. const [$en, $fr1, $fr2] = await Promise.all([ next.render$('/ca/en'), next.render$('/ca/fr'), next.render$('/ca/fr'), ]) const randomEn = $en('#random').text() const randomFr1 = $fr1('#random').text() const randomFr2 = $fr2('#random').text() expect(randomEn).toBeTruthy() expect(randomFr1).toBeTruthy() // ca/en and ca/fr should have different results (isolation). expect(randomEn).not.toBe(randomFr1) // Both ca/fr requests should have the same result (deduped). expect(randomFr1).toBe(randomFr2) }) it('should dedupe same root params and isolate different root params for private caches', async () => { // Three concurrent requests: ca/en, ca/fr, ca/fr. const [$en, $fr1, $fr2] = await Promise.all([ next.render$('/ca/en/use-cache-private'), next.render$('/ca/fr/use-cache-private'), next.render$('/ca/fr/use-cache-private'), ]) const randomEn = $en('#random').text() const randomFr1 = $fr1('#random').text() const randomFr2 = $fr2('#random').text() expect(randomEn).toBeTruthy() expect(randomFr1).toBeTruthy() // Different root params produce different entries, in dev and production. expect(randomEn).not.toBe(randomFr1) if (isNextDev) { // In dev, private caches are persisted and participate in cross-request // deduplication keyed by root params, so the two ca/fr requests join one // in-flight invocation and share a single fill. expect(randomFr1).toBe(randomFr2) } else { // In production, private caches are not persisted and are never deduped // across requests, so each ca/fr request generates its own value. expect(randomFr1).not.toBe(randomFr2) } }) })` - ID 21: `test/e2e/app-dir/cache-components-errors/module-scope.test.ts` β€” `describe('Lazy Module Init', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname + '/fixtures/lazy-module-init', skipStart: true, }) if (isNextDev) { it('does not run in dev', () => {}) return } it('should build statically even if module scope uses sync APIs like current time and random', async () => { try { await next.start() } catch { throw new Error('expected build not to fail for fully static project') } expect(next.cliOutput).toContain('β—‹ /server') expect(next.cliOutput).toContain('β—‹ /client') expect(next.cliOutput).toContain('β—‹ /client-page') expect(next.cliOutput).toContain('◐ /[dyn]') let $ $ = await next.render$('/server') expect($('#id').text().length).toBeGreaterThan(0) $ = await next.render$('/client') expect($('#id').text().length).toBeGreaterThan(0) $ = await next.render$('/client-page') expect($('#id').text().length).toBeGreaterThan(0) $ = await next.render$('/foo') expect($('#id').text().length).toBeGreaterThan(0) $ = await next.render$('/serial-client-sync-io') expect($('#id').text().length).toBeGreaterThan(0) }) })` - ID 27: `test/e2e/app-dir/cache-components/cache-components.connection.test.ts` β€” `describe('cache-components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) it('should partially prerender pages that use connection', async () => { let $ = await next.render$('/connection/static-behavior/boundary', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#foo').text()).toBe('foo') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#foo').text()).toBe('foo') } }) it('should be able to pass connection as a promise to another component and trigger an intermediate Suspense boundary', async () => { const $ = await next.render$('/connection/static-behavior/pass-deeply') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') // In dev, whether or not the fallback appears in the HTML is unreliable // and depends on timing, so we don't assert on its presence // (if we want to assert on it, we should use a browser test) expect($('#page').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#fallback').text()).toBe('at buildtime') expect($('#page').text()).toBe('at runtime') } }) })` - ID 28: `test/e2e/app-dir/cache-components/cache-components.cookies.test.ts` β€” `describe('cache-components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) it('should partially prerender pages that use cookies', async () => { let $ = await next.render$('/cookies/static-behavior', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#x-sentinel').text()).toBe('hello') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#x-sentinel').text()).toBe('hello') } }) it('should be able to pass cookies as a promise to another component and trigger an intermediate Suspense boundary', async () => { const $ = await next.render$('/cookies/static-behavior/pass-deeply') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#fallback').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#fallback').text()).toBe('at buildtime') expect($('#page').text()).toBe('at runtime') } }) it('should be able to access cookie properties', async () => { let $ = await next.render$('/cookies/exercise', {}) let cookieWarnings = next.cliOutput .split('\n') .filter((l) => l.includes('Route "/cookies/exercise')) expect(cookieWarnings).toHaveLength(0) // For...of iteration expect($('#for-of-x-sentinel').text()).toContain('hello') expect($('#for-of-x-sentinel-path').text()).toContain('/cookies/exercise') expect($('#for-of-x-sentinel-rand').text()).toContain('x-sentinel-rand') // ...spread iteration expect($('#spread-x-sentinel').text()).toContain('hello') expect($('#spread-x-sentinel-path').text()).toContain('/cookies/exercise') expect($('#spread-x-sentinel-rand').text()).toContain('x-sentinel-rand') // cookies().size expect(parseInt($('#size-cookies').text())).toBeGreaterThanOrEqual(3) // cookies().get('...') && cookies().getAll('...') expect($('#get-x-sentinel').text()).toContain('hello') expect($('#get-x-sentinel-path').text()).toContain('/cookies/exercise') expect($('#get-x-sentinel-rand').text()).toContain('x-sentinel-rand') // cookies().has('...') expect($('#has-x-sentinel').text()).toContain('true') expect($('#has-x-sentinel-foobar').text()).toContain('false') // cookies().set('...', '...') expect($('#set-result-x-sentinel').text()).toContain( 'Cookies can only be modified in a Server Action' ) expect($('#set-value-x-sentinel').text()).toContain('hello') // cookies().delete('...', '...') expect($('#delete-result-x-sentinel').text()).toContain( 'Cookies can only be modified in a Server Action' ) expect($('#delete-value-x-sentinel').text()).toContain('hello') // cookies().clear() expect($('#clear-result').text()).toContain( 'Cookies can only be modified in a Server Action' ) expect($('#clear-value-x-sentinel').text()).toContain('hello') // cookies().toString() expect($('#toString').text()).toContain('x-sentinel=hello') expect($('#toString').text()).toContain('x-sentinel-path') expect($('#toString').text()).toContain('x-sentinel-rand=') }) })` - ID 29: `test/e2e/app-dir/cache-components/cache-components.date.test.ts` β€” `describe('cache-components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) it('should not have route specific errors', async () => { expect(next.cliOutput).not.toMatch('Error: Route "/') expect(next.cliOutput).not.toMatch('Error occurred prerendering page') }) it('should prerender pages with cached `Date.now()` calls', async () => { let $ = await next.render$('/date/now/cached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#value').text()).toMatch(/^\d+$/) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#value').text()).toMatch(/^\d+$/) } }) it('should prerender pages with cached `Date()` calls', async () => { let $ = await next.render$('/date/date/cached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#value').text()).toContain('GMT') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#value').text()).toContain('GMT') } }) it('should prerender pages with cached `new Date()` calls', async () => { let $ = await next.render$('/date/new-date/cached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#value').text()).toContain('GMT') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#value').text()).toContain('GMT') } }) it('should prerender pages with cached static Date instances like `new Date(0)`', async () => { let $ = await next.render$('/date/static-date/cached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#value').text()).toContain('GMT') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#value').text()).toContain('GMT') } }) it('should not prerender pages with uncached static Date instances like `new Date(0)`', async () => { let $ = await next.render$('/date/static-date/uncached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#value').text()).toContain('GMT') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#value').text()).toContain('GMT') } }) })` - ID 30: `test/e2e/app-dir/cache-components/cache-components.draft-mode.test.ts` β€” `describe('cache-components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) let cliIndex = 0 beforeEach(() => { cliIndex = next.cliOutput.length }) function getLines(containing: string): Array<string> { const warnings = next.cliOutput .slice(cliIndex) .split('\n') .filter((l) => l.includes(containing)) cliIndex = next.cliOutput.length return warnings } it('should fully prerender pages that use draftMode', async () => { expect(getLines('Route "/draftmode')).toEqual([]) let $ = await next.render$('/draftmode', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#draft-mode').text()).toBe('false') expect(getLines('Route "/draftmode')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#draft-mode').text()).toBe('false') expect(getLines('Route "/draftmode')).toEqual([]) } }) if (!isNextDev) { it('should stream Suspense fallbacks when draft mode is enabled', async () => { const draftRes = await next.fetch('/draftmode/toggle') const setCookie = draftRes.headers.get('set-cookie') const cookieHeader = { Cookie: setCookie?.split(';', 1)[0] } expect(cookieHeader.Cookie).toBeTruthy() const $ = await next.render$('/draftmode/streaming', undefined, { headers: cookieHeader, }) expect($('#draft-mode').text()).toBe('true') expect($('#delayed-runtime-fallback').text()).toBe( 'Loading draft content...' ) }) } })` - ID 32: `test/e2e/app-dir/cache-components/cache-components.node-crypto.test.ts` β€” `describe('cache-components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) it('should not have route specific errors', async () => { expect(next.cliOutput).not.toMatch('Error: Route "/') expect(next.cliOutput).not.toMatch('Error occurred prerendering page') }) it("should prerender pages with cached `require('node:crypto').getRandomValues(...)` calls", async () => { let $ = await next.render$('/node-crypto/get-random-values/cached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#first').text()).not.toEqual($('#second').text()) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#first').text()).not.toEqual($('#second').text()) } }) it("should prerender pages with cached `require('node:crypto').randomUUID()` calls", async () => { let $ = await next.render$('/node-crypto/random-uuid/cached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#first').text()).not.toEqual($('#second').text()) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#first').text()).not.toEqual($('#second').text()) } }) it("should prerender pages with cached `require('node:crypto').randomBytes(size)` calls", async () => { let $ = await next.render$('/node-crypto/random-bytes/cached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#first').text()).not.toEqual($('#second').text()) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#first').text()).not.toEqual($('#second').text()) } }) it("should prerender pages with cached `require('node:crypto').randomFillSync(buffer)` calls", async () => { let $ = await next.render$('/node-crypto/random-fill-sync/cached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#first').text()).not.toEqual($('#second').text()) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#first').text()).not.toEqual($('#second').text()) } }) it("should prerender pages with cached `require('node:crypto').randomInt(max)` calls", async () => { let $ = await next.render$('/node-crypto/random-int/up-to/cached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#first').text()).not.toEqual($('#second').text()) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#first').text()).not.toEqual($('#second').text()) } }) it("should prerender pages with cached `require('node:crypto').randomInt(min, max)` calls", async () => { let $ = await next.render$('/node-crypto/random-int/between/cached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#first').text()).not.toEqual($('#second').text()) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#first').text()).not.toEqual($('#second').text()) } }) it("should prerender pages with cached `require('node:crypto').generatePrimeSync(size, options)` calls", async () => { let $ = await next.render$('/node-crypto/generate-prime-sync/cached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#first').text()).not.toEqual($('#second').text()) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#first').text()).not.toEqual($('#second').text()) } }) it("should prerender pages with cached `require('node:crypto').generateKeyPairSync(type, options)` calls", async () => { let $ = await next.render$('/node-crypto/generate-key-pair-sync/cached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#first').text()).not.toEqual($('#second').text()) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#first').text()).not.toEqual($('#second').text()) } }) it("should prerender pages with cached `require('node:crypto').generateKeySync(type, options)` calls", async () => { let $ = await next.render$('/node-crypto/generate-key-sync/cached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#first').text()).not.toEqual($('#second').text()) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#first').text()).not.toEqual($('#second').text()) } }) })` - ID 33: `test/e2e/app-dir/cache-components/cache-components.params.test.ts` β€” `describe('cache-components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) let cliIndex = 0 beforeEach(() => { cliIndex = next.cliOutput.length }) function getLines(containing: string): Array<string> { const warnings = next.cliOutput .slice(cliIndex) .split('\n') .filter((l) => l.includes(containing)) cliIndex = next.cliOutput.length return warnings } describe('Params', () => { it('should partially prerender pages that await params in a server components', async () => { expect(getLines('Route "/params')).toEqual([]) let $ = await next.render$( '/params/semantics/one/build/layout-access/server' ) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-lowcard').text()).toBe('one') expect($('#param-highcard').text()).toBe('build') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#param-lowcard').text()).toBe('one') expect($('#param-highcard').text()).toBe('build') } $ = await next.render$('/params/semantics/one/run/layout-access/server') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-lowcard').text()).toBe('one') expect($('#param-highcard').text()).toBe('run') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#highcard-fallback').text()).toBe('loading highcard children') expect($('#page').text()).toBe('at runtime') expect($('#param-lowcard').text()).toBe('one') expect($('#param-highcard').text()).toBe('run') expect(getLines('Route "/params')).toEqual([]) } $ = await next.render$('/params/semantics/one/build/page-access/server') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-lowcard').text()).toBe('one') expect($('#param-highcard').text()).toBe('build') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#param-lowcard').text()).toBe('one') expect($('#param-highcard').text()).toBe('build') expect(getLines('Route "/params')).toEqual([]) } $ = await next.render$('/params/semantics/one/run/page-access/server') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-lowcard').text()).toBe('one') expect($('#param-highcard').text()).toBe('run') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#highcard-fallback').text()).toBe('loading highcard children') expect($('#page').text()).toBe('at runtime') expect($('#param-lowcard').text()).toBe('one') expect($('#param-highcard').text()).toBe('run') expect(getLines('Route "/params')).toEqual([]) } }) // Since #85155, we intentionally omit search params from client segments // if the page is otherwise static, and resume using a client fetch // instead. So it's expected that the value is missing pre-hydration. // There are separate tests that verify that it is eventually hydrated. // TODO: Rewrite or update this test. it.skip('should partially prerender pages that use params in a client components', async () => { expect(getLines('Route "/params')).toEqual([]) let $ = await next.render$( '/params/semantics/one/build/layout-access/client' ) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-lowcard').text()).toBe('one') expect($('#param-highcard').text()).toBe('build') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#param-lowcard').text()).toBe('one') expect($('#param-highcard').text()).toBe('build') expect(getLines('Route "/params')).toEqual([]) } $ = await next.render$('/params/semantics/one/run/layout-access/client') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-lowcard').text()).toBe('one') expect($('#param-highcard').text()).toBe('run') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#highcard-fallback').text()).toBe('loading highcard children') expect($('#page').text()).toBe('at runtime') expect($('#param-lowcard').text()).toBe('one') expect($('#param-highcard').text()).toBe('run') expect(getLines('Route "/params')).toEqual([]) } $ = await next.render$('/params/semantics/one/build/page-access/client') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-lowcard').text()).toBe('one') expect($('#param-highcard').text()).toBe('build') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#param-lowcard').text()).toBe('one') expect($('#param-highcard').text()).toBe('build') expect(getLines('Route "/params')).toEqual([]) } $ = await next.render$('/params/semantics/one/run/page-access/client') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-lowcard').text()).toBe('one') expect($('#param-highcard').text()).toBe('run') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#highcard-fallback').text()).toBe('loading highcard children') expect($('#page').text()).toBe('at runtime') expect($('#param-lowcard').text()).toBe('one') expect($('#param-highcard').text()).toBe('run') expect(getLines('Route "/params')).toEqual([]) } }) it('should fully prerender pages that check individual param keys after awaiting params in a server component', async () => { expect(getLines('Route "/params')).toEqual([]) let $ = await next.render$( '/params/semantics/one/build/layout-has/server' ) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-has-lowcard').text()).toBe('true') expect($('#param-has-highcard').text()).toBe('true') expect($('#param-has-foo').text()).toBe('false') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#param-has-lowcard').text()).toBe('true') expect($('#param-has-highcard').text()).toBe('true') expect($('#param-has-foo').text()).toBe('false') expect(getLines('Route "/params')).toEqual([]) } $ = await next.render$('/params/semantics/one/build/page-has/server') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-has-lowcard').text()).toBe('true') expect($('#param-has-highcard').text()).toBe('true') expect($('#param-has-foo').text()).toBe('false') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#param-has-lowcard').text()).toBe('true') expect($('#param-has-highcard').text()).toBe('true') expect($('#param-has-foo').text()).toBe('false') expect(getLines('Route "/params')).toEqual([]) } $ = await next.render$('/params/semantics/one/run/layout-has/server') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-has-lowcard').text()).toBe('true') expect($('#param-has-highcard').text()).toBe('true') expect($('#param-has-foo').text()).toBe('false') expect(getLines('Route "/params')).toEqual([]) } else { // With PPR fallbacks the first visit is still partially prerendered expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#highcard-fallback').text()).toBe('loading highcard children') expect($('#page').text()).toBe('at runtime') expect($('#param-has-lowcard').text()).toBe('true') expect($('#param-has-highcard').text()).toBe('true') expect($('#param-has-foo').text()).toBe('false') expect(getLines('Route "/params')).toEqual([]) } $ = await next.render$('/params/semantics/one/run/page-has/server') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-has-lowcard').text()).toBe('true') expect($('#param-has-highcard').text()).toBe('true') expect($('#param-has-foo').text()).toBe('false') expect(getLines('Route "/params')).toEqual([]) } else { // With PPR fallbacks the first visit is still partially prerendered expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#highcard-fallback').text()).toBe('loading highcard children') expect($('#page').text()).toBe('at runtime') expect($('#param-has-lowcard').text()).toBe('true') expect($('#param-has-highcard').text()).toBe('true') expect($('#param-has-foo').text()).toBe('false') expect(getLines('Route "/params')).toEqual([]) } }) // Since #85155, we intentionally omit search params from client segments // if the page is otherwise static, and resume using a client fetch // instead. So it's expected that the value is missing pre-hydration. // There are separate tests that verify that it is eventually hydrated. // TODO: Rewrite or update this test. it.skip('should fully prerender pages that check individual param keys after `use`ing params in a client component', async () => { expect(getLines('Route "/params')).toEqual([]) let $ = await next.render$( '/params/semantics/one/build/layout-has/client' ) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-has-lowcard').text()).toBe('true') expect($('#param-has-highcard').text()).toBe('true') expect($('#param-has-foo').text()).toBe('false') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#param-has-lowcard').text()).toBe('true') expect($('#param-has-highcard').text()).toBe('true') expect($('#param-has-foo').text()).toBe('false') expect(getLines('Route "/params')).toEqual([]) } $ = await next.render$('/params/semantics/one/build/page-has/client') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-has-lowcard').text()).toBe('true') expect($('#param-has-highcard').text()).toBe('true') expect($('#param-has-foo').text()).toBe('false') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#param-has-lowcard').text()).toBe('true') expect($('#param-has-highcard').text()).toBe('true') expect($('#param-has-foo').text()).toBe('false') expect(getLines('Route "/params')).toEqual([]) } $ = await next.render$('/params/semantics/one/run/layout-has/client') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-has-lowcard').text()).toBe('true') expect($('#param-has-highcard').text()).toBe('true') expect($('#param-has-foo').text()).toBe('false') expect(getLines('Route "/params')).toEqual([]) } else { // With PPR fallbacks the first visit is still partially prerendered expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#highcard-fallback').text()).toBe('loading highcard children') expect($('#page').text()).toBe('at runtime') expect($('#param-has-lowcard').text()).toBe('true') expect($('#param-has-highcard').text()).toBe('true') expect($('#param-has-foo').text()).toBe('false') expect(getLines('Route "/params')).toEqual([]) } $ = await next.render$('/params/semantics/one/run/page-has/client') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-has-lowcard').text()).toBe('true') expect($('#param-has-highcard').text()).toBe('true') expect($('#param-has-foo').text()).toBe('false') expect(getLines('Route "/params')).toEqual([]) } else { // With PPR fallbacks the first visit is still partially prerendered expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#highcard-fallback').text()).toBe('loading highcard children') expect($('#page').text()).toBe('at runtime') expect($('#param-has-lowcard').text()).toBe('true') expect($('#param-has-highcard').text()).toBe('true') expect($('#param-has-foo').text()).toBe('false') expect(getLines('Route "/params')).toEqual([]) } }) it('should partially prerender pages that spread awaited params in a server component', async () => { expect(getLines('Route "/params')).toEqual([]) let $ = await next.render$( '/params/semantics/one/build/layout-spread/server' ) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-copied-lowcard').text()).toBe('one') expect($('#param-copied-highcard').text()).toBe('build') expect($('#param-key-count').text()).toBe('2') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#param-copied-lowcard').text()).toBe('one') expect($('#param-copied-highcard').text()).toBe('build') expect($('#param-key-count').text()).toBe('2') expect(getLines('Route "/params')).toEqual([]) } $ = await next.render$('/params/semantics/one/build/page-spread/server') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-copied-lowcard').text()).toBe('one') expect($('#param-copied-highcard').text()).toBe('build') expect($('#param-key-count').text()).toBe('2') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#param-copied-lowcard').text()).toBe('one') expect($('#param-copied-highcard').text()).toBe('build') expect($('#param-key-count').text()).toBe('2') expect(getLines('Route "/params')).toEqual([]) } $ = await next.render$('/params/semantics/one/run/layout-spread/server') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-copied-lowcard').text()).toBe('one') expect($('#param-copied-highcard').text()).toBe('run') expect($('#param-key-count').text()).toBe('2') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#highcard-fallback').text()).toBe('loading highcard children') expect($('#page').text()).toBe('at runtime') expect($('#param-copied-lowcard').text()).toBe('one') expect($('#param-copied-highcard').text()).toBe('run') expect($('#param-key-count').text()).toBe('2') expect(getLines('Route "/params')).toEqual([]) } $ = await next.render$('/params/semantics/one/run/page-spread/server') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-copied-lowcard').text()).toBe('one') expect($('#param-copied-highcard').text()).toBe('run') expect($('#param-key-count').text()).toBe('2') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#highcard-fallback').text()).toBe('loading highcard children') expect($('#page').text()).toBe('at runtime') expect($('#param-copied-lowcard').text()).toBe('one') expect($('#param-copied-highcard').text()).toBe('run') expect($('#param-key-count').text()).toBe('2') expect(getLines('Route "/params')).toEqual([]) } }) // Since #85155, we intentionally omit search params from client segments // if the page is otherwise static, and resume using a client fetch // instead. So it's expected that the value is missing pre-hydration. // There are separate tests that verify that it is eventually hydrated. // TODO: Rewrite or update this test. it.skip('should partially prerender pages that spread `use`ed params in a client component', async () => { expect(getLines('Route "/params')).toEqual([]) let $ = await next.render$( '/params/semantics/one/build/layout-spread/client' ) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-copied-lowcard').text()).toBe('one') expect($('#param-copied-highcard').text()).toBe('build') expect($('#param-key-count').text()).toBe('2') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#param-copied-lowcard').text()).toBe('one') expect($('#param-copied-highcard').text()).toBe('build') expect($('#param-key-count').text()).toBe('2') expect(getLines('Route "/params')).toEqual([]) } $ = await next.render$('/params/semantics/one/build/page-spread/client') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-copied-lowcard').text()).toBe('one') expect($('#param-copied-highcard').text()).toBe('build') expect($('#param-key-count').text()).toBe('2') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#param-copied-lowcard').text()).toBe('one') expect($('#param-copied-highcard').text()).toBe('build') expect($('#param-key-count').text()).toBe('2') expect(getLines('Route "/params')).toEqual([]) } $ = await next.render$('/params/semantics/one/run/layout-spread/client') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-copied-lowcard').text()).toBe('one') expect($('#param-copied-highcard').text()).toBe('run') expect($('#param-key-count').text()).toBe('2') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#highcard-fallback').text()).toBe('loading highcard children') expect($('#page').text()).toBe('at runtime') expect($('#param-copied-lowcard').text()).toBe('one') expect($('#param-copied-highcard').text()).toBe('run') expect($('#param-key-count').text()).toBe('2') expect(getLines('Route "/params')).toEqual([]) } $ = await next.render$('/params/semantics/one/run/page-spread/client') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#lowcard').text()).toBe('at runtime') expect($('#highcard').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-copied-lowcard').text()).toBe('one') expect($('#param-copied-highcard').text()).toBe('run') expect($('#param-key-count').text()).toBe('2') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#lowcard').text()).toBe('at buildtime') expect($('#highcard').text()).toBe('at buildtime') expect($('#highcard-fallback').text()).toBe('loading highcard children') expect($('#page').text()).toBe('at runtime') expect($('#param-copied-lowcard').text()).toBe('one') expect($('#param-copied-highcard').text()).toBe('run') expect($('#param-key-count').text()).toBe('2') expect(getLines('Route "/params')).toEqual([]) } }) }) describe('Param Shadowing', () => { it('should correctly allow param names like then, value, and status when awaiting params in a server component', async () => { expect(getLines('Route "/params')).toEqual([]) let $ = await next.render$( '/params/shadowing/foo/bar/baz/qux/layout/server' ) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-dyn').text()).toBe('foo') expect($('#param-then').text()).toBe('bar') expect($('#param-value').text()).toBe('baz') expect($('#param-status').text()).toBe('qux') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at runtime') expect($('#param-dyn').text()).toBe('foo') expect($('#param-then').text()).toBe('bar') expect($('#param-value').text()).toBe('baz') expect($('#param-status').text()).toBe('qux') expect(getLines('Route "/params')).toEqual([]) } $ = await next.render$('/params/shadowing/foo/bar/baz/qux/page/server') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-dyn').text()).toBe('foo') expect($('#param-then').text()).toBe('bar') expect($('#param-value').text()).toBe('baz') expect($('#param-status').text()).toBe('qux') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at runtime') expect($('#param-dyn').text()).toBe('foo') expect($('#param-then').text()).toBe('bar') expect($('#param-value').text()).toBe('baz') expect($('#param-status').text()).toBe('qux') expect(getLines('Route "/params')).toEqual([]) } }) // Since #85155, we intentionally omit search params from client segments // if the page is otherwise static, and resume using a client fetch // instead. So it's expected that the value is missing pre-hydration. // There are separate tests that verify that it is eventually hydrated. // TODO: Rewrite or update this test. it.skip('should correctly allow param names like then, value, and status when `use`ing params in a client component', async () => { expect(getLines('Route "/params')).toEqual([]) let $ = await next.render$( '/params/shadowing/foo/bar/baz/qux/layout/client' ) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-dyn').text()).toBe('foo') expect($('#param-then').text()).toBe('bar') expect($('#param-value').text()).toBe('baz') expect($('#param-status').text()).toBe('qux') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at runtime') expect($('#param-dyn').text()).toBe('foo') expect($('#param-then').text()).toBe('bar') expect($('#param-value').text()).toBe('baz') expect($('#param-status').text()).toBe('qux') expect(getLines('Route "/params')).toEqual([]) } $ = await next.render$('/params/shadowing/foo/bar/baz/qux/page/client') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#param-dyn').text()).toBe('foo') expect($('#param-then').text()).toBe('bar') expect($('#param-value').text()).toBe('baz') expect($('#param-status').text()).toBe('qux') expect(getLines('Route "/params')).toEqual([]) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at runtime') expect($('#param-dyn').text()).toBe('foo') expect($('#param-then').text()).toBe('bar') expect($('#param-value').text()).toBe('baz') expect($('#param-status').text()).toBe('qux') expect(getLines('Route "/params')).toEqual([]) } }) }) if (!isNextDev) { describe('generateStaticParams', () => { // This test is skipped as the previous workaround of using `fetch-cache` will no longer be supported with DIO. it.skip('should have cacheComponents semantics inside generateStaticParams', async () => { // This test is named what we want but our current implementation is not actually correct yet. // We are asserting current behavior and will update the test when we land the correct behavior const lines: Array<string> = next.cliOutput.split('\n') let i = 0 while (true) { const line = lines[i++] if (typeof line !== 'string') { throw new Error( 'Could not find expected route output for /params/generate-static-params/[slug]/page/...' ) } if ( line.startsWith('β”œ') && line.includes('/params/generate-static-params/[slug]') ) { let nextLine = lines[i++] // we expect the fallback shell first expect(nextLine).toContain('/params/generate-static-params/[slug]') nextLine = lines[i++] expect(nextLine).toMatch( /\/params\/generate-static-params\/\d+\/page/ ) nextLine = lines[i++] // Because we force-cache we only end up with one prebuilt page. // When cacheComponents semantics are fully respected we will end up with two. expect(nextLine).not.toMatch( /\/params\/generate-static-params\/\d+\/page/ ) break } } }) }) } })` - ID 34: `test/e2e/app-dir/cache-components/cache-components.random.test.ts` β€” `describe('cache-components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) it('should not have route specific errors', async () => { expect(next.cliOutput).not.toMatch('Error: Route "/') expect(next.cliOutput).not.toMatch('Error occurred prerendering page') }) it('should prerender pages with cached Math.random() calls', async () => { let $ = await next.render$('/random/cached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') } }) })` - ID 35: `test/e2e/app-dir/cache-components/cache-components.routes.test.ts` β€” `describe('cache-components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) let cliIndex = 0 beforeEach(() => { cliIndex = next.cliOutput.length }) function getLines(containing: string): Array<string> { const warnings = next.cliOutput .slice(cliIndex) .split('\n') .filter((l) => l.includes(containing)) cliIndex = next.cliOutput.length return warnings } it('should not prerender GET route handlers that use dynamic APIs', async () => { let str = await next.render('/routes/dynamic-cookies', {}) let json = JSON.parse(str) expect(json.value).toEqual('at runtime') expect(json.type).toEqual('cookies') str = await next.render('/routes/dynamic-headers', {}) json = JSON.parse(str) expect(json.value).toEqual('at runtime') expect(json.type).toEqual('headers') str = await next.render('/routes/dynamic-stream', {}) json = JSON.parse(str) expect(json.value).toEqual('at runtime') expect(json.message).toEqual('dynamic stream') str = await next.render('/routes/dynamic-url?foo=bar', {}) json = JSON.parse(str) expect(json.value).toEqual('at runtime') expect(json.search).toEqual('?foo=bar') }) it('should prerender GET route handlers that have entirely cached io (fetches)', async () => { let str = await next.render('/routes/fetch-cached', {}) let json = JSON.parse(str) let random1 = json.random1 let random2 = json.random2 if (isNextDev) { expect(json.value).toEqual('at runtime') expect(typeof random1).toBe('string') expect(typeof random2).toBe('string') } else { expect(json.value).toEqual('at buildtime') expect(typeof random1).toBe('string') expect(typeof random2).toBe('string') } str = await next.render('/routes/fetch-cached', {}) json = JSON.parse(str) if (isNextDev) { expect(json.value).toEqual('at runtime') expect(random1).toEqual(json.random1) expect(random2).toEqual(json.random2) } else { expect(json.value).toEqual('at buildtime') expect(random1).toEqual(json.random1) expect(random2).toEqual(json.random2) } }) it('should not prerender GET route handlers that have some uncached io (fetches)', async () => { let str = await next.render('/routes/fetch-mixed', {}) let json = JSON.parse(str) let random1 = json.random1 let random2 = json.random2 expect(json.value).toEqual('at runtime') expect(typeof random1).toBe('string') expect(typeof random2).toBe('string') str = await next.render('/routes/fetch-mixed', {}) json = JSON.parse(str) expect(json.value).toEqual('at runtime') expect(random1).toEqual(json.random1) expect(random2).not.toEqual(json.random2) }) it('should prerender GET route handlers that have entirely cached io (unstable_cache)', async () => { let str = await next.render('/routes/io-cached', {}) let json = JSON.parse(str) let message1 = json.message1 let message2 = json.message2 if (isNextDev) { expect(json.value).toEqual('at runtime') expect(typeof message1).toBe('string') expect(typeof message2).toBe('string') } else { expect(json.value).toEqual('at buildtime') expect(typeof message1).toBe('string') expect(typeof message2).toBe('string') } str = await next.render('/routes/io-cached', {}) json = JSON.parse(str) if (isNextDev) { expect(json.value).toEqual('at runtime') expect(message1).toEqual(json.message1) expect(message2).toEqual(json.message2) } else { expect(json.value).toEqual('at buildtime') expect(message1).toEqual(json.message1) expect(message2).toEqual(json.message2) } }) it('should prerender GET route handlers that have entirely cached io ("use cache")', async () => { let str = await next.render('/routes/use_cache-cached', {}) let json = JSON.parse(str) let message1 = json.message1 let message2 = json.message2 if (isNextDev) { expect(json.value).toEqual('at runtime') expect(typeof message1).toBe('string') expect(typeof message2).toBe('string') } else { expect(json.value).toEqual('at buildtime') expect(typeof message1).toBe('string') expect(typeof message2).toBe('string') } str = await next.render('/routes/use_cache-cached', {}) json = JSON.parse(str) if (isNextDev) { expect(json.value).toEqual('at runtime') expect(message1).toEqual(json.message1) expect(message2).toEqual(json.message2) } else { expect(json.value).toEqual('at buildtime') expect(message1).toEqual(json.message1) expect(message2).toEqual(json.message2) } }) it('should not prerender GET route handlers that have some uncached io (unstable_cache)', async () => { let str = await next.render('/routes/io-mixed', {}) let json = JSON.parse(str) let message1 = json.message1 let message2 = json.message2 expect(json.value).toEqual('at runtime') expect(typeof message1).toBe('string') expect(typeof message2).toBe('string') str = await next.render('/routes/io-mixed', {}) json = JSON.parse(str) expect(json.value).toEqual('at runtime') expect(message1).toEqual(json.message1) expect(message2).not.toEqual(json.message2) }) it('should prerender GET route handlers that complete synchronously or in a microtask', async () => { let str = await next.render('/routes/microtask', {}) let json = JSON.parse(str) if (isNextDev) { expect(json.value).toEqual('at runtime') expect(json.message).toBe('microtask') } else { expect(json.value).toEqual('at buildtime') expect(json.message).toBe('microtask') } str = await next.render('/routes/static-stream-sync', {}) json = JSON.parse(str) if (isNextDev) { expect(json.value).toEqual('at runtime') expect(json.message).toBe('stream response') } else { expect(json.value).toEqual('at buildtime') expect(json.message).toBe('stream response') } str = await next.render('/routes/static-stream-async', {}) json = JSON.parse(str) if (isNextDev) { expect(json.value).toEqual('at runtime') expect(json.message).toBe('stream response') } else { expect(json.value).toEqual('at buildtime') expect(json.message).toBe('stream response') } str = await next.render('/routes/static-string-sync', {}) json = JSON.parse(str) if (isNextDev) { expect(json.value).toEqual('at runtime') expect(json.message).toBe('string response') } else { expect(json.value).toEqual('at buildtime') expect(json.message).toBe('string response') } str = await next.render('/routes/static-string-async', {}) json = JSON.parse(str) if (isNextDev) { expect(json.value).toEqual('at runtime') expect(json.message).toBe('string response') } else { expect(json.value).toEqual('at buildtime') expect(json.message).toBe('string response') } }) it('should not prerender GET route handlers that complete in a new Task', async () => { let str = await next.render('/routes/task', {}) let json = JSON.parse(str) expect(json.value).toEqual('at runtime') expect(json.message).toBe('task') }) it('should prerender GET route handlers when accessing params', async () => { expect(getLines('Route "/routes/[dyn]')).toEqual([]) let str = await next.render('/routes/1', {}) let json = JSON.parse(str) if (isNextDev) { expect(json.value).toEqual('at runtime') expect(json.type).toBe('dynamic params') expect(json.param).toBe('1') expect(getLines('Route "/routes/[dyn]')).toEqual([]) } else { expect(json.value).toEqual('at buildtime') expect(json.type).toBe('dynamic params') expect(json.param).toBe('1') expect(getLines('Route "/routes/[dyn]')).toEqual([]) } str = await next.render('/routes/2', {}) json = JSON.parse(str) if (isNextDev) { expect(json.value).toEqual('at runtime') expect(json.type).toBe('dynamic params') expect(json.param).toBe('2') expect(getLines('Route "/routes/[dyn]')).toEqual([]) } else { expect(json.value).toEqual('at runtime') expect(json.type).toBe('dynamic params') expect(json.param).toBe('2') expect(getLines('Route "/routes/[dyn]')).toEqual([]) } }) })` - ID 36: `test/e2e/app-dir/cache-components/cache-components.search.test.ts` β€” `describe('cache-components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) it('should partially prerender pages that await searchParams in a server component', async () => { let $ = await next.render$('/search/server/await?sentinel=hello') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#value').text()).toBe('hello') expect($('#page').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('main').text()).toContain('inner loading...') expect($('main').text()).not.toContain('outer loading...') expect($('#value').text()).toBe('hello') expect($('#page').text()).toBe('at runtime') } }) it('should partially prerender pages that `use` searchParams in a server component', async () => { let $ = await next.render$('/search/server/use?sentinel=hello') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#value').text()).toBe('hello') expect($('#page').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('main').text()).toContain('inner loading...') expect($('main').text()).not.toContain('outer loading...') expect($('#value').text()).toBe('hello') expect($('#page').text()).toBe('at runtime') } }) it('should partially prerender pages that `use` searchParams in a client component', async () => { let $ = await next.render$('/search/client/use?sentinel=hello') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#value').text()).toBe('hello') expect($('#page').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('main').text()).toContain('inner loading...') expect($('main').text()).not.toContain('outer loading...') // Since #85155, we intentionally omit search params from client segments // if the page is otherwise static, and resume using a client fetch // instead. So it's expected that the value is missing pre-hydration. // There are separate tests that verify that it is eventually hydrated. // TODO: Rewrite or update this test. // expect($('#value').text()).toBe('hello') // expect($('#page').text()).toBe('at runtime') expect($('#value').text()).toBe('') expect($('#page').text()).toBe('') } }) })` - ID 37: `test/e2e/app-dir/cache-components/cache-components.test.ts` β€” `describe('cache-components', () => { const { next, isNextDev, isNextStart } = nextTestSetup({ files: __dirname, }) it('should not have route specific errors', async () => { expect(next.cliOutput).not.toMatch('Error: Route "/') expect(next.cliOutput).not.toMatch('Error occurred prerendering page') }) if (isNextDev) { it('should not log not-found errors', async () => { const cliOutputLength = next.cliOutput.length await next.browser('/cases/not-found') const cliOutput = next.cliOutput.slice(cliOutputLength) expect(cliOutput).not.toMatch('Error: NEXT_HTTP_ERROR_FALLBACK;404') expect(cliOutput).not.toMatch('unhandledRejection') }) } else { it('should not warn about potential memory leak for even listeners on AbortSignal', async () => { expect(next.cliOutput).not.toMatch('MaxListenersExceededWarning') }) } it('should prerender fully static pages', async () => { let $ = await next.render$('/cases/static', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') } $ = await next.render$('/cases/static_async', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') } }) it('should prerender static not-found pages', async () => { // Using `browser` instead of `render$` because error pages must be hydrated // apparently. const browser = await next.browser('/cases/not-found') if (isNextDev) { expect(await browser.elementById('layout').text()).toBe('at runtime') expect(await browser.elementById('page').text()).toBe('at runtime') } else { expect(await browser.elementById('layout').text()).toBe('at buildtime') expect(await browser.elementById('page').text()).toBe('at buildtime') } }) it('should render not-found with Suspense in layout without connection errors', async () => { const browser = await next.browser('/cases/not-found-suspense') // The custom not-found component should render expect(await browser.elementById('not-found-text').text()).toBe( 'Custom 404 - Not Found' ) // The async Suspense content in the layout should also render expect(await browser.elementById('async-data').text()).toBe( 'Async Data Loaded' ) }) it('should prerender pages that render in a microtask', async () => { let $ = await next.render$('/cases/microtask', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') } $ = await next.render$('/cases/microtask_deep_tree', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') } }) it('should partially prerender pages that take longer than a task to render', async () => { let $ = await next.render$('/cases/task', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#inner').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') // The inner slot is computed during the prerender but is hidden // it gets revealed when the resume happens expect($('#inner').text()).toBe('at buildtime') } }) it('should prerender pages that only use cached fetches', async () => { const $ = await next.render$('/cases/fetch_cached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') } }) it('should partially prerender pages that use at least one fetch without cache', async () => { let $ = await next.render$('/cases/fetch_mixed', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#inner').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#inner').text()).toBe('at buildtime') } }) it('should prerender pages that only use cached (unstable_cache) IO', async () => { const $ = await next.render$('/cases/io_cached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') } }) it('should prerender pages that only use cached ("use cache") IO', async () => { const $ = await next.render$('/cases/use_cache_cached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') } }) it('should prerender pages that cached the whole page', async () => { const $ = await next.render$('/cases/full_cached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') } }) it('should partially prerender pages that do any uncached IO', async () => { let $ = await next.render$('/cases/io_mixed', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#inner').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#inner').text()).toBe('at buildtime') } }) it('should partially prerender pages that do any uncached IO (use cache)', async () => { let $ = await next.render$('/cases/use_cache_mixed', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#inner').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#inner').text()).toBe('at buildtime') } }) it('should partially prerender pages that use `cookies()`', async () => { let $ = await next.render$('/cases/dynamic_api_cookies', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#inner').text()).toBe('at runtime') expect($('#value').text()).toBe('hello') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#inner').text()).toBe('at buildtime') expect($('#value').text()).toBe('hello') } }) it('should partially prerender pages that use `headers()`', async () => { let $ = await next.render$('/cases/dynamic_api_headers') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#inner').text()).toBe('at runtime') expect($('#value').text()).toBe('hello') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#inner').text()).toBe('at buildtime') expect($('#value').text()).toBe('hello') } }) it('should fully prerender pages that use `unstable_noStore()`', async () => { let $ = await next.render$('/cases/dynamic_api_no_store', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#inner').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#inner').text()).toBe('at buildtime') } }) it('should partially prerender pages that use `searchParams` in Server Components', async () => { let $ = await next.render$( '/cases/dynamic_api_search_params_server?sentinel=my+sentinel', {} ) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#inner').text()).toBe('at runtime') expect($('#value').text()).toBe('my sentinel') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#inner').text()).toBe('at buildtime') expect($('#value').text()).toBe('my sentinel') } }) it('should partially prerender pages that use `searchParams` in Client Components', async () => { let $ = await next.render$( '/cases/dynamic_api_search_params_client?sentinel=my+sentinel', {} ) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#inner').text()).toBe('at runtime') expect($('#value').text()).toBe('my sentinel') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') // The second component renders before the first one aborts so we end up // capturing the static value during buildtime expect($('#inner').text()).toBe('at buildtime') // Since there was no dynamic data access on this page, the search params // are completely ommitted from the HTML document and filled in by // the client expect($('#value').text()).toBe('') expect($('#fallback-component-one-').text()).toBe('loading...') } }) it('can prerender pages with parallel routes that are static', async () => { const $ = await next.render$('/cases/parallel/static', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page-slot').text()).toBe('at runtime') expect($('#page-children').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page-slot').text()).toBe('at buildtime') expect($('#page-children').text()).toBe('at buildtime') } }) it('can prerender pages with parallel routes that resolve in a microtask', async () => { const $ = await next.render$('/cases/parallel/microtask', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page-slot').text()).toBe('at runtime') expect($('#page-children').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page-slot').text()).toBe('at buildtime') expect($('#page-children').text()).toBe('at buildtime') } }) it('does not prerender pages with parallel routes that resolve in a task', async () => { const $ = await next.render$('/cases/parallel/task', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page-slot').text()).toBe('at runtime') expect($('#page-children').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page-slot').text()).toBe('at runtime') expect($('#page-children').text()).toBe('at buildtime') } }) it('does not prerender pages with parallel routes that uses a dynamic API', async () => { let $ = await next.render$('/cases/parallel/no-store', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page-slot').text()).toBe('at runtime') expect($('#page-children').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page-slot').text()).toBe('at buildtime') expect($('#page-children').text()).toBe('at buildtime') } $ = await next.render$('/cases/parallel/cookies', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page-slot').text()).toBe('at runtime') expect($('#page-children').text()).toBe('at runtime') } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page-slot').text()).toBe('at runtime') expect($('#page-children').text()).toBe('at buildtime') } }) if (isNextStart) { it('should ignore late setHeader calls for direct RSC handlers after headers are sent', async () => { const pageModulePath = path.join( next.testDir, '.next', 'server', 'app', 'cases', 'static', 'page.js' ) const previousCwd = process.cwd() const port = await findPort() let server: http.Server | undefined let handlerError: unknown let lateHeaderAttempted = false let lateHeaderError: unknown let resolveHandled: (() => void) | undefined const handled = new Promise<void>((resolve) => { resolveHandled = resolve }) try { process.chdir(next.testDir) const { handler } = require(pageModulePath) as { handler: ( req: http.IncomingMessage, res: http.ServerResponse, ctx: { requestMeta?: Record<string, unknown> waitUntil?: (promise: Promise<void>) => void } ) => Promise<void> } server = http.createServer(async (req, res) => { const originalWriteHead = res.writeHead.bind(res) res.writeHead = ((...args: any[]) => { const result = originalWriteHead(...args) if (!lateHeaderAttempted) { lateHeaderAttempted = true try { res.setHeader('x-test-late', '1') } catch (error) { lateHeaderError = error } } return result }) as typeof res.writeHead try { await handler(req, res, { waitUntil: () => {}, requestMeta: { initURL: `https://localhost:${port}${req.url ?? '/'}`, minimalMode: true, relativeProjectDir: '.', }, }) } catch (error) { handlerError = error if (!res.writableEnded) { if (!res.headersSent) { res.statusCode = 500 } res.end() } } finally { resolveHandled?.() } }) await new Promise<void>((resolve, reject) => { server.listen(port, () => { resolve() }) server.once('error', reject) }) const stateTree = JSON.stringify(['', {}]) const requestUrl = new URL('/cases/static', `http://localhost:${port}`) const cacheBustingParam = await computeCacheBustingSearchParam( undefined, undefined, stateTree, undefined ) if (cacheBustingParam) { requestUrl.searchParams.set('_rsc', cacheBustingParam) } const res = await fetchViaHTTP( port, requestUrl.pathname + requestUrl.search, undefined, { headers: { rsc: '1', 'next-router-state-tree': stateTree, }, redirect: 'manual', } ) const flight = await res.text() expect(res.status).toBe(200) expect(res.headers.get('content-type')).toContain('text/x-component') await handled expect(handlerError).toBeUndefined() expect(lateHeaderAttempted).toBe(true) expect(lateHeaderError).toBeUndefined() expect(flight.length).toBeGreaterThan(0) } finally { process.chdir(previousCwd) if (server) { await new Promise<void>((resolve, reject) => { server.close((error) => { if (error) { reject(error) return } resolve() }) }) } } }) } it('should not resume when client components are dynamic but the RSC render was static', async () => { let html = await next.render('/cases/static-rsc-dynamic-client', {}) const $ = cheerio.load(html) // Confirm the HTML document was sent completely expect(html).toContain('</body></html>') if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') // In dev we SSR the time expect($('#time').length).toBe(1) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') // Confirm the time span is not part of the completed HTML document expect($('#time').length).toBe(0) } const browser = await next.browser('/cases/static-rsc-dynamic-client') const now = new Date() if (isNextDev) { expect(await browser.elementById('layout').text()).toBe('at runtime') expect(await browser.elementById('page').text()).toBe('at runtime') // Assert that we rendered a time within the last couple seconds. const inPageDate = new Date( await browser.waitForElementByCss('#time').text() ) expect(inPageDate.getTime() - now.getTime()).toBeLessThan(2000) } else { expect(await browser.elementById('layout').text()).toBe('at buildtime') expect(await browser.elementById('page').text()).toBe('at buildtime') // Assert that we rendered a time within the last 2 seconds. const inPageDate = new Date( await browser.waitForElementByCss('#time').text() ) expect(inPageDate.getTime() - now.getTime()).toBeLessThan(2000) } }) })` - ID 38: `test/e2e/app-dir/cache-components/cache-components.web-crypto.test.ts` β€” `describe('cache-components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) it('should not have route specific errors', async () => { expect(next.cliOutput).not.toMatch('Error: Route "/') expect(next.cliOutput).not.toMatch('Error occurred prerendering page') }) it('should prerender pages with cached `crypto.getRandomValues(...)` calls', async () => { let $ = await next.render$('/web-crypto/get-random-values/cached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#first').text()).not.toEqual($('#second').text()) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#first').text()).not.toEqual($('#second').text()) } }) it('should prerender pages with cached `crypto.randomUUID()` calls', async () => { let $ = await next.render$('/web-crypto/random-uuid/cached', {}) if (isNextDev) { expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') expect($('#first').text()).not.toEqual($('#second').text()) } else { expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#first').text()).not.toEqual($('#second').text()) } }) })` - ID 40: `test/e2e/app-dir/instant-validation-static-shells/instant-validation-static-shells.test.ts` β€” `describe('instant validation - opting out of static shells', () => { const { next, isNextDev } = nextTestSetup({ files: join(__dirname, 'fixtures', 'valid'), }) // NOTE: if something's wrong in build, we'll fail before any tests run. // Visiting the pages is mostly just a sanity check. it('does not require a static shell if a root layouts is configured as blocking', async () => { const browser = await next.browser('/blocking-root-layout') await browser.elementByCss('main') if (isNextDev) await waitForNoErrorToast(browser) }) it('does not require a static shell if a layout is configured as blocking', async () => { const browser = await next.browser('/blocking-layout') await browser.elementByCss('main') if (isNextDev) await waitForNoErrorToast(browser) }) it('does not require a static shell if a page is configured as blocking', async () => { const browser = await next.browser('/blocking-page') await browser.elementByCss('main') if (isNextDev) await waitForNoErrorToast(browser) }) })` - ID 42: `test/e2e/app-dir/non-rsc-router-prefetch/non-rsc-router-prefetch.test.ts` β€” `describe('non-rsc-router-prefetch', () => { const { next } = nextTestSetup({ files: __dirname, }) beforeAll(async () => { const res = await next.fetch('/') await res.text() }) it('ignores the router prefetch header for HTML requests', async () => { const res = await next.fetch('/', { headers: { [NEXT_ROUTER_PREFETCH_HEADER]: '1', }, signal: AbortSignal.timeout(5_000), }) const html = await res.text() expect(res.status).toBe(200) expect(res.headers.get('content-type')).toContain('text/html') expect(html).toContain('hello world') }) it('honors the router prefetch header for RSC requests', async () => { const res = await next.fetch('/', { headers: { [RSC_HEADER]: '1', [NEXT_ROUTER_PREFETCH_HEADER]: '1', }, }) expect(res.status).toBe(200) expect(res.headers.get('content-type')).toContain('text/x-component') }) })` - ID 56: `test/e2e/app-dir/use-cache-infinity-profile/use-cache-infinity-profile.test.ts` β€” `describe('use-cache-infinity-profile', () => { const { next, isNextStart } = nextTestSetup({ files: __dirname, }) it('caches forever with a configured profile using Infinity revalidate and expire', async () => { const $ = await next.render$('/') const initialValue = $('#value').text() expect(initialValue).toMatch(uuidRegExp) // An infinite cache life must not degrade into a dynamic cache life, so // the value stays the same across requests instead of regenerating. const $second = await next.render$('/') expect($second('#value').text()).toBe(initialValue) if (isNextStart) { // The page must be fully prerendered at build time. const prerendered = await next.readFile('.next/server/app/index.html') expect(prerendered).toContain(initialValue) } }) it('serves an inline Infinity cache life from a JSON-backed cache handler across requests', async () => { const $ = await next.render$('/inline?key=a') const initialValue = $('#value').text() expect(initialValue).toMatch(uuidRegExp) // The second request reads the entry back from the cache handler. If the // infinite cache life doesn't survive the handler's JSON round trip, the // entry is treated as immediately expired and the value regenerates. const $second = await next.render$('/inline?key=a') expect($second('#value').text()).toBe(initialValue) }) })` - ID 57: `test/e2e/app-dir/use-cache-og-image-top-level-await/use-cache-og-image-top-level-await.test.ts` β€” `describe('use-cache-og-image-top-level-await', () => { const { next, isNextStart } = nextTestSetup({ files: __dirname, skipStart: true, }) if (isNextStart) { beforeAll(async () => { await next.build({ args: ['--experimental-build-mode', 'compile'] }) }) it('should prerender a page whose opengraph image uses a top-level await', async () => { const { exitCode, cliOutput } = await next.build({ args: [ '--experimental-build-mode', 'generate', '--debug-build-paths', 'app/[slug]/page.tsx,app/[slug]/opengraph-image.tsx', ], }) expect(cliOutput).not.toContain( 'Unexpected cache miss after cache warming phase' ) expect(cliOutput).not.toContain( 'Next.js encountered uncached or runtime data in `generateMetadata()`' ) expect(exitCode).toBe(0) // The image route uses generateStaticParams, so the build is expected // to prerender it for each param. expect(cliOutput).toMatch(/● \/first-post\/opengraph-image/) expect(cliOutput).toMatch(/● \/second-post\/opengraph-image/) }) } else { beforeAll(async () => { await next.start() }) it('should render a page whose opengraph image uses a top-level await', async () => { const $ = await next.render$('/first-post') expect($('article').text()).toBe('First Post') const res = await next.fetch('/first-post/opengraph-image') expect(res.status).toBe(200) expect(res.headers.get('content-type')).toBe('image/png') }) } })` - ID 58: `test/e2e/app-dir/use-cache-output-export/use-cache-output-export.test.ts` β€” `describe('use-cache-output-export', () => { const { next, isNextStart } = nextTestSetup({ files: __dirname, skipStart: process.env.NEXT_TEST_MODE !== 'dev', }) if (process.env.__NEXT_CACHE_COMPONENTS === 'true') { return it.skip('for PPR', () => { // PPR is not compatible with `output: 'export'`. }) } it('should work', async () => { let html: string let server: Server | undefined if (isNextStart) { const { cliOutput } = await next.build() expect(cliOutput).not.toInclude( 'Server Actions are not supported with static export.' ) server = await startCleanStaticServer(join(next.testDir, 'out')) const { port } = server.address() as AddressInfo html = await renderViaHTTP(port, '/') } else { html = await next.render('/') } expect(html).toMatch(/<p>[0,1]\.\d+<\/p>/) if (server) { await new Promise((resolve) => server.close(resolve)) } }) })` </details> <details> <summary>Deployment evidence for the additional scopes</summary> - `test/e2e/app-dir/app-prefetch/prefetching.stale-times.test.ts` β€” `describe('app dir - prefetching (custom staleTime)', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710370); Cache Components excluded by manifest. - `test/e2e/app-dir/cache-components-errors/module-scope.test.ts` β€” `describe('Lazy Module Init', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710410); Cache Components excluded by manifest. - `test/e2e/app-dir/cache-components/cache-components.params.test.ts` β€” `describe('cache-components', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710449); Cache Components excluded by manifest. - `test/e2e/app-dir/instant-validation-static-shells/instant-validation-static-shells.test.ts` β€” `describe('instant validation - opting out of static shells', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710456), [cache](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710457). - `test/e2e/app-dir/use-cache-output-export/use-cache-output-export.test.ts` β€” `describe('use-cache-output-export', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710496), [cache](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710503); Cache Components explicitly skipped for PPR. </details> <!-- NEXT_JS_LLM --> --- .../app-client-cache/client-cache.original.test.ts | 3 --- .../app-dir/app-custom-cache-handler/index.test.ts | 12 ------------ .../app-prefetch/prefetching.stale-times.test.ts | 3 --- .../app-root-params-getters/use-cache.test.ts | 6 +++--- .../cache-components-errors/module-scope.test.ts | 3 --- .../cache-components.connection.test.ts | 3 --- .../cache-components.cookies.test.ts | 3 --- .../cache-components/cache-components.date.test.ts | 3 --- .../cache-components.draft-mode.test.ts | 3 --- .../cache-components.node-crypto.test.ts | 3 --- .../cache-components/cache-components.params.test.ts | 3 --- .../cache-components/cache-components.random.test.ts | 3 --- .../cache-components/cache-components.routes.test.ts | 3 --- .../cache-components/cache-components.search.test.ts | 3 --- .../cache-components/cache-components.test.ts | 3 --- .../cache-components.web-crypto.test.ts | 3 --- .../instant-validation-static-shells.test.ts | 3 --- .../non-rsc-router-prefetch.test.ts | 3 --- .../use-cache-infinity-profile.test.ts | 3 --- .../use-cache-og-image-top-level-await.test.ts | 4 ---- .../use-cache-output-export.test.ts | 3 --- 21 files changed, 3 insertions(+), 73 deletions(-) diff --git a/test/e2e/app-dir/app-client-cache/client-cache.original.test.ts b/test/e2e/app-dir/app-client-cache/client-cache.original.test.ts index c771144a81e8..9b95be2ccb34 100644 --- a/test/e2e/app-dir/app-client-cache/client-cache.original.test.ts +++ b/test/e2e/app-dir/app-client-cache/client-cache.original.test.ts @@ -9,9 +9,6 @@ import { import path from 'path' // This preserves existing tests for the 30s/5min heuristic (previous router defaults) -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// Assertions don't apply to deploy mode (output differs vs. local Next.js server). -// @force-gate !deploy describe('app dir client cache semantics (30s/5min)', () => { const { next, isNextDev } = nextTestSetup({ files: path.join(__dirname, 'fixtures', 'regular'), diff --git a/test/e2e/app-dir/app-custom-cache-handler/index.test.ts b/test/e2e/app-dir/app-custom-cache-handler/index.test.ts index c4a63985ff81..b4dc1d964e46 100644 --- a/test/e2e/app-dir/app-custom-cache-handler/index.test.ts +++ b/test/e2e/app-dir/app-custom-cache-handler/index.test.ts @@ -30,9 +30,6 @@ function runTests( }) } -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// It likely asserts local CLI or runtime output that deploy tests do not expose. -// @force-gate !deploy describe('app-dir - custom-cache-handler - cjs', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, @@ -44,9 +41,6 @@ describe('app-dir - custom-cache-handler - cjs', () => { runTests('cjs module exports', { next, isNextDev }) }) -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// It likely asserts local CLI or runtime output that deploy tests do not expose. -// @force-gate !deploy describe('app-dir - custom-cache-handler - cjs-default-export', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, @@ -58,9 +52,6 @@ describe('app-dir - custom-cache-handler - cjs-default-export', () => { runTests('cjs default export', { next, isNextDev }) }) -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// It likely asserts local CLI or runtime output that deploy tests do not expose. -// @force-gate !deploy describe('app-dir - custom-cache-handler - esm', () => { const { next, isNextDev } = nextTestSetup({ files: { @@ -82,9 +73,6 @@ describe('app-dir - custom-cache-handler - esm', () => { runTests('esm default export', { next, isNextDev }) }) -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// It likely asserts local CLI or runtime output that deploy tests do not expose. -// @force-gate !deploy describe('app-dir - custom-cache-handler - esm import.meta.resolve', () => { const { next, isNextDev } = nextTestSetup({ files: { diff --git a/test/e2e/app-dir/app-prefetch/prefetching.stale-times.test.ts b/test/e2e/app-dir/app-prefetch/prefetching.stale-times.test.ts index 2ae70ed5c04b..5cb90100e39f 100644 --- a/test/e2e/app-dir/app-prefetch/prefetching.stale-times.test.ts +++ b/test/e2e/app-dir/app-prefetch/prefetching.stale-times.test.ts @@ -3,9 +3,6 @@ import { createRouterAct } from 'router-act' import { createTimeController } from './test-utils' import { join } from 'path' -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// No deploy-specific incompatibility is documented. -// @force-gate !deploy describe('app dir - prefetching (custom staleTime)', () => { const { next, isNextDev } = nextTestSetup({ files: { diff --git a/test/e2e/app-dir/app-root-params-getters/use-cache.test.ts b/test/e2e/app-dir/app-root-params-getters/use-cache.test.ts index 46c2db957c3a..0399b257decf 100644 --- a/test/e2e/app-dir/app-root-params-getters/use-cache.test.ts +++ b/test/e2e/app-dir/app-root-params-getters/use-cache.test.ts @@ -380,15 +380,15 @@ describe('app-root-param-getters - cache - at build', () => { } }) -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// In deploy mode, concurrent requests could hit different lambdas. -// @force-gate !deploy describe('app-root-param-getters - cache dedup with root params', () => { const { next, isNextDev } = nextTestSetup({ files: join(__dirname, 'fixtures', 'use-cache-dedup'), }) it('should dedupe same root params and isolate different root params', async () => { + // In deploy mode, this can flake if Fluid routes concurrent requests + // to different function instances: each instance may independently + // compute a value before the cache is populated. // Three concurrent requests: ca/en, ca/fr, ca/fr. const [$en, $fr1, $fr2] = await Promise.all([ next.render$('/ca/en'), diff --git a/test/e2e/app-dir/cache-components-errors/module-scope.test.ts b/test/e2e/app-dir/cache-components-errors/module-scope.test.ts index 55e4b1bd7a8d..8e4dc98a077e 100644 --- a/test/e2e/app-dir/cache-components-errors/module-scope.test.ts +++ b/test/e2e/app-dir/cache-components-errors/module-scope.test.ts @@ -1,8 +1,5 @@ import { nextTestSetup } from 'e2e-utils' -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// It likely asserts local CLI or runtime output that deploy tests do not expose. -// @force-gate !deploy describe('Lazy Module Init', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname + '/fixtures/lazy-module-init', diff --git a/test/e2e/app-dir/cache-components/cache-components.connection.test.ts b/test/e2e/app-dir/cache-components/cache-components.connection.test.ts index d3a58bffc996..a3c70b86e7cc 100644 --- a/test/e2e/app-dir/cache-components/cache-components.connection.test.ts +++ b/test/e2e/app-dir/cache-components/cache-components.connection.test.ts @@ -1,8 +1,5 @@ import { nextTestSetup } from 'e2e-utils' -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// No deploy-specific incompatibility is documented. -// @force-gate !deploy describe('cache-components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, diff --git a/test/e2e/app-dir/cache-components/cache-components.cookies.test.ts b/test/e2e/app-dir/cache-components/cache-components.cookies.test.ts index 40288d57ae26..52a2ebea4890 100644 --- a/test/e2e/app-dir/cache-components/cache-components.cookies.test.ts +++ b/test/e2e/app-dir/cache-components/cache-components.cookies.test.ts @@ -1,8 +1,5 @@ import { nextTestSetup } from 'e2e-utils' -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// It likely asserts local CLI or runtime output that deploy tests do not expose. -// @force-gate !deploy describe('cache-components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, diff --git a/test/e2e/app-dir/cache-components/cache-components.date.test.ts b/test/e2e/app-dir/cache-components/cache-components.date.test.ts index 814c8afdd436..b10eff2322b3 100644 --- a/test/e2e/app-dir/cache-components/cache-components.date.test.ts +++ b/test/e2e/app-dir/cache-components/cache-components.date.test.ts @@ -1,9 +1,6 @@ import { nextTestSetup } from 'e2e-utils' import expect from 'expect' -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// It likely asserts local CLI or runtime output that deploy tests do not expose. -// @force-gate !deploy describe('cache-components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, diff --git a/test/e2e/app-dir/cache-components/cache-components.draft-mode.test.ts b/test/e2e/app-dir/cache-components/cache-components.draft-mode.test.ts index af5a2a4c2282..9264c263ee09 100644 --- a/test/e2e/app-dir/cache-components/cache-components.draft-mode.test.ts +++ b/test/e2e/app-dir/cache-components/cache-components.draft-mode.test.ts @@ -1,8 +1,5 @@ import { nextTestSetup } from 'e2e-utils' -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// It likely asserts local CLI or runtime output that deploy tests do not expose. -// @force-gate !deploy describe('cache-components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, diff --git a/test/e2e/app-dir/cache-components/cache-components.node-crypto.test.ts b/test/e2e/app-dir/cache-components/cache-components.node-crypto.test.ts index 9c56f2af51cf..2a839f318f8b 100644 --- a/test/e2e/app-dir/cache-components/cache-components.node-crypto.test.ts +++ b/test/e2e/app-dir/cache-components/cache-components.node-crypto.test.ts @@ -1,8 +1,5 @@ import { nextTestSetup } from 'e2e-utils' -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// It likely asserts local CLI or runtime output that deploy tests do not expose. -// @force-gate !deploy describe('cache-components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, diff --git a/test/e2e/app-dir/cache-components/cache-components.params.test.ts b/test/e2e/app-dir/cache-components/cache-components.params.test.ts index 712abb817291..46a57d1e5e79 100644 --- a/test/e2e/app-dir/cache-components/cache-components.params.test.ts +++ b/test/e2e/app-dir/cache-components/cache-components.params.test.ts @@ -1,9 +1,6 @@ import { nextTestSetup } from 'e2e-utils' // cSpell:words lowcard highcard -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// It likely asserts local CLI or runtime output that deploy tests do not expose. -// @force-gate !deploy describe('cache-components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, diff --git a/test/e2e/app-dir/cache-components/cache-components.random.test.ts b/test/e2e/app-dir/cache-components/cache-components.random.test.ts index 2daf96accae9..077eca446938 100644 --- a/test/e2e/app-dir/cache-components/cache-components.random.test.ts +++ b/test/e2e/app-dir/cache-components/cache-components.random.test.ts @@ -1,8 +1,5 @@ import { nextTestSetup } from 'e2e-utils' -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// It likely asserts local CLI or runtime output that deploy tests do not expose. -// @force-gate !deploy describe('cache-components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, diff --git a/test/e2e/app-dir/cache-components/cache-components.routes.test.ts b/test/e2e/app-dir/cache-components/cache-components.routes.test.ts index d6f5b40710e9..abfd0d71e73e 100644 --- a/test/e2e/app-dir/cache-components/cache-components.routes.test.ts +++ b/test/e2e/app-dir/cache-components/cache-components.routes.test.ts @@ -1,8 +1,5 @@ import { nextTestSetup } from 'e2e-utils' -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// It likely asserts local CLI or runtime output that deploy tests do not expose. -// @force-gate !deploy describe('cache-components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, diff --git a/test/e2e/app-dir/cache-components/cache-components.search.test.ts b/test/e2e/app-dir/cache-components/cache-components.search.test.ts index 2a35e0625378..83a14d301b55 100644 --- a/test/e2e/app-dir/cache-components/cache-components.search.test.ts +++ b/test/e2e/app-dir/cache-components/cache-components.search.test.ts @@ -1,8 +1,5 @@ import { nextTestSetup } from 'e2e-utils' -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// No deploy-specific incompatibility is documented. -// @force-gate !deploy describe('cache-components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, diff --git a/test/e2e/app-dir/cache-components/cache-components.test.ts b/test/e2e/app-dir/cache-components/cache-components.test.ts index d0cc5a3d2b6d..541253a0aa61 100644 --- a/test/e2e/app-dir/cache-components/cache-components.test.ts +++ b/test/e2e/app-dir/cache-components/cache-components.test.ts @@ -5,9 +5,6 @@ import { computeCacheBustingSearchParam } from 'next/dist/shared/lib/router/util import cheerio from 'cheerio' import { fetchViaHTTP, findPort } from 'next-test-utils' -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// It likely asserts local CLI or runtime output that deploy tests do not expose. -// @force-gate !deploy describe('cache-components', () => { const { next, isNextDev, isNextStart } = nextTestSetup({ files: __dirname, diff --git a/test/e2e/app-dir/cache-components/cache-components.web-crypto.test.ts b/test/e2e/app-dir/cache-components/cache-components.web-crypto.test.ts index 09ef2db50685..8dba7deb2d16 100644 --- a/test/e2e/app-dir/cache-components/cache-components.web-crypto.test.ts +++ b/test/e2e/app-dir/cache-components/cache-components.web-crypto.test.ts @@ -1,8 +1,5 @@ import { nextTestSetup } from 'e2e-utils' -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// It likely asserts local CLI or runtime output that deploy tests do not expose. -// @force-gate !deploy describe('cache-components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, diff --git a/test/e2e/app-dir/instant-validation-static-shells/instant-validation-static-shells.test.ts b/test/e2e/app-dir/instant-validation-static-shells/instant-validation-static-shells.test.ts index be1e6be451d4..9ad2b982ec10 100644 --- a/test/e2e/app-dir/instant-validation-static-shells/instant-validation-static-shells.test.ts +++ b/test/e2e/app-dir/instant-validation-static-shells/instant-validation-static-shells.test.ts @@ -2,9 +2,6 @@ import { nextTestSetup } from 'e2e-utils' import { waitForNoErrorToast } from 'next-test-utils' import { join } from 'node:path' -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// It likely asserts local CLI or runtime output that deploy tests do not expose. -// @force-gate !deploy describe('instant validation - opting out of static shells', () => { const { next, isNextDev } = nextTestSetup({ files: join(__dirname, 'fixtures', 'valid'), diff --git a/test/e2e/app-dir/non-rsc-router-prefetch/non-rsc-router-prefetch.test.ts b/test/e2e/app-dir/non-rsc-router-prefetch/non-rsc-router-prefetch.test.ts index d38eaa1df6ef..feb29ba5e9ab 100644 --- a/test/e2e/app-dir/non-rsc-router-prefetch/non-rsc-router-prefetch.test.ts +++ b/test/e2e/app-dir/non-rsc-router-prefetch/non-rsc-router-prefetch.test.ts @@ -4,9 +4,6 @@ import { RSC_HEADER, } from 'next/dist/client/components/app-router-headers' -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// No deploy-specific incompatibility is documented. -// @force-gate !deploy describe('non-rsc-router-prefetch', () => { const { next } = nextTestSetup({ files: __dirname, diff --git a/test/e2e/app-dir/use-cache-infinity-profile/use-cache-infinity-profile.test.ts b/test/e2e/app-dir/use-cache-infinity-profile/use-cache-infinity-profile.test.ts index 4ae67724464f..bbf9b525ec61 100644 --- a/test/e2e/app-dir/use-cache-infinity-profile/use-cache-infinity-profile.test.ts +++ b/test/e2e/app-dir/use-cache-infinity-profile/use-cache-infinity-profile.test.ts @@ -3,9 +3,6 @@ import { nextTestSetup } from 'e2e-utils' const uuidRegExp = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/ -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// Deployment platforms provide their own cache handlers. -// @force-gate !deploy describe('use-cache-infinity-profile', () => { const { next, isNextStart } = nextTestSetup({ files: __dirname, diff --git a/test/e2e/app-dir/use-cache-og-image-top-level-await/use-cache-og-image-top-level-await.test.ts b/test/e2e/app-dir/use-cache-og-image-top-level-await/use-cache-og-image-top-level-await.test.ts index cd8cc02e9162..5cd34efa05e7 100644 --- a/test/e2e/app-dir/use-cache-og-image-top-level-await/use-cache-og-image-top-level-await.test.ts +++ b/test/e2e/app-dir/use-cache-og-image-top-level-await/use-cache-og-image-top-level-await.test.ts @@ -1,9 +1,5 @@ import { nextTestSetup } from 'e2e-utils' -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// The prerendered output can't be observed in a deployment, and without -// it nothing distinguishes broken from fixed behavior. -// @force-gate !deploy describe('use-cache-og-image-top-level-await', () => { const { next, isNextStart } = nextTestSetup({ files: __dirname, diff --git a/test/e2e/app-dir/use-cache-output-export/use-cache-output-export.test.ts b/test/e2e/app-dir/use-cache-output-export/use-cache-output-export.test.ts index 3bb551cd31f2..4f5c3eabba70 100644 --- a/test/e2e/app-dir/use-cache-output-export/use-cache-output-export.test.ts +++ b/test/e2e/app-dir/use-cache-output-export/use-cache-output-export.test.ts @@ -3,9 +3,6 @@ import { renderViaHTTP, startCleanStaticServer } from 'next-test-utils' import { join } from 'path' import { AddressInfo, Server } from 'net' -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// It likely asserts local CLI or runtime output that deploy tests do not expose. -// @force-gate !deploy describe('use-cache-output-export', () => { const { next, isNextStart } = nextTestSetup({ files: __dirname, From f41cc4dba59120fbf8f4837caa838b1b167ab2d7 Mon Sep 17 00:00:00 2001 From: Jamiboy Mohammad <jamiboym@gmail.com> Date: Tue, 15 Sep 2026 10:29:05 -0700 Subject: [PATCH 10/14] test: enable verified app-router deploy tests (#98524) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Enable the same 33 previously selected deployment-test scopes across 32 app-router test files, now in a stack rooted on canary. Remove 33 `skipDeployment` options and their obsolete skip guards. Other mode, bundler, middleware, and Cache Components exclusions remain in place. This preserves the selection with passing evidence from the previous deployment runs. No additional candidate scopes are enabled; excluded variants are not counted as deployment coverage. ## Verification - All selected test registration names and assertion bodies match the previous enabled revision, checked by AST comparison. - Verified that the canary diff contains only the inventoried exclusions and their obsolete skip plumbing; other exclusions are preserved. - Formatting and lint passed; 77 gate infrastructure unit tests passed. - Full local bootstrap was blocked by missing package-level dependencies in the temporary worktree. Fresh deployment execution on these rewritten commits remains to be verified in CI. <details> <summary>Preserved scope inventory (33)</summary> - ID 78: `test/e2e/app-dir/actions-allowed-origins/app-action-allowed-origins.test.ts` β€” `describe('app-dir action allowed origins', () => { const { next } = nextTestSetup({ files: join(__dirname, 'safe-origins'), dependencies: { 'server-only': 'latest', }, // An arbitrary & random port. forcedPort: 'random', }) it('should pass if localhost is set as a safe origin', async function () { const browser = await next.browser('/') await browser.elementByCss('button').click() await check(async () => { return await browser.elementByCss('#res').text() }, 'hi') }) })` - ID 80: `test/e2e/app-dir/actions-allowed-origins/app-action-opaque-origin.test.ts` β€” `describe('app-dir action allowed from opaque origins', () => { const { next } = nextTestSetup({ files: join(__dirname, 'opaque-origin'), env: { NEXT_TEST_ALLOW_OPAQUE_ORIGIN: '1', }, }) it('should succeed on submission', async function () { const browser = await next.browser('/sandboxed') await browser.elementByCss('input[type="submit"]').click() await retry(async () => { expect(await browser.elementByCss('output').text()).toEqual( 'Action Invoked' ) }) }) })` - ID 82: `test/e2e/app-dir/app-a11y/index.test.ts` β€” `describe('app a11y features', () => { const { next } = nextTestSetup({ files: __dirname, packageJson: {}, }) describe('route announcer', () => { async function getAnnouncerContent(browser: Playwright) { return browser.eval( `document.getElementsByTagName('next-route-announcer')[0]?.shadowRoot.childNodes[0]?.innerHTML` ) } it('should not announce the initital title', async () => { const browser = await next.browser('/page-with-h1') await check(() => getAnnouncerContent(browser), '') }) it('should announce document.title changes', async () => { const browser = await next.browser('/page-with-h1') await browser.elementById('page-with-title').click() await check(() => getAnnouncerContent(browser), 'page-with-title') }) it('should announce h1 changes', async () => { const browser = await next.browser('/page-with-h1') await browser.elementById('noop-layout-page-1').click() await check(() => getAnnouncerContent(browser), 'noop-layout/page-1') }) it('should announce route changes when h1 changes inside an inner layout', async () => { const browser = await next.browser('/noop-layout/page-1') await browser.elementById('noop-layout-page-2').click() await check(() => getAnnouncerContent(browser), 'noop-layout/page-2') }) }) })` - ID 84: `test/e2e/app-dir/app-rendering/rendering.test.ts` β€” `describe('app dir rendering', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) it('should serve app/page.server.js at /', async () => { const html = await next.render('/') expect(html).toContain('app/page.server.js') }) describe('SSR only', () => { it('should run data in layout and page', async () => { const $ = await next.render$('/ssr-only/nested') expect($('#layout-message').text()).toBe('hello from layout') expect($('#page-message').text()).toBe('hello from page') }) it('should run data fetch in parallel', async () => { const startTime = Date.now() const $ = await next.render$('/ssr-only/slow') const endTime = Date.now() const duration = endTime - startTime // Each part takes 5 seconds so it should be below 10 seconds // Using 7 seconds to ensure external factors causing slight slowness don't fail the tests expect(duration).toBeLessThan(10_000) expect($('#slow-layout-message').text()).toBe('hello from slow layout') expect($('#slow-page-message').text()).toBe('hello from slow page') }) }) describe('static only', () => { it('should run data in layout and page', async () => { const $ = await next.render$('/static-only/nested') expect($('#layout-message').text()).toBe('hello from layout') expect($('#page-message').text()).toBe('hello from page') }) it(`should run data in parallel ${ isNextDev ? 'during development' : 'and use cached version for production' }`, async () => { // const startTime = Date.now() const $ = await next.render$('/static-only/slow') // const endTime = Date.now() // const duration = endTime - startTime // Each part takes 5 seconds so it should be below 10 seconds // Using 7 seconds to ensure external factors causing slight slowness don't fail the tests // TODO: cache static props in prod // expect(duration < (isDev ? 7000 : 2000)).toBe(true) // expect(duration < 7000).toBe(true) expect($('#slow-layout-message').text()).toBe('hello from slow layout') expect($('#slow-page-message').text()).toBe('hello from slow page') }) }) describe('ISR', () => { it('should revalidate the page when revalidate is configured', async () => { const getPage = async () => { const res = await next.fetch('isr-multiple/nested') const html = await res.text() return { $: cheerio.load(html), cacheHeader: res.headers['x-nextjs-cache'], } } const { $ } = await getPage() expect($('#layout-message').text()).toBe('hello from layout') expect($('#page-message').text()).toBe('hello from page') const layoutNow = $('#layout-now').text() const pageNow = $('#page-now').text() await waitFor(2000) // TODO: implement // Trigger revalidate // const { cacheHeader: revalidateCacheHeader } = await getPage() // expect(revalidateCacheHeader).toBe('STALE') // TODO: implement const { $: $revalidated /* cacheHeader: revalidatedCacheHeader */ } = await getPage() // expect(revalidatedCacheHeader).toBe('REVALIDATED') const layoutNowRevalidated = $revalidated('#layout-now').text() const pageNowRevalidated = $revalidated('#page-now').text() // Expect that the `Date.now()` is different as the page have been regenerated expect(layoutNow).not.toBe(layoutNowRevalidated) expect(pageNow).not.toBe(pageNowRevalidated) }) }) // TODO: implement describe.skip('mixed static and dynamic', () => { it('should generate static data during build and use it', async () => { const getPage = async () => { const $ = await next.render$('isr-ssr-combined/nested') return { $, } } const { $ } = await getPage() expect($('#layout-message').text()).toBe('hello from layout') expect($('#page-message').text()).toBe('hello from page') const layoutNow = $('#layout-now').text() const pageNow = $('#page-now').text() const { $: $second } = await getPage() const layoutNowSecond = $second('#layout-now').text() const pageNowSecond = $second('#page-now').text() // Expect that the `Date.now()` is different as it came from getServerSideProps expect(layoutNow).not.toBe(layoutNowSecond) // Expect that the `Date.now()` is the same as it came from getStaticProps expect(pageNow).toBe(pageNowSecond) }) }) })` - ID 86: `test/e2e/app-dir/app-validation/validation.test.ts` β€” `describe('app dir - validation', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should error when passing invalid router state tree', async () => { const stateTree1 = JSON.stringify(['', '']) const stateTree2 = JSON.stringify(['', {}]) const headers1 = { rsc: '1', 'next-router-state-tree': stateTree1, } const headers2 = { rsc: '1', 'next-router-state-tree': stateTree2, } const url1 = new URL('/', 'http://localhost') const url2 = new URL('/', 'http://localhost') // Add cache busting search param for both requests const cacheBustingParam1 = await computeCacheBustingSearchParam( undefined, undefined, stateTree1, undefined ) const cacheBustingParam2 = await computeCacheBustingSearchParam( undefined, undefined, stateTree2, undefined ) if (cacheBustingParam1) { url1.searchParams.set('_rsc', cacheBustingParam1) } if (cacheBustingParam2) { url2.searchParams.set('_rsc', cacheBustingParam2) } const res = await next.fetch(url1.toString(), { headers: headers1 }) expect(res.status).toBe(500) const res2 = await next.fetch(url2.toString(), { headers: headers2 }) expect(res2.status).toBe(200) }) it('should generate distinct cache-busting params for known colliding RSC variants', async () => { const stateTree = '%5B%22%22%2C%7B%7D%5D' const fullRequestHash = await computeCacheBustingSearchParam( undefined, undefined, stateTree, undefined ) const prefetchRequestHash = await computeCacheBustingSearchParam( '1', '/_tree', stateTree, '/pcsta0' ) expect(fullRequestHash).toHaveLength(16) expect(prefetchRequestHash).toHaveLength(16) expect(fullRequestHash).not.toBe(prefetchRequestHash) }) it('should accept legacy cache-busting params on plain HTTP requests', async () => { const stateTree = '%5B%22%22%2C%7B%7D%5D' const url = new URL('/', 'http://localhost') const headers = { rsc: '1', 'next-router-state-tree': stateTree, } url.searchParams.set( '_rsc', computeLegacyCacheBustingSearchParam( undefined, undefined, stateTree, undefined ) ) const res = await next.fetch(url.toString(), { headers, redirect: 'manual', }) expect(res.status).toBe(200) }) })` - ID 87: `test/e2e/app-dir/async-component-preload/async-component-preload.test.ts` β€” `describe('async-component-preload', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should handle redirect in an async page', async () => { const browser = await next.browser('/') expect(await browser.waitForElementByCss('#success').text()).toBe('Success') }) })` - ID 90: `test/e2e/app-dir/client-reference-side-effects/client-reference-side-effects.test.ts` β€” `describe('client-reference-side-effects', () => { const { next, isTurbopack } = nextTestSetup({ files: __dirname, }) it('side effect behavior when only importing', async () => { const browser = await next.browser('/imported') expect(await browser.elementByCss('body').text()).toContain('Server') let client = await browser.eval('window.client') let client_sideeffect_reexport = await browser.eval( 'window.client_sideeffect_reexport' ) let client_sideeffect_only = await browser.eval( 'window.client_sideeffect_only' ) // No client references are rendered, so nothing is executed. expect(client).toBeUndefined() expect(client_sideeffect_reexport).toBeUndefined() expect(client_sideeffect_only).toBeUndefined() }) it('side effect behavior when rendering', async () => { const browser = await next.browser('/rendered') const body = await browser.elementByCss('body').text() expect(body).toContain('Server') expect(body).toContain('client component') let client = await browser.eval('window.client') let client_sideeffect_reexport = await browser.eval( 'window.client_sideeffect_reexport' ) let client_sideeffect_only = await browser.eval( 'window.client_sideeffect_only' ) expect(client).toBeTrue() expect(client_sideeffect_reexport).toBeTrue() if (isTurbopack) { expect(client_sideeffect_only).toBeUndefined() } else { // Webpack eagerly initializes all client reference modules once at least one of them is // rendered. expect(client_sideeffect_only).toBeTrue() } }) })` - ID 92: `test/e2e/app-dir/duplicate-layout-components/duplicate-layout-components.test.ts` β€” `describe('app dir - duplicate layout components', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should not duplicate layout elements when navigating to 404', async () => { const browser = await next.browser('/solutions/404') // Verify counts haven't changed - no duplication expect((await browser.elementsByCss('body')).length).toBe(1) expect((await browser.elementsByCss('#header')).length).toBe(1) expect((await browser.elementsByCss('#footer')).length).toBe(1) }) })` - ID 93: `test/e2e/app-dir/dynamic-data/dynamic-data.test.ts` β€” `describe('dynamic-data', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname + '/fixtures/main', }) it('should render the dynamic apis dynamically when used in a top-level scope', async () => { const $ = await next.render$( '/top-level?foo=foosearch', {}, { headers: { fooheader: 'foo header value', cookie: 'foocookie=foo cookie value', }, } ) if (isNextDev) { // in dev we expect the entire page to be rendered at runtime expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') } else if (process.env.__NEXT_CACHE_COMPONENTS) { // in PPR we expect the shell to be rendered at build and the page to be rendered at runtime expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at runtime') } else { // in static generation we expect the entire page to be rendered at runtime expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') } expect($('#headers .fooheader').text()).toBe('foo header value') expect($('#cookies .foocookie').text()).toBe('foo cookie value') expect($('#searchparams .foo').text()).toBe('foosearch') }) it('should render the dynamic apis dynamically when used in a top-level scope with force dynamic', async () => { const $ = await next.render$( '/force-dynamic?foo=foosearch', {}, { headers: { fooheader: 'foo header value', cookie: 'foocookie=foo cookie value', }, } ) if (isNextDev) { // in dev we expect the entire page to be rendered at runtime expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') } else if (process.env.__NEXT_CACHE_COMPONENTS) { // @TODO this should actually be build but there is a bug in how we do segment level dynamic in PPR at the moment // see note in create-component-tree expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') } else { // in static generation we expect the entire page to be rendered at runtime expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') } expect($('#headers .fooheader').text()).toBe('foo header value') expect($('#cookies .foocookie').text()).toBe('foo cookie value') expect($('#searchparams .foo').text()).toBe('foosearch') }) it('should render empty objects for dynamic APIs when rendering with force-static', async () => { const $ = await next.render$( '/force-static?foo=foosearch', {}, { headers: { fooheader: 'foo header value', cookie: 'foocookie=foo cookie value', }, } ) if (isNextDev) { // in dev we expect the entire page to be rendered at runtime expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') } else if (process.env.__NEXT_CACHE_COMPONENTS) { // in PPR we expect the shell to be rendered at build and the page to be rendered at runtime expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') // we expect there to be a suspense boundary in fallback state expect($('#boundary').html()).toBeNull() } else { // in static generation we expect the entire page to be rendered at runtime expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') // we expect there to be no suspense boundary in fallback state expect($('#boundary').html()).toBeNull() } expect($('#headers .fooheader').html()).toBeNull() expect($('#cookies .foocookie').html()).toBeNull() expect($('#searchparams .foo').html()).toBeNull() }) it('should track searchParams access as dynamic when the Page is a client component', async () => { const $ = await next.render$( '/client-page?foo=foosearch', {}, { headers: { fooheader: 'foo header value', cookie: 'foocookie=foo cookie value', }, } ) if (isNextDev) { // in dev we expect the entire page to be rendered at runtime expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') // we don't assert the state of the fallback because it can depend on the timing // of when streaming starts and how fast the client references resolve } else if (process.env.__NEXT_CACHE_COMPONENTS) { // in PPR we expect the shell to be rendered at build and the page to be rendered at runtime expect($('#layout').text()).toBe('at buildtime') expect($('#page').text()).toBe('at runtime') // we expect there to be a suspense boundary in fallback state expect($('#boundary').html()).not.toBeNull() } else { // in static generation we expect the entire page to be rendered at runtime expect($('#layout').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') // we don't assert the state of the fallback because it can depend on the timing // of when streaming starts and how fast the client references resolve } expect($('#searchparams .foo').text()).toBe('foosearch') }) if (!isNextDev) { it('should track dynamic apis when rendering app routes', async () => { expect(next.cliOutput).toContain( `Caught Error: Dynamic server usage: Route /routes/url couldn't be rendered statically because it used \`request.url\`.` ) expect(next.cliOutput).toContain( `Caught Error: Dynamic server usage: Route /routes/next-url couldn't be rendered statically because it used \`nextUrl.toString\`.` ) }) } })` - ID 94: `test/e2e/app-dir/dynamic-href/dynamic-href.test.ts` β€” `describe('dynamic-href', () => { const { isNextDev: isDev, next } = nextTestSetup({ files: __dirname, }) if (isDev) { it('should error when using dynamic href.pathname in app dir', async () => { const browser = await next.browser('/object') await expect(browser).toDisplayRedbox(` { "description": "Dynamic href \`/object/[slug]\` found in <Link> while using the \`/app\` router, this is not supported. Read more: https://nextjs.org/docs/messages/app-dir-dynamic-href", "environmentLabel": null, "label": "Runtime Error", "source": "app/object/page.js (5:5) @ HomePage > 5 | <Link | ^", "stack": [ "HomePage app/object/page.js (5:5)", ], } `) // Fix error const pageContent = await next.readFile('app/object/page.js') await next.patchFile( 'app/object/page.js', pageContent.replace( "pathname: '/object/[slug]'", "pathname: '/object/slug'" ) ) expect(await browser.waitForElementByCss('#link').text()).toBe('to slug') // Navigate to new page await browser.elementByCss('#link').click() expect(await browser.waitForElementByCss('#pathname').text()).toBe( '/object/slug' ) expect(await browser.elementByCss('#slug').text()).toBe('1') }) it('should error when using dynamic href in app dir', async () => { const browser = await next.browser('/string') await expect(browser).toDisplayRedbox(` { "description": "Dynamic href \`/object/[slug]\` found in <Link> while using the \`/app\` router, this is not supported. Read more: https://nextjs.org/docs/messages/app-dir-dynamic-href", "environmentLabel": null, "label": "Runtime Error", "source": "app/string/page.js (5:5) @ HomePage > 5 | <Link id="link" href="/object/[slug]"> | ^", "stack": [ "HomePage app/string/page.js (5:5)", ], } `) }) } else { it('should not error on /object in prod', async () => { const browser = await next.browser('/object') expect(await browser.elementByCss('#link').text()).toBe('to slug') }) it('should not error on /string in prod', async () => { const browser = await next.browser('/string') expect(await browser.elementByCss('#link').text()).toBe('to slug') }) } })` - ID 95: `test/e2e/app-dir/dynamic-import-tree-shaking/dynamic-import-tree-shaking.test.ts` β€” `describe('dynamic-import-tree-shaking', () => { const { next, isNextStart, isTurbopack } = nextTestSetup({ files: __dirname, }) // Recursively read all .js files in a directory function getAllServerFiles(dir: string): string[] { const results: string[] = [] try { const entries = fs.readdirSync(dir, { withFileTypes: true }) for (const entry of entries) { const fullPath = path.join(dir, entry.name) if (entry.isDirectory()) { results.push(...getAllServerFiles(fullPath)) } else if (entry.name.endsWith('.js')) { results.push(fullPath) } } } catch { // directory doesn't exist } return results } async function getAllServerContent(): Promise<string> { const serverDir = path.join(next.testDir, '.next/server') const files = getAllServerFiles(serverDir) const contents = await Promise.all( files.map((f) => fs.promises.readFile(f, 'utf8')) ) return contents.join('\n') } // Verify that each page renders correctly (these should always pass in both dev and production) it('should render const destructure page', async () => { const $ = await next.render$('/const-destructure') expect($('div').text()).toContain('TREESHAKE_CONST_USED') }) it('should render var destructure page', async () => { const $ = await next.render$('/var-destructure') expect($('div').text()).toContain('TREESHAKE_VAR_USED') }) it('should render let destructure page', async () => { const $ = await next.render$('/let-destructure') expect($('div').text()).toContain('TREESHAKE_LET_USED') }) it('should render rename destructure page', async () => { const $ = await next.render$('/rename-destructure') expect($('div').text()).toContain('TREESHAKE_RENAME_USED') }) it('should render nested destructure page', async () => { const $ = await next.render$('/nested-destructure') expect($('div').text()).toContain('TREESHAKE_NESTED_USED') }) it('should render default destructure page', async () => { const $ = await next.render$('/default-destructure') expect($('div').text()).toContain('TREESHAKE_DEFAULT_USED') }) it('should render empty destructure page', async () => { const $ = await next.render$('/empty-destructure') expect($('div').text()).toContain('TREESHAKE_EMPTY_PAGE') }) it('should render member access page', async () => { const $ = await next.render$('/member-access') expect($('div').text()).toContain('TREESHAKE_MEMBER_USED') }) it('should render webpack-exports-comment page', async () => { const $ = await next.render$('/webpack-exports-comment') expect($('div').text()).toContain('TREESHAKE_COMMENT_USED') }) it('should render rest destructure page', async () => { const $ = await next.render$('/rest-destructure') expect($('div').text()).toContain('TREESHAKE_REST_USED') }) it('should render multiple imports page', async () => { const $ = await next.render$('/multiple-imports') expect($('div').text()).toContain('TREESHAKE_MULTI_A_USED') expect($('div').text()).toContain('TREESHAKE_MULTI_B_USED') }) it('should render reassign page', async () => { const $ = await next.render$('/reassign') expect($('div').text()).toContain('TREESHAKE_REASSIGN_USED') }) it('should render then-arrow-destructure page', async () => { const $ = await next.render$('/then-arrow-destructure') expect($('div').text()).toContain('TREESHAKE_THEN_ARROW_USED') }) it('should render then-function-destructure page', async () => { const $ = await next.render$('/then-function-destructure') expect($('div').text()).toContain('TREESHAKE_THEN_FUNC_USED') }) // Tree shaking assertions: unused exports should NOT be in the server bundle // Tree shaking is only enabled in production builds, so skip these in dev mode if (isNextStart) { it('should tree-shake unused export with const destructured dynamic import', async () => { const content = await getAllServerContent() expect(content).toContain('TREESHAKE_CONST_USED') expect(content).not.toContain('TREESHAKE_CONST_UNUSED') }) it('should tree-shake unused export with var destructured dynamic import', async () => { const content = await getAllServerContent() expect(content).toContain('TREESHAKE_VAR_USED') expect(content).not.toContain('TREESHAKE_VAR_UNUSED') }) it('should tree-shake unused export with let destructured dynamic import', async () => { const content = await getAllServerContent() expect(content).toContain('TREESHAKE_LET_USED') expect(content).not.toContain('TREESHAKE_LET_UNUSED') }) it('should tree-shake unused export with renamed destructured dynamic import', async () => { const content = await getAllServerContent() expect(content).toContain('TREESHAKE_RENAME_USED') expect(content).not.toContain('TREESHAKE_RENAME_UNUSED') }) it('should tree-shake unused export with nested destructured dynamic import', async () => { const content = await getAllServerContent() expect(content).toContain('TREESHAKE_NESTED_USED') expect(content).not.toContain('TREESHAKE_NESTED_UNUSED') }) it('should tree-shake unused export with default destructured dynamic import', async () => { const content = await getAllServerContent() expect(content).toContain('TREESHAKE_DEFAULT_USED') expect(content).not.toContain('TREESHAKE_DEFAULT_UNUSED') }) it('should tree-shake all exports with empty destructured dynamic import', async () => { const content = await getAllServerContent() // Side effects should still be included expect(content).toContain('TREESHAKE_EMPTY_SIDE_EFFECT') // But no exports should be included expect(content).not.toContain('TREESHAKE_EMPTY_USED') expect(content).not.toContain('TREESHAKE_EMPTY_UNUSED') }) it('should tree-shake unused export with webpackExports comment', async () => { const content = await getAllServerContent() expect(content).toContain('TREESHAKE_COMMENT_USED') expect(content).not.toContain('TREESHAKE_COMMENT_UNUSED') }) // Member access on dynamic import is only tree-shaken by Turbopack, not webpack if (isTurbopack) { it('should tree-shake unused export with member access on dynamic import', async () => { const content = await getAllServerContent() expect(content).toContain('TREESHAKE_MEMBER_USED') expect(content).not.toContain('TREESHAKE_MEMBER_UNUSED') }) } it('should NOT tree-shake with rest destructured dynamic import', async () => { const content = await getAllServerContent() expect(content).toContain('TREESHAKE_REST_USED') // rest elements prevent tree-shaking, so unused exports should still be present expect(content).toContain('TREESHAKE_REST_UNUSED') }) it('should tree-shake unused exports with multiple dynamic imports in one file', async () => { const content = await getAllServerContent() expect(content).toContain('TREESHAKE_MULTI_A_USED') expect(content).not.toContain('TREESHAKE_MULTI_A_UNUSED') expect(content).toContain('TREESHAKE_MULTI_B_USED') expect(content).not.toContain('TREESHAKE_MULTI_B_UNUSED') }) it('should NOT tree-shake with reassigned dynamic import', async () => { const content = await getAllServerContent() expect(content).toContain('TREESHAKE_REASSIGN_USED') // re-assignment prevents destructuring analysis, so unused exports should remain expect(content).toContain('TREESHAKE_REASSIGN_UNUSED') }) // .then() callback destructuring is only tree-shaken by Turbopack, not webpack if (isTurbopack) { it('should tree-shake unused export with .then() arrow destructured dynamic import', async () => { const content = await getAllServerContent() expect(content).toContain('TREESHAKE_THEN_ARROW_USED') expect(content).not.toContain('TREESHAKE_THEN_ARROW_UNUSED') }) it('should tree-shake unused export with .then() function destructured dynamic import', async () => { const content = await getAllServerContent() expect(content).toContain('TREESHAKE_THEN_FUNC_USED') expect(content).not.toContain('TREESHAKE_THEN_FUNC_UNUSED') }) } } })` - ID 96: `test/e2e/app-dir/dynamic-in-generate-params/index.test.ts` β€” `describe('app-dir - dynamic in generate params', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should render sitemap with generateSitemaps in force-dynamic config dynamically', async () => { const firstTime = await getLastModifiedTime(next, 'sitemap/0.xml') const secondTime = await getLastModifiedTime(next, 'sitemap/0.xml') expect(firstTime).not.toEqual(secondTime) }) it('should be able to call while generating multiple dynamic sitemaps', async () => { const res0 = await next.fetch('sitemap/0.xml') const res1 = await next.fetch('sitemap/1.xml') assertSitemapResponse(res0) assertSitemapResponse(res1) }) it('should be able to call fetch while generating multiple dynamic pages', async () => { const pageRes0 = await next.fetch('dynamic/0') const pageRes1 = await next.fetch('dynamic/1') expect(pageRes0.status).toBe(200) expect(pageRes1.status).toBe(200) }) })` - ID 97: `test/e2e/app-dir/dynamic/dynamic.test.ts` β€” `describe('app dir - next/dynamic', () => { const { next, isNextStart, isNextDev } = nextTestSetup({ files: __dirname, }) it('should handle ssr: false in pages when appDir is enabled', async () => { const $ = await next.render$('/legacy/no-ssr') expect($.html()).not.toContain('navigator') const browser = await next.browser('/legacy/no-ssr') expect(await browser.waitForElementByCss('#pure-client').text()).toContain( 'navigator' ) }) it('should handle next/dynamic in SSR correctly', async () => { const $ = await next.render$('/dynamic') // filter out the script const selector = 'body div' const serverContent = $(selector).text() // should load chunks generated via async import correctly with React.lazy expect(serverContent).toContain('next-dynamic lazy') // should support `dynamic` in both server and client components expect(serverContent).toContain('next-dynamic dynamic on server') expect(serverContent).toContain('next-dynamic dynamic on client') expect(serverContent).toContain('next-dynamic server import client') expect(serverContent).not.toContain('next-dynamic dynamic no ssr on client') }) it('should handle next/dynamic in hydration correctly', async () => { const browser = await next.browser('/dynamic') await browser.waitForElementByCss('#css-text-dynamic-no-ssr-client') expect( await browser.elementByCss('#css-text-dynamic-no-ssr-client').text() ).toBe('next-dynamic dynamic no ssr on client:suffix') }) it('should generate correct client manifest for dynamic chunks', async () => { const $ = await next.render$('/chunk-loading/server') expect($('h1').text()).toBe('hello') }) it('should render loading by default if loading is specified and loader is slow', async () => { const $ = await next.render$('/default-loading') // First render in dev should show loading, production build will resolve the content. expect($('body').text()).toContain( isNextDev ? 'Loading...' : 'This is a dynamically imported component' ) }) it('should not render loading by default', async () => { const $ = await next.render$('/default') expect($('#dynamic-component').text()).not.toContain('loading') }) it('should ignore next/dynamic in routes', async () => { const response = await next.fetch('/api') expect(await response.text()).toEqual('Hello function') }) it('should ignore next/dynamic in sitemap', async () => { const response = await next.fetch('/sitemap.xml') expect(await response.text()).toInclude('<changefreq>yearly</changefreq>') }) if (isNextDev) { it('should directly raise error when dynamic component error on server', async () => { const pagePath = 'app/default-loading/dynamic-component.js' const page = await next.readFile(pagePath) await next.patchFile( pagePath, page.replace('const isDevTest = false', 'const isDevTest = true') ) await retry(async () => { const { status } = await next.fetch('/default-loading') expect(status).toBe(200) }) }) } describe('no SSR', () => { it('should not render client component imported through ssr: false in client components in edge runtime', async () => { // noSSR should not show up in html const $ = await next.render$('/dynamic-mixed-ssr-false/client-edge') expect($('#server-false-client-module')).not.toContain( 'ssr-false-client-module-text' ) // noSSR should not show up in browser const browser = await next.browser('/dynamic-mixed-ssr-false/client-edge') expect( await browser.elementByCss('#ssr-false-client-module').text() ).toBe('ssr-false-client-module-text') // in the server bundle should not contain client component imported through ssr: false if (isNextStart) { const middlewareManifest = JSON.parse( await next.readFile('.next/server/middleware-manifest.json') ) const uniquePageFiles = [ ...new Set<string>( middlewareManifest.functions[ '/dynamic-mixed-ssr-false/client-edge/page' ].files ), ] for (const file of uniquePageFiles) { const contents = await next.readFile(path.join('.next', file)) expect(contents).not.toContain('ssr-false-client-module-text') } } }) it('should not render client component imported through ssr: false in client components', async () => { // noSSR should not show up in html const $ = await next.render$('/dynamic-mixed-ssr-false/client') expect($('#client-false-client-module')).not.toContain( 'ssr-false-client-module-text' ) // noSSR should not show up in browser const browser = await next.browser('/dynamic-mixed-ssr-false/client') expect( await browser.elementByCss('#ssr-false-client-module').text() ).toBe('ssr-false-client-module-text') // in the server bundle should not contain both server and client component imported through ssr: false if (isNextStart) { const pageServerChunk = await next.readFile( '.next/server/app/dynamic-mixed-ssr-false/client/page.js' ) expect(pageServerChunk).not.toContain('ssr-false-client-module-text') } }) it('should support dynamic import with accessing named exports from client component', async () => { const $ = await next.render$('/dynamic/named-export') expect($('#client-button').text()).toBe('this is a client button') }) it('should support dynamic import with TLA in client components', async () => { const $ = await next.render$('/dynamic/async-client') expect($('#client-button').text()).toBe( 'this is an async client button with SSR' ) expect($('#client-button-no-ssr').text()).toBe('') const browser = await next.browser('/dynamic/async-client') expect(await browser.elementByCss('#client-button').text()).toBe( 'this is an async client button with SSR' ) expect(await browser.elementByCss('#client-button-no-ssr').text()).toBe( 'this is an async client button' ) }) }) })` - ID 101: `test/e2e/app-dir/forbidden/default/forbidden-default.test.ts` β€” `describe('app dir - forbidden with default forbidden boundary', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) // TODO: error forbidden usage in root layout it.skip('should error on client forbidden from root layout in browser', async () => { const browser = await next.browser('/') await browser.elementByCss('#trigger-forbidden').click() if (isNextDev) { await waitForRedbox(browser) expect(await getRedboxDescription(browser)).toMatch( /forbidden\(\) is not allowed to use in root layout/ ) } }) // TODO: error forbidden usage in root layout it.skip('should error on server forbidden from root layout on server-side', async () => { const browser = await next.browser('/?root-forbidden=1') if (isNextDev) { await waitForRedbox(browser) expect(await getRedboxDescription(browser)).toBe( 'Error: forbidden() is not allowed to use in root layout' ) } }) it('should be able to navigate to page calling forbidden', async () => { const browser = await next.browser('/') await browser.elementByCss('#navigate-forbidden').click() await browser.waitForElementByCss('.next-error-h1') expect(await browser.elementByCss('h1').text()).toBe('403') expect(await browser.elementByCss('h2').text()).toBe( 'This page could not be accessed.' ) }) it('should be able to navigate to page with calling forbidden in metadata', async () => { const browser = await next.browser('/') await browser.elementByCss('#metadata-layout-forbidden').click() await browser.waitForElementByCss('.next-error-h1') expect(await browser.elementByCss('h1').text()).toBe('403') expect(await browser.elementByCss('h2').text()).toBe( 'This page could not be accessed.' ) }) it('should render default forbidden for group routes if forbidden is not defined', async () => { const browser = await next.browser('/group-dynamic/123') expect(await browser.elementByCss('#page').text()).toBe( 'group-dynamic [id]' ) await browser.loadPage(next.url + '/group-dynamic/403') await waitForNoRedbox(browser) await browser.waitForElementByCss('.group-root-layout') expect(await browser.elementByCss('.next-error-h1').text()).toBe('403') }) })` - ID 102: `test/e2e/app-dir/global-error/catch-all/index.test.ts` β€” `describe('app dir - global error - with catch-all route', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should render catch-all route correctly', async () => { expect(await next.render('/en/foo')).toContain('catch-all page') }) it('should render 404 page correctly', async () => { expect(await next.render('/en')).toContain('This page could not be found.') }) it('should render global error correctly', async () => { const browser = await next.browser('/en/error') const text = await browser.elementByCss('#global-error').text() expect(text).toMatchInlineSnapshot(`"global-error"`) }) })` - ID 103: `test/e2e/app-dir/global-error/layout-error/index.test.ts` β€” `describe('app dir - global error - layout error', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) it('should render global error for error in server components', async () => { const browser = await next.browser('/') if (isNextDev) { await expect(browser).toDisplayRedbox(` { "description": "layout error", "environmentLabel": "Server", "label": "Runtime Error", "source": "app/layout.js (2:9) @ layout > 2 | throw new Error('layout error') | ^", "stack": [ "layout app/layout.js (2:9)", ], } `) } expect(await browser.elementByCss('h1').text()).toBe('Global Error') expect(await browser.elementByCss('#error').text()).toBe( isNextDev ? 'Global error: layout error' : 'Global error: Minified React error #441; visit https://react.dev/errors/441 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.' ) expect(await browser.elementByCss('#digest').text()).toMatch(/\w+/) }) })` - ID 114: `test/e2e/app-dir/io/io.test.ts` β€” `describe('io with cache components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname + '/fixtures/cache-components', }) it('should make content after io() dynamic during prerender', async () => { const $ = await next.render$('/io-boundary') if (isNextDev) { // In dev mode everything renders at runtime expect($('#before').text()).toBe('at runtime') expect($('#after-io').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') } else { // In production with cache components, io() creates a dynamic // boundary. Content in the static shell is rendered at buildtime. // Content after io() is rendered at request time because the // hanging promise prevented it from executing during the build prerender. expect($('#before').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') expect($('#after-io').text()).toBe('at runtime') } }) it('should resolve immediately inside a "use cache" scope', async () => { const $ = await next.render$('/io-in-cache') if (isNextDev) { expect($('#cached-value').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') } else { // io() inside "use cache" is a no-op so the cached value is // computed at cache-fill time during the build expect($('#cached-value').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') } }) it('should work in pages router with getServerSideProps (CC)', async () => { const $ = await next.render$('/pages-gssp') expect($('#pages-content').text()).toBe('ok') }) it('should work in pages router with getStaticProps (CC)', async () => { const $ = await next.render$('/pages-gsp') expect($('#pages-content').text()).toBe('ok') }) it('should work in pages router with React.use() (CC)', async () => { const $ = await next.render$('/pages-use') expect($('#pages-content').text()).toBe('ok') }) })` - ID 115: `test/e2e/app-dir/io/io.test.ts` β€” `describe('io without cache components', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname + '/fixtures/default', }) it('should be a no-op during prerender without cache components', async () => { const $ = await next.render$('/io-boundary') if (isNextDev) { expect($('#before').text()).toBe('at runtime') expect($('#after-io').text()).toBe('at runtime') expect($('#page').text()).toBe('at runtime') } else { // Without cache components, io() resolves immediately during // prerendering so the entire page is fully static expect($('#before').text()).toBe('at buildtime') expect($('#after-io').text()).toBe('at buildtime') expect($('#page').text()).toBe('at buildtime') } }) it('should work in pages router with getServerSideProps', async () => { const $ = await next.render$('/pages-gssp') expect($('#pages-content').text()).toBe('ok') }) it('should work in pages router with getStaticProps', async () => { const $ = await next.render$('/pages-gsp') expect($('#pages-content').text()).toBe('ok') }) it('should work in pages router with React.use()', async () => { const $ = await next.render$('/pages-use') expect($('#pages-content').text()).toBe('ok') }) })` - ID 116: `test/e2e/app-dir/metadata-json-manifest/index.test.ts` β€” `describe('app-dir metadata-json-manifest', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should support metadata.json manifest', async () => { const response = await next.fetch('/manifest.json') expect(response.status).toBe(200) const json = await response.json() expect(json).toEqual({ name: 'My Next.js Application', short_name: 'Next.js App', description: 'An application built with Next.js', start_url: '/', }) }) })` - ID 117: `test/e2e/app-dir/metadata-suspense/index.test.ts` β€” `describe('app dir - metadata dynamic routes suspense', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should render metadata in head when root layout is wrapped with Suspense for bot requests', async () => { const $ = await next.render$('/', undefined, { headers: { 'User-Agent': 'Discordbot/2.0;', }, }) expect($('head title').text()).toBe('My title') expect($('head meta[name="application-name"]').attr('content')).toBe( 'suspense-app' ) // unique title expect($('title').length).toBe(1) }) })` - ID 119: `test/e2e/app-dir/metadata-warnings/metadata-warnings-with-metadatabase.test.ts` β€” `describe('app dir - metadata missing metadataBase', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, overrideFiles: { 'app/layout.js': ` export default function Layout({ children }) { return ( <div> {children} </div> ) } export const metadata = { metadataBase: new URL('https://example.com'), } `, }, }) // If it's start mode, we get the whole logs since they're from build process. // If it's development mode, we get the logs after request function getCliOutput(logStartPosition: number) { return isNextDev ? next.cliOutput.slice(logStartPosition) : next.cliOutput } it('should not show warning in output in default build output mode', async () => { const logStartPosition = next.cliOutput.length await next.fetch('/og-image-convention') const output = getCliOutput(logStartPosition) expect(output).not.toInclude(METADATA_BASE_WARN_STRING) }) it('should not warn metadataBase is missing and a relative URL is used', async () => { const logStartPosition = next.cliOutput.length await next.fetch('/relative-url-og') const output = getCliOutput(logStartPosition) expect(output).not.toInclude(METADATA_BASE_WARN_STRING) }) it('should warn for unsupported metadata properties', async () => { const logStartPosition = next.cliOutput.length await next.fetch('/unsupported-metadata') const output = getCliOutput(logStartPosition) expect(output).toInclude( 'Unsupported metadata themeColor is configured in metadata export in /unsupported-metadata. Please move it to viewport' ) expect(output).toInclude( 'Read more: https://nextjs.org/docs/app/api-reference/functions/generate-viewport' ) }) it('should not warn for viewport properties during manually merging metadata', async () => { const outputLength = next.cliOutput.length await next.fetch('/merge') // Should not log the unsupported metadata viewport warning in the output // during merging the metadata, if the value is still nullable. const output = next.cliOutput.slice(outputLength) expect(output).not.toContain('Unsupported metadata viewport') }) it('should warn for deprecated fields in other property', async () => { const logStartPosition = next.cliOutput.length await next.fetch('/deprecated-other-fields') const output = getCliOutput(logStartPosition) expect(output).toInclude('Use appleWebApp instead') expect(output).toInclude('Use icons.apple instead') }) })` - ID 125: `test/e2e/app-dir/not-found-with-layout-and-group-not-found/index.test.ts` β€” `describe('app dir - not found with nested layouts and custom not-found', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should render the custom not-found page when notFound() is thrown from a page within the group', async () => { const browser = await next.browser('/') await waitForNoRedbox(browser) const heading = await browser.elementByCss('h1#not-found-heading') expect(await heading.text()).toBe('Group Not Found Page') }) })` - ID 126: `test/e2e/app-dir/not-found-with-nested-layouts/index.test.ts` β€” `describe('app dir - not found with nested layouts', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should render the custom not-found page when notFound() is thrown from a page', async () => { const browser = await next.browser('/') await waitForNoRedbox(browser) const heading = await browser.elementByCss('h1#not-found-heading') expect(await heading.text()).toBe('Custom Not Found Page') }) })` - ID 129: `test/e2e/app-dir/not-found/default/default.test.ts` β€” `describe('app dir - not-found - default', () => { const { next, isNextStart } = nextTestSetup({ files: __dirname, }) it('should has noindex in the head html', async () => { const $ = await next.render$('/does-not-exist') expect(await $('meta[name="robots"]').attr('content')).toBe('noindex') }) if (isNextStart) { it('should contain noindex contain in the page', async () => { const html = await next.readFile('.next/server/app/_not-found.html') const rsc = isPPREnabled ? 'noindex' : await next.readFile(`.next/server/app/_not-found.rsc`) expect(html).toContain('noindex') expect(rsc).toContain('noindex') }) } })` - ID 130: `test/e2e/app-dir/not-found/group-route-root-not-found/index.test.ts` β€” `describe('app dir - group routes with root not-found', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should render default 404 with root layout for non-existent page', async () => { const browser = await next.browser('/non-existent') expect(await browser.elementByCss('p').text()).toBe('Not found placeholder') expect(await browser.elementByCss('h1').text()).toBe('Root layout') }) it('should render root not found for group routes if hit 404', async () => { const browser = await next.browser('/group-dynamic/123') expect(await browser.elementByCss('p').text()).toBe('group-dynamic [id]') await browser.loadPage(next.url + '/group-dynamic/404') expect(await browser.elementByCss('p').text()).toBe('Not found placeholder') expect(await browser.elementByCss('h1').text()).toBe('Root layout') }) })` - ID 132: `test/e2e/app-dir/parallel-routes-and-interception-nested-dynamic-routes/parallel-routes-and-interception-nested-dynamic-routes.test.ts` β€” `describe('parallel-routes-and-interception-nested-dynamic-routes', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should intercept the route for nested dynamic routes', async () => { const browser = await next.browser('/1/1') expect(await browser.elementByCss('h1').text()).toBe('foo id 1, bar id 1') await browser.elementByCss('a').click() // Should intercept the route. expect(await browser.waitForElementByCss('p').text()).toBe('intercepted!') // Should preserve the previous component. expect(await browser.elementByCss('h1').text()).toBe('foo id 1, bar id 1') await browser.refresh() // Should display the correct /baz_id/1 content. expect(await browser.waitForElementByCss('p').text()).toBe('baz_id/1') }) })` - ID 133: `test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts` β€” `describe('parallel-routes-and-interception-conflicting-pages', () => { const { next } = nextTestSetup({ files: { app: new FileRef(path.join(__dirname, 'app')), 'app/parallel/nested-2/page.js': ` export default function Page() { return 'hello world' } `, }, nextConfig, }) it('should gracefully handle when two page segments match the `children` parallel slot', async () => { const html = await next.render('/parallel/nested-2') // before adding this file, the page would have matched `/app/parallel/(new)/@baz/nested-2/page` // but we've added a more specific page, so it should match that instead if (process.env.IS_TURBOPACK_TEST) { // TODO: this matches differently in Turbopack because the Webpack loader does some sorting on the paths // Investigate the discrepancy in a follow-up. For now, since no errors are being thrown (and since this test was previously ignored in Turbopack), // we'll just verify that the page is rendered and some content was matched. expect(html).toContain('parallel/(new)/@baz/nested/page') } else { expect(html).toContain('hello world') } }) })` - ID 134: `test/e2e/app-dir/parallel-routes-not-found/parallel-routes-not-found.test.ts` β€” `describe('parallel-routes-and-interception', () => { const { next } = nextTestSetup({ files: __dirname, }) // TODO: revisit the error for missing parallel routes slot it('should not render the @children slot when the @slot is not found', async () => { const browser = await next.browser('/') // we make sure the page is available through navigating expect(await browser.elementByCss('body').text()).toMatch( /This page could not be found/ ) // we also check that the #children-slot id is not present expect(await browser.hasElementByCssSelector('#children-slot')).toBe(false) await retry(async () => { const title = await browser.eval(() => { return document.title }) // TODO: the fact that the title on the client (in hydration data) disagrees with the title SSRd // when cache components is off is a sign we don't have coherent handling of notFound titles // This test now asserts the prod client title in next start that would actually be observed // by site visitors post hydration. expect(title).toBe('404: This page could not be found.') }) }) it('should render the title once for the non-existed route', async () => { const browser = await next.browser('/non-existed') const titles = await browser.elementsByCss('title') // FIXME: (metadata), the title should only be rendered once and using the not-found title expect(titles).toHaveLength(3) }) })` - ID 137: `test/e2e/app-dir/root-layout-render-once/index.test.ts` β€” `describe('app-dir root layout render once', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should only render root layout once', async () => { let $ = await next.render$('/render-once') expect($('#counter').text()).toBe('0') $ = await next.render$('/render-once') expect($('#counter').text()).toBe('1') $ = await next.render$('/render-once') expect($('#counter').text()).toBe('2') }) })` - ID 138: `test/e2e/app-dir/root-layout/root-layout.test.ts` β€” `describe('app-dir root layout', () => { const { next, isNextDev: isDev } = nextTestSetup({ files: __dirname, }) if (isDev) { // TODO-APP: re-enable after reworking the error overlay. describe.skip('Missing required tags', () => { it('should error on page load', async () => { const browser = await next.browser('/missing-tags', { waitHydration: false, }) await waitForRedbox(browser) expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` "Please make sure to include the following tags in your root layout: <html>, <body>. Missing required root layout tags: html, body" `) }) it('should error on page navigation', async () => { const browser = await next.browser('/has-tags', { waitHydration: false, }) await browser.elementByCss('a').click() await waitForRedbox(browser) expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` "Please make sure to include the following tags in your root layout: <html>, <body>. Missing required root layout tags: html, body" `) }) it('should error on page load on static generation', async () => { const browser = await next.browser('/static-missing-tags/slug', { waitHydration: false, }) await waitForRedbox(browser) expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` "Please make sure to include the following tags in your root layout: <html>, <body>. Missing required root layout tags: html, body" `) }) }) } describe('Should do a mpa navigation when switching root layout', () => { it('should work with basic routes', async () => { const browser = await next.browser('/basic-route') expect(await browser.elementById('basic-route').text()).toBe( 'Basic route' ) await browser.eval('window.__TEST_NO_RELOAD = true') // Navigate to page with same root layout await browser.elementByCss('a').click() expect( await browser.waitForElementByCss('#inner-basic-route').text() ).toBe('Inner basic route') expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeTrue() // Navigate to page with different root layout await browser.elementByCss('a').click() expect(await browser.waitForElementByCss('#route-group').text()).toBe( 'Route group' ) expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeUndefined() }) it('should work with route groups', async () => { const browser = await next.browser('/route-group') expect(await browser.elementById('route-group').text()).toBe( 'Route group' ) await browser.eval('window.__TEST_NO_RELOAD = true') // Navigate to page with same root layout await browser.elementByCss('a').click() expect( await browser.waitForElementByCss('#nested-route-group').text() ).toBe('Nested route group') expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeTrue() // Navigate to page with different root layout await browser.elementByCss('a').click() expect(await browser.waitForElementByCss('#parallel-one').text()).toBe( 'One' ) expect(await browser.waitForElementByCss('#parallel-two').text()).toBe( 'Two' ) expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeUndefined() }) it('should work with parallel routes', async () => { const browser = await next.browser('/with-parallel-routes') expect(await browser.elementById('parallel-one').text()).toBe('One') expect(await browser.elementById('parallel-two').text()).toBe('Two') await browser.eval('window.__TEST_NO_RELOAD = true') // Navigate to page with same root layout await check(async () => { await browser.elementByCss('a').click() expect( await browser.waitForElementByCss('#parallel-one-inner').text() ).toBe('One inner') expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeTrue() return 'success' }, 'success') // Navigate to page with different root layout await check(async () => { await browser.elementByCss('a').click() expect(await browser.waitForElementByCss('#dynamic-hello').text()).toBe( 'dynamic hello' ) return 'success' }, 'success') expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeUndefined() }) it('should work with dynamic routes', async () => { const browser = await next.browser('/dynamic/first') expect(await browser.elementById('dynamic-first').text()).toBe( 'dynamic first' ) await browser.eval('window.__TEST_NO_RELOAD = true') // Navigate to page with same root layout await browser.elementByCss('a').click() expect( await browser.waitForElementByCss('#dynamic-first-second').text() ).toBe('dynamic first second') expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeTrue() // Navigate to page with different root layout await browser.elementByCss('a').click() expect( await browser.waitForElementByCss('#inner-basic-route').text() ).toBe('Inner basic route') expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeUndefined() }) it('should work with dynamic catchall routes', async () => { const browser = await next.browser('/dynamic-catchall/slug') expect(await browser.elementById('catchall-slug').text()).toBe( 'catchall slug' ) await browser.eval('window.__TEST_NO_RELOAD = true') // Navigate to page with same root layout await browser.elementById('to-next-url').click() expect( await browser.waitForElementByCss('#catchall-slug-slug').text() ).toBe('catchall slug slug') expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeTrue() // Navigate to page with different root layout await browser.elementById('to-dynamic-first').click() expect(await browser.elementById('dynamic-first').text()).toBe( 'dynamic first' ) expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeUndefined() }) it('should work with static routes', async () => { const browser = await next.browser('/static-mpa-navigation/slug1') expect(await browser.elementById('static-slug1').text()).toBe( 'static slug1' ) await browser.eval('window.__TEST_NO_RELOAD = true') // Navigate to page with same root layout await browser.elementByCss('a').click() expect(await browser.waitForElementByCss('#static-slug2').text()).toBe( 'static slug2' ) expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeTrue() // Navigate to page with different root layout await browser.elementByCss('a').click() expect(await browser.elementById('basic-route').text()).toBe( 'Basic route' ) expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeUndefined() const res = await next.fetch( `${next.url}/static-mpa-navigation/slug-not-existed` ) expect(res.status).toBe(404) }) }) it('should correctly handle navigation between multiple root layouts', async () => { const browser = await next.browser('/root-layout-a') await browser.waitForElementByCss('#root-a') expect(await browser.hasElementByCssSelector('#root-b')).toBeFalse() await browser .elementById('link-to-b') .click() .waitForElementByCss('#root-b') expect(await browser.hasElementByCssSelector('#root-a')).toBeFalse() }) it('should correctly handle navigation between multiple root layouts when redirecting in a server action', async () => { const browser = await next.browser('/root-layout-a') await browser.waitForElementByCss('#action-redirect-to-b') expect(await browser.hasElementByCssSelector('#root-b')).toBeFalse() await browser .elementById('action-redirect-to-b') .click() .waitForElementByCss('#root-b') expect(await browser.hasElementByCssSelector('#root-a')).toBeFalse() }) })` - ID 139: `test/e2e/app-dir/root-suspense-dynamic/root-suspense-dynamic.test.ts` β€” `describe('Root Suspense Dynamic Rendering', () => { const { next, isNextStart } = nextTestSetup({ files: __dirname + '/fixtures/default', }) // TODO: remove when there is a test for isNextDev === false it('placeholder to satisfy at least one test when isNextDev is false', async () => { expect(true).toBe(true) }) if (isNextStart) { it('should handle dynamic content wrapped in Suspense above HTML structure', async () => { try { // Should render the page successfully const $ = await next.render$('/') expect($('body').text()).toContain('Hello World') } catch (error) { throw new Error( 'Expected build to succeed for Suspense wrapping dynamic content above HTML', { cause: error } ) } }) it('should correctly mark route as dynamic', async () => { // The route should be marked as dynamic (Ζ’) not static (β—‹) expect(next.cliOutput).toContain('Ζ’ /') }) } })` - ID 140: `test/e2e/app-dir/similar-pages-paths/similar-pages-paths.test.ts` β€” `describe('app-dir similar pages paths', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should not have conflicts for similar pattern page paths between app and pages', async () => { // pages/page and app/page const res1 = await next.fetch('/') expect(res1.status).toBe(200) expect(await res1.text()).toContain('(app/page.js)') const res2 = await next.fetch('/page') expect(res2.status).toBe(200) expect(await res2.text()).toContain('(pages/page.js)') }) })` - ID 143: `test/e2e/app-dir/unauthorized/default/unauthorized-default.test.ts` β€” `describe('app dir - unauthorized with default unauthorized boundary', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) // TODO: error unauthorized usage in root layout it.skip('should error on client unauthorized from root layout in browser', async () => { const browser = await next.browser('/') await browser.elementByCss('#trigger-unauthorized').click() if (isNextDev) { await waitForRedbox(browser) expect(await getRedboxDescription(browser)).toMatch( /unauthorized\(\) is not allowed to use in root layout/ ) } }) // TODO: error unauthorized usage in root layout it.skip('should error on server unauthorized from root layout on server-side', async () => { const browser = await next.browser('/?root-unauthorized=1') if (isNextDev) { await waitForRedbox(browser) expect(await getRedboxDescription(browser)).toBe( 'Error: unauthorized() is not allowed to use in root layout' ) } }) it('should be able to navigate to page calling unauthorized', async () => { const browser = await next.browser('/') await browser.elementByCss('#navigate-unauthorized').click() await browser.waitForElementByCss('.next-error-h1') expect(await browser.elementByCss('h1').text()).toBe('401') expect(await browser.elementByCss('h2').text()).toBe( `You're not authorized to access this page.` ) }) it('should be able to navigate to page with calling unauthorized in metadata', async () => { const browser = await next.browser('/') await browser.elementByCss('#metadata-layout-unauthorized').click() await browser.waitForElementByCss('.next-error-h1') expect(await browser.elementByCss('h1').text()).toBe('401') expect(await browser.elementByCss('h2').text()).toBe( `You're not authorized to access this page.` ) }) it('should render default unauthorized for group routes if unauthorized is not defined', async () => { const browser = await next.browser('/group-dynamic/123') expect(await browser.elementByCss('#page').text()).toBe( 'group-dynamic [id]' ) await browser.loadPage(next.url + '/group-dynamic/401') await waitForNoRedbox(browser) await browser.waitForElementByCss('.group-root-layout') expect(await browser.elementByCss('.next-error-h1').text()).toBe('401') }) })` </details> <details> <summary>Deployment evidence for the additional scopes</summary> - `test/e2e/app-dir/app-rendering/rendering.test.ts` β€” `describe('app dir rendering', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426784855/job/102715677110); Cache Components excluded by manifest. - `test/e2e/app-dir/dynamic-data/dynamic-data.test.ts` β€” `describe('dynamic-data', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426784855/job/102715677056); Cache Components excluded by manifest. - `test/e2e/app-dir/forbidden/default/forbidden-default.test.ts` β€” `describe('app dir - forbidden with default forbidden boundary', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426784855/job/102715677074); Cache Components excluded by manifest. - `test/e2e/app-dir/root-layout/root-layout.test.ts` β€” `describe('app-dir root layout', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426784855/job/102715677110); Cache Components excluded by manifest. - `test/e2e/app-dir/unauthorized/default/unauthorized-default.test.ts` β€” `describe('app dir - unauthorized with default unauthorized boundary', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426784855/job/102715677110); Cache Components excluded by manifest. - `test/e2e/app-dir/actions-allowed-origins/app-action-opaque-origin.test.ts` β€” `describe('app-dir action allowed from opaque origins', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426784855/job/102715677074), [cache](https://github.com/vercel/next.js/actions/runs/34426784855/job/102715677108); this scope passed although another scope in the file failed. - `test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts` β€” `describe('parallel-routes-and-interception-conflicting-pages', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426784855/job/102715677110); Cache Components excluded by manifest; this scope passed although another scope in the file failed. </details> <!-- NEXT_JS_LLM --> --- .../app-action-allowed-origins.test.ts | 7 +------ .../app-action-opaque-origin.test.ts | 7 +------ test/e2e/app-dir/app-a11y/index.test.ts | 7 +------ test/e2e/app-dir/app-rendering/rendering.test.ts | 7 +------ test/e2e/app-dir/app-validation/validation.test.ts | 7 +------ .../async-component-preload.test.ts | 7 +------ .../client-reference-side-effects.test.ts | 1 - .../duplicate-layout-components.test.ts | 7 +------ test/e2e/app-dir/dynamic-data/dynamic-data.test.ts | 7 +------ test/e2e/app-dir/dynamic-href/dynamic-href.test.ts | 11 +---------- .../dynamic-import-tree-shaking.test.ts | 4 +--- .../dynamic-in-generate-params/index.test.ts | 1 - test/e2e/app-dir/dynamic/dynamic.test.ts | 7 +------ .../forbidden/default/forbidden-default.test.ts | 7 +------ .../app-dir/global-error/catch-all/index.test.ts | 7 +------ .../global-error/layout-error/index.test.ts | 7 +------ test/e2e/app-dir/io/io.test.ts | 14 ++------------ .../app-dir/metadata-json-manifest/index.test.ts | 7 +------ test/e2e/app-dir/metadata-suspense/index.test.ts | 7 +------ .../metadata-warnings-with-metadatabase.test.ts | 7 +------ .../index.test.ts | 7 +------ .../not-found-with-nested-layouts/index.test.ts | 7 +------ test/e2e/app-dir/not-found/default/default.test.ts | 1 - .../group-route-root-not-found/index.test.ts | 7 +------ ...-and-interception-nested-dynamic-routes.test.ts | 1 - .../parallel-routes-and-interception.test.ts | 7 +------ .../parallel-routes-not-found.test.ts | 8 +------- .../app-dir/root-layout-render-once/index.test.ts | 7 +------ test/e2e/app-dir/root-layout/root-layout.test.ts | 11 +---------- .../root-suspense-dynamic.test.ts | 1 - .../similar-pages-paths.test.ts | 7 +------ .../default/unauthorized-default.test.ts | 7 +------ 32 files changed, 28 insertions(+), 179 deletions(-) diff --git a/test/e2e/app-dir/actions-allowed-origins/app-action-allowed-origins.test.ts b/test/e2e/app-dir/actions-allowed-origins/app-action-allowed-origins.test.ts index 7a055bb2fc5a..2b5b0338b41c 100644 --- a/test/e2e/app-dir/actions-allowed-origins/app-action-allowed-origins.test.ts +++ b/test/e2e/app-dir/actions-allowed-origins/app-action-allowed-origins.test.ts @@ -3,9 +3,8 @@ import { check } from 'next-test-utils' import { join } from 'path' describe('app-dir action allowed origins', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: join(__dirname, 'safe-origins'), - skipDeployment: true, dependencies: { 'server-only': 'latest', }, @@ -13,10 +12,6 @@ describe('app-dir action allowed origins', () => { forcedPort: 'random', }) - if (skipped) { - return - } - it('should pass if localhost is set as a safe origin', async function () { const browser = await next.browser('/') diff --git a/test/e2e/app-dir/actions-allowed-origins/app-action-opaque-origin.test.ts b/test/e2e/app-dir/actions-allowed-origins/app-action-opaque-origin.test.ts index b4126ae632d0..fee0326837e5 100644 --- a/test/e2e/app-dir/actions-allowed-origins/app-action-opaque-origin.test.ts +++ b/test/e2e/app-dir/actions-allowed-origins/app-action-opaque-origin.test.ts @@ -3,18 +3,13 @@ import { retry } from 'next-test-utils' import { join } from 'path' describe('app-dir action allowed from opaque origins', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: join(__dirname, 'opaque-origin'), - skipDeployment: true, env: { NEXT_TEST_ALLOW_OPAQUE_ORIGIN: '1', }, }) - if (skipped) { - return - } - it('should succeed on submission', async function () { const browser = await next.browser('/sandboxed') diff --git a/test/e2e/app-dir/app-a11y/index.test.ts b/test/e2e/app-dir/app-a11y/index.test.ts index c142fe3eaac5..e09ad3db5ac0 100644 --- a/test/e2e/app-dir/app-a11y/index.test.ts +++ b/test/e2e/app-dir/app-a11y/index.test.ts @@ -2,16 +2,11 @@ import { nextTestSetup, type Playwright } from 'e2e-utils' import { check } from 'next-test-utils' describe('app a11y features', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, packageJson: {}, - skipDeployment: true, }) - if (skipped) { - return - } - describe('route announcer', () => { async function getAnnouncerContent(browser: Playwright) { return browser.eval( diff --git a/test/e2e/app-dir/app-rendering/rendering.test.ts b/test/e2e/app-dir/app-rendering/rendering.test.ts index beae2c49db49..74282744ea5b 100644 --- a/test/e2e/app-dir/app-rendering/rendering.test.ts +++ b/test/e2e/app-dir/app-rendering/rendering.test.ts @@ -3,15 +3,10 @@ import { waitFor } from 'next-test-utils' import cheerio from 'cheerio' describe('app dir rendering', () => { - const { next, isNextDev, skipped } = nextTestSetup({ + const { next, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should serve app/page.server.js at /', async () => { const html = await next.render('/') expect(html).toContain('app/page.server.js') diff --git a/test/e2e/app-dir/app-validation/validation.test.ts b/test/e2e/app-dir/app-validation/validation.test.ts index a19169d32f22..049233a5424c 100644 --- a/test/e2e/app-dir/app-validation/validation.test.ts +++ b/test/e2e/app-dir/app-validation/validation.test.ts @@ -5,15 +5,10 @@ import { } from 'next/dist/shared/lib/router/utils/cache-busting-search-param' describe('app dir - validation', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should error when passing invalid router state tree', async () => { const stateTree1 = JSON.stringify(['', '']) const stateTree2 = JSON.stringify(['', {}]) diff --git a/test/e2e/app-dir/async-component-preload/async-component-preload.test.ts b/test/e2e/app-dir/async-component-preload/async-component-preload.test.ts index c18189cb10d6..42e1a51d5491 100644 --- a/test/e2e/app-dir/async-component-preload/async-component-preload.test.ts +++ b/test/e2e/app-dir/async-component-preload/async-component-preload.test.ts @@ -1,15 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('async-component-preload', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should handle redirect in an async page', async () => { const browser = await next.browser('/') expect(await browser.waitForElementByCss('#success').text()).toBe('Success') diff --git a/test/e2e/app-dir/client-reference-side-effects/client-reference-side-effects.test.ts b/test/e2e/app-dir/client-reference-side-effects/client-reference-side-effects.test.ts index 94929a7668f2..5ea927518460 100644 --- a/test/e2e/app-dir/client-reference-side-effects/client-reference-side-effects.test.ts +++ b/test/e2e/app-dir/client-reference-side-effects/client-reference-side-effects.test.ts @@ -3,7 +3,6 @@ import { nextTestSetup } from 'e2e-utils' describe('client-reference-side-effects', () => { const { next, isTurbopack } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) it('side effect behavior when only importing', async () => { diff --git a/test/e2e/app-dir/duplicate-layout-components/duplicate-layout-components.test.ts b/test/e2e/app-dir/duplicate-layout-components/duplicate-layout-components.test.ts index 5773eb13ac2d..0ba56a247787 100644 --- a/test/e2e/app-dir/duplicate-layout-components/duplicate-layout-components.test.ts +++ b/test/e2e/app-dir/duplicate-layout-components/duplicate-layout-components.test.ts @@ -1,15 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('app dir - duplicate layout components', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should not duplicate layout elements when navigating to 404', async () => { const browser = await next.browser('/solutions/404') diff --git a/test/e2e/app-dir/dynamic-data/dynamic-data.test.ts b/test/e2e/app-dir/dynamic-data/dynamic-data.test.ts index dfec254a4e74..cd88c36f8263 100644 --- a/test/e2e/app-dir/dynamic-data/dynamic-data.test.ts +++ b/test/e2e/app-dir/dynamic-data/dynamic-data.test.ts @@ -3,15 +3,10 @@ import { nextTestSetup } from 'e2e-utils' process.env.__TEST_SENTINEL = 'at buildtime' describe('dynamic-data', () => { - const { next, isNextDev, skipped } = nextTestSetup({ + const { next, isNextDev } = nextTestSetup({ files: __dirname + '/fixtures/main', - skipDeployment: true, }) - if (skipped) { - return - } - it('should render the dynamic apis dynamically when used in a top-level scope', async () => { const $ = await next.render$( '/top-level?foo=foosearch', diff --git a/test/e2e/app-dir/dynamic-href/dynamic-href.test.ts b/test/e2e/app-dir/dynamic-href/dynamic-href.test.ts index 1f620433f8cf..15472f2441ac 100644 --- a/test/e2e/app-dir/dynamic-href/dynamic-href.test.ts +++ b/test/e2e/app-dir/dynamic-href/dynamic-href.test.ts @@ -1,19 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('dynamic-href', () => { - const { - isNextDev: isDev, - next, - skipped, - } = nextTestSetup({ + const { isNextDev: isDev, next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - if (isDev) { it('should error when using dynamic href.pathname in app dir', async () => { const browser = await next.browser('/object') diff --git a/test/e2e/app-dir/dynamic-import-tree-shaking/dynamic-import-tree-shaking.test.ts b/test/e2e/app-dir/dynamic-import-tree-shaking/dynamic-import-tree-shaking.test.ts index de9be9754471..d098ea7ac3c3 100644 --- a/test/e2e/app-dir/dynamic-import-tree-shaking/dynamic-import-tree-shaking.test.ts +++ b/test/e2e/app-dir/dynamic-import-tree-shaking/dynamic-import-tree-shaking.test.ts @@ -3,11 +3,9 @@ import fs from 'fs' import path from 'path' describe('dynamic-import-tree-shaking', () => { - const { next, skipped, isNextStart, isTurbopack } = nextTestSetup({ + const { next, isNextStart, isTurbopack } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return // Recursively read all .js files in a directory function getAllServerFiles(dir: string): string[] { diff --git a/test/e2e/app-dir/dynamic-in-generate-params/index.test.ts b/test/e2e/app-dir/dynamic-in-generate-params/index.test.ts index 5b31e2cd84a5..6d147f012c58 100644 --- a/test/e2e/app-dir/dynamic-in-generate-params/index.test.ts +++ b/test/e2e/app-dir/dynamic-in-generate-params/index.test.ts @@ -13,7 +13,6 @@ function assertSitemapResponse(res: Response) { describe('app-dir - dynamic in generate params', () => { const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) it('should render sitemap with generateSitemaps in force-dynamic config dynamically', async () => { diff --git a/test/e2e/app-dir/dynamic/dynamic.test.ts b/test/e2e/app-dir/dynamic/dynamic.test.ts index f1bbcfda64b6..200e59bb303b 100644 --- a/test/e2e/app-dir/dynamic/dynamic.test.ts +++ b/test/e2e/app-dir/dynamic/dynamic.test.ts @@ -3,15 +3,10 @@ import { retry } from 'next-test-utils' import path from 'path' describe('app dir - next/dynamic', () => { - const { next, isNextStart, isNextDev, skipped } = nextTestSetup({ + const { next, isNextStart, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should handle ssr: false in pages when appDir is enabled', async () => { const $ = await next.render$('/legacy/no-ssr') expect($.html()).not.toContain('navigator') diff --git a/test/e2e/app-dir/forbidden/default/forbidden-default.test.ts b/test/e2e/app-dir/forbidden/default/forbidden-default.test.ts index 9ef97456dcbf..23fc125bad1d 100644 --- a/test/e2e/app-dir/forbidden/default/forbidden-default.test.ts +++ b/test/e2e/app-dir/forbidden/default/forbidden-default.test.ts @@ -6,15 +6,10 @@ import { } from 'next-test-utils' describe('app dir - forbidden with default forbidden boundary', () => { - const { next, isNextDev, skipped } = nextTestSetup({ + const { next, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - // TODO: error forbidden usage in root layout it.skip('should error on client forbidden from root layout in browser', async () => { const browser = await next.browser('/') diff --git a/test/e2e/app-dir/global-error/catch-all/index.test.ts b/test/e2e/app-dir/global-error/catch-all/index.test.ts index 5c88994110fa..e4c5bed041ff 100644 --- a/test/e2e/app-dir/global-error/catch-all/index.test.ts +++ b/test/e2e/app-dir/global-error/catch-all/index.test.ts @@ -1,15 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('app dir - global error - with catch-all route', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should render catch-all route correctly', async () => { expect(await next.render('/en/foo')).toContain('catch-all page') }) diff --git a/test/e2e/app-dir/global-error/layout-error/index.test.ts b/test/e2e/app-dir/global-error/layout-error/index.test.ts index fc5636efef45..266a311e91d3 100644 --- a/test/e2e/app-dir/global-error/layout-error/index.test.ts +++ b/test/e2e/app-dir/global-error/layout-error/index.test.ts @@ -1,15 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('app dir - global error - layout error', () => { - const { next, isNextDev, skipped } = nextTestSetup({ + const { next, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should render global error for error in server components', async () => { const browser = await next.browser('/') diff --git a/test/e2e/app-dir/io/io.test.ts b/test/e2e/app-dir/io/io.test.ts index 852f4ee9c4f9..53be2785f713 100644 --- a/test/e2e/app-dir/io/io.test.ts +++ b/test/e2e/app-dir/io/io.test.ts @@ -1,15 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('io with cache components', () => { - const { next, isNextDev, skipped } = nextTestSetup({ + const { next, isNextDev } = nextTestSetup({ files: __dirname + '/fixtures/cache-components', - skipDeployment: true, }) - if (skipped) { - return - } - it('should make content after io() dynamic during prerender', async () => { const $ = await next.render$('/io-boundary') if (isNextDev) { @@ -58,15 +53,10 @@ describe('io with cache components', () => { }) describe('io without cache components', () => { - const { next, isNextDev, skipped } = nextTestSetup({ + const { next, isNextDev } = nextTestSetup({ files: __dirname + '/fixtures/default', - skipDeployment: true, }) - if (skipped) { - return - } - it('should be a no-op during prerender without cache components', async () => { const $ = await next.render$('/io-boundary') if (isNextDev) { diff --git a/test/e2e/app-dir/metadata-json-manifest/index.test.ts b/test/e2e/app-dir/metadata-json-manifest/index.test.ts index 562e987a7805..868a13f5a748 100644 --- a/test/e2e/app-dir/metadata-json-manifest/index.test.ts +++ b/test/e2e/app-dir/metadata-json-manifest/index.test.ts @@ -1,15 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('app-dir metadata-json-manifest', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should support metadata.json manifest', async () => { const response = await next.fetch('/manifest.json') expect(response.status).toBe(200) diff --git a/test/e2e/app-dir/metadata-suspense/index.test.ts b/test/e2e/app-dir/metadata-suspense/index.test.ts index ffa57ebc3718..8824fb168c0a 100644 --- a/test/e2e/app-dir/metadata-suspense/index.test.ts +++ b/test/e2e/app-dir/metadata-suspense/index.test.ts @@ -1,15 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('app dir - metadata dynamic routes suspense', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should render metadata in head when root layout is wrapped with Suspense for bot requests', async () => { const $ = await next.render$('/', undefined, { headers: { diff --git a/test/e2e/app-dir/metadata-warnings/metadata-warnings-with-metadatabase.test.ts b/test/e2e/app-dir/metadata-warnings/metadata-warnings-with-metadatabase.test.ts index 53d303869cdb..5fbe31bad25f 100644 --- a/test/e2e/app-dir/metadata-warnings/metadata-warnings-with-metadatabase.test.ts +++ b/test/e2e/app-dir/metadata-warnings/metadata-warnings-with-metadatabase.test.ts @@ -4,9 +4,8 @@ const METADATA_BASE_WARN_STRING = 'metadataBase property in metadata export is not set for resolving social open graph or twitter images,' describe('app dir - metadata missing metadataBase', () => { - const { next, isNextDev, skipped } = nextTestSetup({ + const { next, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, overrideFiles: { 'app/layout.js': ` export default function Layout({ children }) { @@ -24,10 +23,6 @@ describe('app dir - metadata missing metadataBase', () => { }, }) - if (skipped) { - return - } - // If it's start mode, we get the whole logs since they're from build process. // If it's development mode, we get the logs after request function getCliOutput(logStartPosition: number) { diff --git a/test/e2e/app-dir/not-found-with-layout-and-group-not-found/index.test.ts b/test/e2e/app-dir/not-found-with-layout-and-group-not-found/index.test.ts index 82fcb2d92400..f1be745a9566 100644 --- a/test/e2e/app-dir/not-found-with-layout-and-group-not-found/index.test.ts +++ b/test/e2e/app-dir/not-found-with-layout-and-group-not-found/index.test.ts @@ -2,15 +2,10 @@ import { nextTestSetup } from 'e2e-utils' import { waitForNoRedbox } from 'next-test-utils' describe('app dir - not found with nested layouts and custom not-found', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should render the custom not-found page when notFound() is thrown from a page within the group', async () => { const browser = await next.browser('/') await waitForNoRedbox(browser) diff --git a/test/e2e/app-dir/not-found-with-nested-layouts/index.test.ts b/test/e2e/app-dir/not-found-with-nested-layouts/index.test.ts index 724ae91bbed4..820b35227f97 100644 --- a/test/e2e/app-dir/not-found-with-nested-layouts/index.test.ts +++ b/test/e2e/app-dir/not-found-with-nested-layouts/index.test.ts @@ -2,15 +2,10 @@ import { nextTestSetup } from 'e2e-utils' import { waitForNoRedbox } from 'next-test-utils' describe('app dir - not found with nested layouts', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should render the custom not-found page when notFound() is thrown from a page', async () => { const browser = await next.browser('/') await waitForNoRedbox(browser) diff --git a/test/e2e/app-dir/not-found/default/default.test.ts b/test/e2e/app-dir/not-found/default/default.test.ts index 6305f2cb28b7..2875fd64f419 100644 --- a/test/e2e/app-dir/not-found/default/default.test.ts +++ b/test/e2e/app-dir/not-found/default/default.test.ts @@ -5,7 +5,6 @@ const isPPREnabled = process.env.__NEXT_CACHE_COMPONENTS === 'true' describe('app dir - not-found - default', () => { const { next, isNextStart } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) it('should has noindex in the head html', async () => { diff --git a/test/e2e/app-dir/not-found/group-route-root-not-found/index.test.ts b/test/e2e/app-dir/not-found/group-route-root-not-found/index.test.ts index 19e268a2ae2e..857348f0b155 100644 --- a/test/e2e/app-dir/not-found/group-route-root-not-found/index.test.ts +++ b/test/e2e/app-dir/not-found/group-route-root-not-found/index.test.ts @@ -1,15 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('app dir - group routes with root not-found', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should render default 404 with root layout for non-existent page', async () => { const browser = await next.browser('/non-existent') expect(await browser.elementByCss('p').text()).toBe('Not found placeholder') diff --git a/test/e2e/app-dir/parallel-routes-and-interception-nested-dynamic-routes/parallel-routes-and-interception-nested-dynamic-routes.test.ts b/test/e2e/app-dir/parallel-routes-and-interception-nested-dynamic-routes/parallel-routes-and-interception-nested-dynamic-routes.test.ts index 46cadba463d0..96c3fcfd125d 100644 --- a/test/e2e/app-dir/parallel-routes-and-interception-nested-dynamic-routes/parallel-routes-and-interception-nested-dynamic-routes.test.ts +++ b/test/e2e/app-dir/parallel-routes-and-interception-nested-dynamic-routes/parallel-routes-and-interception-nested-dynamic-routes.test.ts @@ -5,7 +5,6 @@ describe('parallel-routes-and-interception-nested-dynamic-routes', () => { files: __dirname, // TODO: re-enable when resolved related thread // https://vercel.slack.com/archives/C07UCHRBWGK/p1759165345308879 - skipDeployment: true, }) it('should intercept the route for nested dynamic routes', async () => { diff --git a/test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts b/test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts index 4e5c31590c37..6076bb5e65fc 100644 --- a/test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts +++ b/test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts @@ -1057,10 +1057,7 @@ describe.each([true, false])( ) describe('parallel-routes-and-interception-conflicting-pages', () => { - const { next, skipped } = nextTestSetup({ - // This is skipped when deployed as it appears to cause an issue when tracing Next.js files - // TODO: Investigate why this causes an issue when deployed - skipDeployment: true, + const { next } = nextTestSetup({ files: { app: new FileRef(path.join(__dirname, 'app')), 'app/parallel/nested-2/page.js': ` @@ -1072,8 +1069,6 @@ describe('parallel-routes-and-interception-conflicting-pages', () => { nextConfig, }) - if (skipped) return - it('should gracefully handle when two page segments match the `children` parallel slot', async () => { const html = await next.render('/parallel/nested-2') diff --git a/test/e2e/app-dir/parallel-routes-not-found/parallel-routes-not-found.test.ts b/test/e2e/app-dir/parallel-routes-not-found/parallel-routes-not-found.test.ts index db8570f1154f..8dfeb2a78d35 100644 --- a/test/e2e/app-dir/parallel-routes-not-found/parallel-routes-not-found.test.ts +++ b/test/e2e/app-dir/parallel-routes-not-found/parallel-routes-not-found.test.ts @@ -2,16 +2,10 @@ import { nextTestSetup } from 'e2e-utils' import { retry } from 'next-test-utils' describe('parallel-routes-and-interception', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - // TODO: remove after deployment handling is updated - skipDeployment: true, }) - if (skipped) { - return - } - // TODO: revisit the error for missing parallel routes slot it('should not render the @children slot when the @slot is not found', async () => { const browser = await next.browser('/') diff --git a/test/e2e/app-dir/root-layout-render-once/index.test.ts b/test/e2e/app-dir/root-layout-render-once/index.test.ts index f4bfa2bed9ab..0617244d4953 100644 --- a/test/e2e/app-dir/root-layout-render-once/index.test.ts +++ b/test/e2e/app-dir/root-layout-render-once/index.test.ts @@ -1,15 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('app-dir root layout render once', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should only render root layout once', async () => { let $ = await next.render$('/render-once') expect($('#counter').text()).toBe('0') diff --git a/test/e2e/app-dir/root-layout/root-layout.test.ts b/test/e2e/app-dir/root-layout/root-layout.test.ts index fde385d9d85d..c2b5f699b72e 100644 --- a/test/e2e/app-dir/root-layout/root-layout.test.ts +++ b/test/e2e/app-dir/root-layout/root-layout.test.ts @@ -2,19 +2,10 @@ import { nextTestSetup } from 'e2e-utils' import { waitForRedbox, check, getRedboxSource } from 'next-test-utils' describe('app-dir root layout', () => { - const { - next, - isNextDev: isDev, - skipped, - } = nextTestSetup({ + const { next, isNextDev: isDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - if (isDev) { // TODO-APP: re-enable after reworking the error overlay. describe.skip('Missing required tags', () => { diff --git a/test/e2e/app-dir/root-suspense-dynamic/root-suspense-dynamic.test.ts b/test/e2e/app-dir/root-suspense-dynamic/root-suspense-dynamic.test.ts index 187cb40d6820..22cbde4144aa 100644 --- a/test/e2e/app-dir/root-suspense-dynamic/root-suspense-dynamic.test.ts +++ b/test/e2e/app-dir/root-suspense-dynamic/root-suspense-dynamic.test.ts @@ -3,7 +3,6 @@ import { nextTestSetup } from 'e2e-utils' describe('Root Suspense Dynamic Rendering', () => { const { next, isNextStart } = nextTestSetup({ files: __dirname + '/fixtures/default', - skipDeployment: true, }) // TODO: remove when there is a test for isNextDev === false diff --git a/test/e2e/app-dir/similar-pages-paths/similar-pages-paths.test.ts b/test/e2e/app-dir/similar-pages-paths/similar-pages-paths.test.ts index d5fe1af996a6..ad30c8800c78 100644 --- a/test/e2e/app-dir/similar-pages-paths/similar-pages-paths.test.ts +++ b/test/e2e/app-dir/similar-pages-paths/similar-pages-paths.test.ts @@ -1,15 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('app-dir similar pages paths', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should not have conflicts for similar pattern page paths between app and pages', async () => { // pages/page and app/page const res1 = await next.fetch('/') diff --git a/test/e2e/app-dir/unauthorized/default/unauthorized-default.test.ts b/test/e2e/app-dir/unauthorized/default/unauthorized-default.test.ts index f1280f6f76ce..d3bbe09f0048 100644 --- a/test/e2e/app-dir/unauthorized/default/unauthorized-default.test.ts +++ b/test/e2e/app-dir/unauthorized/default/unauthorized-default.test.ts @@ -6,15 +6,10 @@ import { } from 'next-test-utils' describe('app dir - unauthorized with default unauthorized boundary', () => { - const { next, isNextDev, skipped } = nextTestSetup({ + const { next, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - // TODO: error unauthorized usage in root layout it.skip('should error on client unauthorized from root layout in browser', async () => { const browser = await next.browser('/') From f825d34b75bdc858eca4e5d21db0643d02ba5921 Mon Sep 17 00:00:00 2001 From: Jamiboy Mohammad <jamiboym@gmail.com> Date: Tue, 15 Sep 2026 10:29:06 -0700 Subject: [PATCH 11/14] test: enable verified tooling deploy tests (#98525) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Enable the same 26 previously selected deployment-test scopes across 25 tooling test files, now in a stack rooted on canary. Remove 26 `skipDeployment` options and their obsolete skip guards. Other mode, bundler, middleware, and Cache Components exclusions remain in place. This preserves the selection with passing evidence from the previous deployment runs. No additional candidate scopes are enabled; excluded variants are not counted as deployment coverage. ## Verification - All selected test registration names and assertion bodies match the previous enabled revision, checked by AST comparison. - Verified that the canary diff contains only the inventoried exclusions and their obsolete skip plumbing; other exclusions are preserved. - Formatting and lint passed; 77 gate infrastructure unit tests passed. - Full local bootstrap was blocked by missing package-level dependencies in the temporary worktree. Fresh deployment execution on these rewritten commits remains to be verified in CI. <details> <summary>Preserved scope inventory (26)</summary> - ID 146: `test/e2e/app-dir/app-config-crossorigin/index.test.ts` β€” `describe('app dir - crossOrigin config', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should render correctly with assetPrefix: "/"', async () => { const $ = await next.render$('/') // Only potential external (assetPrefix) <script /> and <link /> should have crossorigin attribute $( 'script[src*="https://example.vercel.sh"], link[href*="https://example.vercel.sh"]' ).each((_, el) => { const crossOrigin = $(el).attr('crossorigin') expect(crossOrigin).toBe('use-credentials') }) // Inline <script /> (including RSC payload) and <link /> should not have crossorigin attribute $('script:not([src]), link:not([href])').each((_, el) => { const crossOrigin = $(el).attr('crossorigin') expect(crossOrigin).toBeUndefined() }) // Same origin <script /> and <link /> should not have crossorigin attribute either $('script[src^="/"], link[href^="/"]').each((_, el) => { const crossOrigin = $(el).attr('crossorigin') expect(crossOrigin).toBeUndefined() }) }) })` - ID 148: `test/e2e/app-dir/import-meta-glob-text-type/import-meta-glob-text-type.test.ts` β€” `describe('turbopack `text` / `raw` module types', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should load matched files as strings through a `?raw` rule', async () => { const $ = await next.render$('/raw') const items = $('li') .map((_, el) => $(el).text()) .get() expect(items).toEqual([ './content/delta.txt: delta contents', './content/gamma.txt: gamma contents', ]) }) it('should treat `raw` and `text` the same in a `?raw` rule', async () => { const $ = await next.render$('/raw-alias') const items = $('li') .map((_, el) => $(el).text()) .get() expect(items).toEqual([ './content/delta.rst: delta contents', './content/gamma.rst: gamma contents', ]) }) it('should treat `raw` and `text` the same for a plain import', async () => { const $ = await next.render$('/alias') expect(JSON.parse($('#raw').text())).toBe('# alpha\n\nsome markdown\n') expect($('#equal').text()).toBe('true') }) })` - ID 153: `test/e2e/app-dir/multiple-lockfiles/multiple-lockfiles-with-output-file-tracing-root.test.ts` β€” `describe('multiple-lockfiles - has-output-file-tracing-root', () => { const { next } = nextTestSetup({ files: { app: new FileRef(join(__dirname, 'app')), // This will silence the multiple lockfiles warning. 'next.config.js': `module.exports = { outputFileTracingRoot: __dirname }`, // Write a package-lock.json file to the parent directory to simulate // multiple lockfiles. '../package.json': JSON.stringify({ name: 'parent-workspace', version: '1.0.0', }), '../package-lock.json': JSON.stringify({ name: 'parent-workspace', version: '1.0.0', lockfileVersion: 3, packages: { '': { name: 'parent-workspace', version: '1.0.0' } }, }), }, // So that ../package-lock.json doesn't leave the isolated testDir subDir: 'test', // The workspace file would suppress the warning itself, so the test // wouldn't be exercising `outputFileTracingRoot`. deleteWorkspaceFile: true, }) it('should not have multiple lockfiles warnings', async () => { expect(next.cliOutput).not.toMatch( /We detected multiple lockfiles and selected the directory of .+ as the root directory\./ ) }) })` - ID 154: `test/e2e/app-dir/multiple-lockfiles/multiple-lockfiles-with-turbo-root.test.ts` β€” `describe('multiple-lockfiles - has-turbo-root', () => { const { next } = nextTestSetup({ files: { app: new FileRef(join(__dirname, 'app')), // This will silence the multiple lockfiles warning. 'next.config.js': `module.exports = { turbopack: { root: __dirname } }`, // Write a package-lock.json file to the parent directory to simulate // multiple lockfiles. '../package.json': JSON.stringify({ name: 'parent-workspace', version: '1.0.0', }), '../package-lock.json': JSON.stringify({ name: 'parent-workspace', version: '1.0.0', lockfileVersion: 3, packages: { '': { name: 'parent-workspace', version: '1.0.0' } }, }), }, // So that ../package-lock.json doesn't leave the isolated testDir subDir: 'test', // The workspace file would suppress the warning itself, so the test // wouldn't be exercising `turbopack.root`. deleteWorkspaceFile: true, }) it('should not have multiple lockfiles warnings', async () => { expect(next.cliOutput).not.toMatch( /We detected multiple lockfiles and selected the directory of .+ as the root directory\./ ) }) })` - ID 170: `test/e2e/app-dir/segment-config-ts/segment-config-ts.test.ts` β€” `describe('TypeScript type expressions in route segment config', () => { const { next, isNextStart } = nextTestSetup({ files: __dirname, }) describe('app directory', () => { it('should pick up maxDuration declared with `as` type assertion', async () => { const $ = await next.render$('/as') expect($('main').text()).toBe('hello') }) it('should pick up maxDuration declared with `as const` assertion', async () => { const $ = await next.render$('/as-const') expect($('main').text()).toBe('hello') }) it('should pick up maxDuration declared with `satisfies`', async () => { const $ = await next.render$('/satisfies') expect($('main').text()).toBe('hello') }) }) describe('pages directory', () => { it('should pick up maxDuration from config object declared with `as`', async () => { const $ = await next.render$('/config-as') expect($('main').text()).toBe('hello') }) it('should pick up maxDuration from config object declared with `as const`', async () => { const $ = await next.render$('/config-as-const') expect($('main').text()).toBe('hello') }) it('should pick up maxDuration from config object declared with `satisfies`', async () => { const $ = await next.render$('/config-satisfies') expect($('main').text()).toBe('hello') }) }) if (isNextStart) { it('should parse the config correctly', async () => { const config = await next.readJSON( '.next/server/functions-config-manifest.json' ) expect(config).toMatchInlineSnapshot(` { "functions": { "/as": { "maxDuration": 1000, }, "/as-const": { "maxDuration": 1000, }, "/config-as": { "maxDuration": 1000, }, "/config-as-const": { "maxDuration": 1000, }, "/config-satisfies": { "maxDuration": 1000, }, "/satisfies": { "maxDuration": 1000, }, }, "version": 1, } `) }) } })` - ID 172: `test/e2e/app-dir/trace-build-file/trace-build-file.test.ts` β€” `describe('trace-build-file', () => { const { next } = nextTestSetup({ files: __dirname, skipStart: !isNextDev, env: { // Enable persistent caching even when the git working directory is // dirty (e.g. when developing Next.js itself). Without this, the // cache falls back to a temp directory and persistence/compaction // spans are not emitted. TURBO_ENGINE_IGNORE_DIRTY: '1', }, }) if (isNextStart) { it('should create .next/trace-build file during production build', async () => { // Build the app to trigger trace generation await next.build() // Check that trace-build file exists const traceBuildPath = join(next.testDir, '.next/trace-build') expect(existsSync(traceBuildPath)).toBe(true) }) it('should contain high-level build trace events', async () => { // Ensure we have a fresh build await next.build() const traceBuildPath = join(next.testDir, '.next/trace-build') expect(existsSync(traceBuildPath)).toBe(true) const traceStructure = parseTraceFile(traceBuildPath) // Should have events expect(traceStructure.events.length).toBeGreaterThan(0) // Should contain the main next-build event const nextBuildEvents = traceStructure.eventsByName.get('next-build') expect(nextBuildEvents).toBeDefined() expect(nextBuildEvents.length).toBe(1) const nextBuildEvent = nextBuildEvents[0] expect(nextBuildEvent).toHaveProperty('name', 'next-build') expect(nextBuildEvent).toHaveProperty('traceId') expect(nextBuildEvent).toHaveProperty('id') expect(nextBuildEvent).toHaveProperty('duration') expect(typeof nextBuildEvent.duration).toBe('number') expect(typeof nextBuildEvent.traceId).toBe('string') expect(typeof nextBuildEvent.id).toBe('number') }) it('should only contain allowlisted events', async () => { await next.build() const traceBuildPath = join(next.testDir, '.next/trace-build') const traceStructure = parseTraceFile(traceBuildPath) // const allowlistedEvents = new Set([ // 'next-build', // 'run-turbopack', // 'run-webpack', // 'run-typescript', // 'run-eslint', // 'static-check', // 'static-generation', // 'output-export-full-static-export', // ]) const foundEvents = new Set<string>() for (const event of traceStructure.events) { foundEvents.add(event.name) } if (process.env.IS_TURBOPACK_TEST) { // Compaction only runs when it is due, so it may or may not appear. foundEvents.delete('turbopack-compaction') expect([...foundEvents].sort()).toMatchInlineSnapshot(` [ "next-build", "run-turbopack", "run-typescript", "static-check", "static-generation", "telemetry-flush", "turbopack-persistence", ] `) } else { expect([...foundEvents].sort()).toMatchInlineSnapshot(` [ "collect-build-traces", "next-build", "run-typescript", "run-webpack", "static-check", "static-generation", "telemetry-flush", ] `) } }) it('should have next-build as root span with proper hierarchy', async () => { await next.build() const traceBuildPath = join(next.testDir, '.next/trace-build') const traceStructure = parseTraceFile(traceBuildPath) // Should have no orphaned events (all events should have valid parent references) expect(traceStructure.orphanedEvents).toHaveLength(0) // Should have at one root event expect(traceStructure.rootEvents.length).toBe(1) // next-build should be the main root event const nextBuildEvents = traceStructure.eventsByName.get('next-build') expect(nextBuildEvents).toBeDefined() expect(nextBuildEvents.length).toBe(1) const nextBuildEvent = nextBuildEvents[0] expect(nextBuildEvent.parentId).toBeUndefined() // Should be root expect(traceStructure.rootEvents).toContain(nextBuildEvent) // Other build events should be children of next-build or have valid parent references const buildEvents = ['run-webpack', 'run-typescript', 'run-eslint'] for (const eventName of buildEvents) { const events = traceStructure.eventsByName.get(eventName) if (events && events.length > 0) { for (const event of events) { if (event.parentId) { // Should have a valid parent expect( traceStructure.eventsById.has(event.parentId.toString()) ).toBe(true) const parent = traceStructure.eventsById.get( event.parentId.toString() ) // Parent should either be next-build or another valid event expect(parent).toBeDefined() expect(parent.traceId).toBe(event.traceId) // Same trace } } } } }) it('should have consistent traceId across all events', async () => { await next.build() const traceBuildPath = join(next.testDir, '.next/trace-build') const traceStructure = parseTraceFile(traceBuildPath) expect(traceStructure.events.length).toBeGreaterThan(0) const firstEvent = traceStructure.events[0] expect(firstEvent.traceId).toBeDefined() expect(typeof firstEvent.traceId).toBe('string') expect(firstEvent.traceId.length).toBeGreaterThan(0) // All events should have the same traceId for (const event of traceStructure.events) { expect(event.traceId).toBe(firstEvent.traceId) } }) } if (isNextDev) { it('should not create trace-build file in development mode', async () => { // Make a request to trigger some activity await next.render('/') // Check that trace-build file does not exist const traceBuildPath = join(next.testDir, '.next/trace-build') expect(existsSync(traceBuildPath)).toBe(false) }) } it('should work with basic page rendering', async () => { if (isNextStart) { await next.start() } const $ = await next.render$('/') expect($('p').text()).toBe('hello world') }) })` - ID 173: `test/e2e/app-dir/turbopack-loader-content-type/turbopack-loader-content-type.test.ts` β€” `describe('turbopack-loader-content-type', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should apply loader based on contentType glob pattern', async () => { const $ = await next.render$('/') const text = $('#text').text() expect(text).toBe('TEXT:Hello World') }) it('should apply loader based on contentType for text/javascript', async () => { const $ = await next.render$('/') const text = $('#js').text() expect(text).toBe('Hello from loader') }) it('should apply loader based on contentType regex', async () => { const $ = await next.render$('/') const text = $('#image').text() expect(text).toMatch(/^IMAGE:\d+ bytes$/) }) })` - ID 181: `test/e2e/app-dir/webpack-loader-conditions/webpack-loader-conditions.test.ts` β€” `describe('webpack-loader-conditions', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should render correctly on server site', async () => { const res = await next.fetch('/') const html = (await res.text()).replaceAll(/<!-- -->/g, '') expect(html).toContain(`server: {"default":true}`) expect(html).toContain(`client: {"default":true}`) expect(html).toContain(`foreignClient: {}`) }) it('should render correctly on client side', async () => { const browser = await next.browser('/') const text = await browser.elementByCss('body').text() expect(text).toContain(`server: ${JSON.stringify({ default: true })}`) expect(text).toContain(`client: ${JSON.stringify({ browser: true })}`) expect(text).toContain( `foreignClient: ${JSON.stringify({ browser: true, foreign: true })}` ) }) })` - ID 183: `test/e2e/app-dir/webpack-loader-fs/webpack-loader-fs.test.ts` β€” `describe('webpack-loader-fs', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should allow reading the input FS', async () => { const $ = await next.render$('/') expect($('#test').text()).toBe( "Buffer read: 18, string read: 'this is some data', binary read: 6765, glob read: 'one.txt'" ) }) })` - ID 184: `test/e2e/app-dir/webpack-loader-import-module/webpack-loader-import-module.test.ts` β€” `describe('webpack-loader-import-module', () => { const { next, isTurbopack } = nextTestSetup({ files: __dirname, }) it('should support this.importModule() in a webpack loader', async () => { const $ = await next.render$('/') expect($('#title').text()).toBe('Import Module Works') expect($('#items').text()).toBe('apple, banana, cherry') // CJS dependency that itself requires a JSON file expect($('#cjs-greeting').text()).toBe('hello from cjs') expect($('#version').text()).toBe('1.0.0') // ESM dependency imported from config-data.ts expect($('#esm-label').text()).toBe('hello from esm') // ESM .mjs module (config-data.mjs) expect($('#mjs-title').text()).toBe('ESM Config Works') expect($('#mjs-esm-label').text()).toBe('hello from esm') // resolveAlias: importModule with alias as request expect($('#alias-value').text()).toBe('resolved via alias') // resolveAlias: dependency of importModule target uses alias expect($('#alias-dep-label').text()).toBe('hello from esm') // loader rules: importModule on file requiring custom loader expect($('#custom-data-value').text()).toBe('hello from custom loader') // loader rules: dependency of importModule target needs custom loader expect($('#consumed-value').text()).toBe('hello from custom loader') if (isTurbopack) { // new URL('./image.png', import.meta.url) in url-wasm-data.ts expect($('#image-url').text()).toContain('image') expect($('#image-url').text()).toMatch(/\.png/) // WebAssembly add(1, 2) from add.wasm in url-wasm-data.ts expect($('#wasm-add-result').text()).toBe('3') // Dynamic import('./module.js') in url-wasm-data.ts expect($('#dynamic-value').text()).toBe('loaded dynamically') // new URL('./image.png', import.meta.url) in url-wasm-data.mjs expect($('#mjs-image-url').text()).toContain('image') expect($('#mjs-image-url').text()).toMatch(/\.png/) // WebAssembly add(10, 20) from add.wasm in url-wasm-data.mjs expect($('#mjs-wasm-add-result').text()).toBe('30') // Dynamic import('./module.js') in url-wasm-data.mjs expect($('#mjs-dynamic-value').text()).toBe('loaded dynamically') } }) })` - ID 185: `test/e2e/app-dir/webpack-loader-module-type/webpack-loader-module-type.test.ts` β€” `describe('webpack-loader-module-type', () => { const { next, isTurbopack } = nextTestSetup({ files: __dirname, }) // bytes type is Turbopack-only, webpack doesn't have a direct equivalent const itTurbopackOnly = isTurbopack ? it : it.skip it('should load svg as asset/resource and return URL', async () => { const $ = await next.render$('/') const src = $('#svg-url').text() // asset/resource should emit the file and return URL path expect(src).toMatch( /\/_next\/static\/(immutable\/)?media\/test\.[0-9a-z_-]+\.svg$/ ) }) itTurbopackOnly( 'should load data file as bytes and return Uint8Array', async () => { const $ = await next.render$('/') const bytesType = $('#bytes-type').text() const bytesLength = $('#bytes-length').text() const bytesText = $('#bytes-text').text() // eslint-disable-next-line jest/no-standalone-expect expect(bytesType).toBe('Uint8Array') // eslint-disable-next-line jest/no-standalone-expect expect(bytesLength).toBe('11') // eslint-disable-next-line jest/no-standalone-expect expect(bytesText).toBe('hello world') } ) })` - ID 186: `test/e2e/app-dir/webpack-loader-resolve/webpack-loader-resolve.test.ts` β€” `describe('webpack-loader-resolve', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should support resolving absolute path via loader getResolve', async () => { const $ = await next.render$('/') expect($('#absolute').text()).toBe('abc') expect($('#relative').text()).toBe('xyz') }) it('should support loader getResolve without options', async () => { const $ = await next.render$('/no-options') expect($('#no-options').text()).toBe('xyz') }) it('should support callback-style loader resolve', async () => { const $ = await next.render$('/callback') expect($('#resolved').text()).toBe('resolved-value.js') }) })` - ID 187: `test/e2e/app-dir/webpack-loader-resource-query/webpack-loader-resource-query.test.js` β€” `describe('webpack-loader-resource-query', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should pass query to loader', async () => { await next.render$('/') expect(next.cliOutput).toContain('resource query: ?test=hi') }) it('should apply loader based on resourceQuery', async () => { const $ = await next.render$('/') const text = $('#reversed').text() expect(text).toBe('dlroW olleH') }) it('should apply loader based on resourceQuery regex', async () => { const $ = await next.render$('/') const text = $('#upper').text() expect(text).toBe('HELLO WORLD') }) })` - ID 188: `test/e2e/app-dir/webpack-loader-ts-transform/webpack-loader-ts-transform.test.ts` β€” `describe('webpack-loader-ts-transform', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should accept Typescript returned from Webpack loaders', async () => { const $ = await next.render$('/') expect($('p').text()).toBe('something') }) })` - ID 189: `test/e2e/app-dir/with-babel/with-babel.test.ts` β€” `describe('with babel', () => { const { next, isNextStart, isTurbopack } = nextTestSetup({ files: __dirname, }) it('should support babel in app dir', async () => { const $ = await next.render$('/') expect($('h1').text()).toBe('hello') }) if (isNextStart) { // Turbopack always runs SWC, so this shouldn't be an issue, but this test // refers to a webpack-specific output path. // https://github.com/vercel/next.js/pull/51067 ;(isTurbopack ? it.skip : it)( 'should contain og package files in middleware', async () => { await retry(async () => { const middleware = await next.readFile('.next/server/middleware.js') // @vercel/og default font should be bundled expect(middleware).not.toContain('Geist-Regular.ttf') }) } ) } })` - ID 192: `test/e2e/config-schema-check/index.test.ts` β€” `describe('next.config.js schema validating - defaultConfig', () => { const { next } = nextTestSetup({ files: { 'pages/index.js': ` export default function Page() { return <p>hello world</p> } `, 'next.config.js': ` module.exports = (phase, { defaultConfig }) => { return defaultConfig } `, }, }) it('should validate against defaultConfig', async () => { const output = stripAnsi(next.cliOutput) expect(output).not.toContain('Invalid next.config.js options detected') }) })` - ID 193: `test/e2e/config-schema-check/index.test.ts` β€” `describe('next.config.js schema validating - invalid config', () => { const { next, isNextStart } = nextTestSetup({ files: { 'pages/index.js': ` export default function Page() { return <p>hello world</p> } `, 'next.config.js': ` module.exports = { badKey: 'badValue' } `, }, }) it('should warn the invalid next config', async () => { await check(() => { const output = stripAnsi(next.cliOutput) const warningTimes = output.split('badKey').length - 1 expect(output).toContain('Invalid next.config.js options detected') expect(output).toContain('badKey') // for next start and next build we both display the warnings expect(warningTimes).toBe(isNextStart ? 2 : 1) return 'success' }, 'success') }) })` - ID 198: `test/e2e/import-meta-env/import-meta-env.test.ts` β€” `describe('import.meta.env', () => { const { next } = nextTestSetup({ files: __dirname, }) it('exposes built-in environment values on the server and client', async () => { const browser = await next.browser('/docs') const expectedMode = isNextDev ? 'development' : 'production' expect( JSON.parse(await browser.elementByCss('#server-env dd').text()) ).toEqual({ DEV: isNextDev, PROD: !isNextDev, MODE: expectedMode, BASE_URL: '/docs/', SSR: true, }) expect( JSON.parse(await browser.elementByCss('#client-env dd').text()) ).toEqual({ DEV: isNextDev, PROD: !isNextDev, MODE: expectedMode, BASE_URL: '/docs/', SSR: false, }) }) it('supports static bracket access and unknown properties', async () => { const browser = await next.browser('/docs') const $ = await next.render$('/docs') const expectedMode = isNextDev ? 'development' : 'production' expect($('#server-env dd').eq(1).text()).toBe(expectedMode) expect($('#server-env dd').eq(2).text()).toBe('undefined') expect( await browser.elementByCss('#client-env dd:nth-of-type(2)').text() ).toBe(expectedMode) expect( await browser.elementByCss('#client-env dd:nth-of-type(3)').text() ).toBe('undefined') }) })` - ID 199: `test/e2e/import-meta-glob/import-meta-glob.test.ts` β€” `describe('import-meta-glob', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should resolve lazy glob modules', async () => { const $ = await next.render$('/') const lazyKeys = JSON.parse($('#lazy-keys').text()) expect(lazyKeys).toEqual([ './modules/bar.ts', './modules/foo.ts', './modules/skip.ts', ]) const lazyResults = JSON.parse($('#lazy-results').text()) expect(lazyResults).toEqual({ './modules/bar.ts': 'bar', './modules/foo.ts': 'foo', './modules/skip.ts': 'skip', }) }) it('should resolve eager glob modules', async () => { const $ = await next.render$('/') const eagerKeys = JSON.parse($('#eager-keys').text()) expect(eagerKeys).toEqual([ './modules/bar.ts', './modules/foo.ts', './modules/skip.ts', ]) const eagerResults = JSON.parse($('#eager-results').text()) expect(eagerResults).toEqual({ './modules/bar.ts': 'bar', './modules/foo.ts': 'foo', './modules/skip.ts': 'skip', }) }) it('should resolve named import glob modules', async () => { const $ = await next.render$('/') const defaultResults = JSON.parse($('#default-results').text()) expect(defaultResults).toEqual({ './modules/bar.ts': 'bar-value', './modules/foo.ts': 'foo-value', './modules/skip.ts': 'skip-value', }) }) it('should support negative patterns', async () => { const $ = await next.render$('/') const filteredKeys = JSON.parse($('#filtered-keys').text()) expect(filteredKeys).toEqual(['./modules/bar.ts', './modules/foo.ts']) const filteredResults = JSON.parse($('#filtered-results').text()) expect(filteredResults).toEqual({ './modules/bar.ts': 'bar', './modules/foo.ts': 'foo', }) }) it('should support multiple patterns', async () => { const $ = await next.render$('/') const multiKeys = JSON.parse($('#multi-keys').text()) expect(multiKeys).toEqual([ './modules/bar.ts', './modules/foo.ts', './modules/skip.ts', './other/baz.ts', ]) const multiResults = JSON.parse($('#multi-results').text()) expect(multiResults).toEqual({ './modules/bar.ts': 'bar', './modules/foo.ts': 'foo', './modules/skip.ts': 'skip', './other/baz.ts': 'baz', }) }) })` - ID 200: `test/e2e/jsconfig-baseurl/jsconfig-baseurl.test.ts` β€” `describe('jsconfig.json baseurl', () => { const { next } = nextTestSetup({ files: __dirname, }) describe('default behavior', () => { it('should render the page', async () => { const $ = await next.render$('/hello') expect($('body').text()).toMatch(/World/) }) // Integration ran this under `launchApp` only. e2e splits dev vs `next start` jobs, so // `it.skip` when !isNextDev is correct: the module-not-found overlay is dev-only; production // jobs still cover `should trace correctly` under `should build` below. ;(isNextDev ? it : it.skip)( 'should have correct module not found error', async () => { const contents = await next.readFile('pages/hello.js') try { await next.patchFile( 'pages/hello.js', contents.replace('components/world', 'components/worldd') ) await retry(async () => { await next.render('/hello').catch(() => {}) const strippedOutput = stripAnsi(next.cliOutput) expect(strippedOutput).toMatch( /Module not found: Can't resolve 'components\/worldd'/ ) }) } finally { await next.patchFile('pages/hello.js', contents) } } ) }) ;(isNextStart ? describe : describe.skip)('should build', () => { it('should trace correctly', async () => { const helloTrace = JSON.parse( await next.readFile('.next/server/pages/hello.js.nft.json') ) expect( helloTrace.files.some((file: string) => file.includes('components/world.js') ) ).toBe(false) expect( helloTrace.files.some((file: string) => file.includes('react/index.js')) ).toBe(true) }) }) })` - ID 207: `test/e2e/swc-plugins-env/index.test.ts` β€” `describe('swc-plugins-env', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) it('should pass correct environment to swc plugins', async () => { const $ = await next.render$('/') if (isNextDev) { expect($('main').text()).toBe('The SWC plugin received env=development') } else { expect($('main').text()).toBe('The SWC plugin received env=production') } }) })` - ID 208: `test/e2e/swc-plugins/index.test.ts` β€” `describe('supports swcPlugins', () => { const { next } = nextTestSetup({ files: __dirname, dependencies: { '@swc/plugin-react-remove-properties': '13.0.0', }, }) it('basic case', async () => { const html = await next.render('/') expect(html).toContain('Hello World') expect(html).not.toContain('data-custom-attribute') }) })` - ID 212: `test/e2e/transpile-packages-typescript-foreign/index.test.ts` β€” `describe('with transpilePackages', () => { const { next } = nextTestSetup({ files: __dirname, dependencies: { pkg: `file:./pkg`, }, nextConfig: { transpilePackages: ['pkg'], }, }) it('should work', async () => { const $ = await next.render$('/') expect($('main').text()).toEqual('Hello 123') }) })` - ID 213: `test/e2e/turbopack-import-with-type/index.test.ts` β€” `describe('turbopack-import-with-type', () => { const { next } = nextTestSetup({ files: __dirname, }) // Testing this together on one route ensures we also avoid weird duplicate module ident things it('supports import with type: text, type: bytes, and type: json', async () => { const response = JSON.parse(await next.render('/api')) expect(response).toEqual({ text: { typeofString: true, length: 12, content: 'hello world\n', }, jsAsText: { typeofString: true, content: jsContent, }, bytes: { instanceofUint8Array: true, length: 18, content: 'this is some data\n', }, jsAsBytes: { instanceofUint8Array: true, content: jsContent, }, configuredAsJsAsBytes: { instanceofUint8Array: true, content: "throw new Error('this file is configured as ecmascript but imported as bytes')\n", }, json: { typeofObject: true, content: { hello: 'world' }, }, jsonAsText: { typeofString: true, content: '{ "hello": "world" }\n', }, }) }) })` - ID 214: `test/e2e/turbopack-loader-config/index.test.ts` β€” `describe('turbopack-loader-config', () => { const { next, isTurbopack, isNextDev } = nextTestSetup({ files: __dirname, // we can't set `nextConfig` inline because it contains regexes that fail to serialize, it needs // to be set in a separate module (`next.config.ts`) }) if (!isTurbopack) { it('should only run the test in turbopack', () => {}) return } it('should replace modules with their loader-generated versions', async () => { const response = JSON.parse(await next.render('/api')) expect(response).toEqual({ foo: 'default return value', bar: 'has export substring' + (isNextDev ? ' on dev' : ' on prod'), }) }) })` - ID 217: `test/e2e/typescript/typescript.test.ts` β€” `describe('TypeScript Features', () => { const { next, isTurbopack } = nextTestSetup({ files: __dirname, dependencies: { sass: 'latest', }, }) it('should render the page', async () => { const $ = await next.render$('/hello') expect($('body').text()).toMatch(/Hello World/) expect($('body').text()).toMatch(/1000000000000/) }) it('should render the cookies page', async () => { const $ = await next.render$('/ssr/cookies') expect($('#cookies').text()).toBe('{}') }) it('should render the cookies page with cookies', async () => { const res = await next.fetch('/ssr/cookies', { headers: { Cookie: 'key=value;', }, }) const html = await res.text() expect(html).toContain(`{"key":"value"}`) }) it('should render the generics page', async () => { const $ = await next.render$('/generics') expect($('#value').text()).toBe('Hello World from Generic') }) it('should render the angle bracket type assertions page', async () => { const $ = await next.render$('/angle-bracket-type-assertions') expect($('#value').text()).toBe('test') }) // Turbopack prefers `.ts`/`.tsx` over `.js`/`.jsx`, webpack prefers `.js`/`.jsx` ;(isTurbopack ? it.skip : it)( 'should resolve files in correct order', async () => { const $ = await next.render$('/hello') // eslint-disable-next-line jest/no-standalone-expect expect($('#imported-value').text()).toBe('OK') } ) // old behavior: it.skip('should report type checking to stdout', () => { expect(next.cliOutput).toContain('waiting for typecheck results...') }) it('should respond to sync API route correctly', async () => { const html = await next.render('/api/sync') const data = JSON.parse(html) expect(data).toEqual({ code: 'ok' }) }) it('should respond to async API route correctly', async () => { const html = await next.render('/api/async') const data = JSON.parse(html) expect(data).toEqual({ code: 'ok' }) }) if (isNextDev) { it('should not fail to render when an inactive page has an error', async () => { await next.patchFile( 'pages/evil.tsx', `import React from 'react' export default function EvilPage(): JSX.Element { return <div notARealProp /> } ` ) try { const $ = await next.render$('/hello') expect($('body').text()).toMatch(/Hello World/) } finally { await next.deleteFile('pages/evil.tsx') } }) } if (isNextStart) { it('should build the app successfully', async () => { expect(next.cliOutput).toMatch(/Compiled successfully/) }) it('should not inform when using default tsconfig path', () => { expect(next.cliOutput).not.toMatch(/Using tsconfig file:/) }) } })` </details> <details> <summary>Deployment evidence for the additional scopes</summary> - `test/e2e/app-dir/app-config-crossorigin/index.test.ts` β€” `describe('app dir - crossOrigin config', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426785087/job/102715742650), [cache](https://github.com/vercel/next.js/actions/runs/34426785087/job/102715742724). - `test/e2e/app-dir/webpack-loader-module-type/webpack-loader-module-type.test.ts` β€” `describe('webpack-loader-module-type', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426785087/job/102715742634), [cache](https://github.com/vercel/next.js/actions/runs/34426785087/job/102715742671). - `test/e2e/app-dir/with-babel/with-babel.test.ts` β€” `describe('with babel', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426785087/job/102715742666), [cache](https://github.com/vercel/next.js/actions/runs/34426785087/job/102715742685). - `test/e2e/jsconfig-baseurl/jsconfig-baseurl.test.ts` β€” `describe('jsconfig.json baseurl', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426785087/job/102715742650), [cache](https://github.com/vercel/next.js/actions/runs/34426785087/job/102715742724). - `test/e2e/swc-plugins/index.test.ts` β€” `describe('supports swcPlugins', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426785087/job/102715742599), [cache](https://github.com/vercel/next.js/actions/runs/34426785087/job/102715742667). - `test/e2e/transpile-packages-typescript-foreign/index.test.ts` β€” `describe('with transpilePackages', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426785087/job/102715742666), [cache](https://github.com/vercel/next.js/actions/runs/34426785087/job/102715742685). - `test/e2e/typescript/typescript.test.ts` β€” `describe('TypeScript Features', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426785087/job/102715742666), [cache](https://github.com/vercel/next.js/actions/runs/34426785087/job/102715742685). </details> <!-- NEXT_JS_LLM --> --- .../app-dir/app-config-crossorigin/index.test.ts | 7 +------ .../import-meta-glob-text-type.test.ts | 5 +---- ...lockfiles-with-output-file-tracing-root.test.ts | 7 +------ .../multiple-lockfiles-with-turbo-root.test.ts | 7 +------ .../segment-config-ts/segment-config-ts.test.ts | 1 - .../trace-build-file/trace-build-file.test.ts | 1 - .../turbopack-loader-content-type.test.ts | 5 +---- .../webpack-loader-conditions.test.ts | 5 +---- .../webpack-loader-fs/webpack-loader-fs.test.ts | 5 +---- .../webpack-loader-import-module.test.ts | 7 +------ .../webpack-loader-module-type.test.ts | 5 +---- .../webpack-loader-resolve.test.ts | 8 +------- .../webpack-loader-resource-query.test.js | 5 +---- .../webpack-loader-ts-transform.test.ts | 6 +----- test/e2e/app-dir/with-babel/with-babel.test.ts | 7 +------ test/e2e/config-schema-check/index.test.ts | 14 ++------------ test/e2e/import-meta-env/import-meta-env.test.ts | 5 +---- test/e2e/import-meta-glob/import-meta-glob.test.ts | 5 +---- test/e2e/jsconfig-baseurl/jsconfig-baseurl.test.ts | 4 +--- test/e2e/swc-plugins-env/index.test.ts | 4 +--- test/e2e/swc-plugins/index.test.ts | 4 +--- .../index.test.ts | 7 +------ test/e2e/turbopack-import-with-type/index.test.ts | 7 +------ test/e2e/turbopack-loader-config/index.test.ts | 7 +------ test/e2e/typescript/typescript.test.ts | 4 +--- 25 files changed, 24 insertions(+), 118 deletions(-) diff --git a/test/e2e/app-dir/app-config-crossorigin/index.test.ts b/test/e2e/app-dir/app-config-crossorigin/index.test.ts index 3282fd4f0a61..79f7acf6e931 100644 --- a/test/e2e/app-dir/app-config-crossorigin/index.test.ts +++ b/test/e2e/app-dir/app-config-crossorigin/index.test.ts @@ -5,15 +5,10 @@ const assetPrefix = 'https://example.vercel.sh' if (!isNextStart) { describe('app dir - crossOrigin config', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should render correctly with assetPrefix: "/"', async () => { const $ = await next.render$('/') // Only potential external (assetPrefix) <script /> and <link /> should have crossorigin attribute diff --git a/test/e2e/app-dir/import-meta-glob-text-type/import-meta-glob-text-type.test.ts b/test/e2e/app-dir/import-meta-glob-text-type/import-meta-glob-text-type.test.ts index 1b43f99af361..8aad738221c2 100644 --- a/test/e2e/app-dir/import-meta-glob-text-type/import-meta-glob-text-type.test.ts +++ b/test/e2e/app-dir/import-meta-glob-text-type/import-meta-glob-text-type.test.ts @@ -7,13 +7,10 @@ const testFn = : describe testFn('turbopack `text` / `raw` module types', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return - it('should load matched files as strings through a `?raw` rule', async () => { const $ = await next.render$('/raw') const items = $('li') diff --git a/test/e2e/app-dir/multiple-lockfiles/multiple-lockfiles-with-output-file-tracing-root.test.ts b/test/e2e/app-dir/multiple-lockfiles/multiple-lockfiles-with-output-file-tracing-root.test.ts index 26ec60224c2f..fed19996bb32 100644 --- a/test/e2e/app-dir/multiple-lockfiles/multiple-lockfiles-with-output-file-tracing-root.test.ts +++ b/test/e2e/app-dir/multiple-lockfiles/multiple-lockfiles-with-output-file-tracing-root.test.ts @@ -2,7 +2,7 @@ import { join } from 'path' import { FileRef, nextTestSetup } from 'e2e-utils' describe('multiple-lockfiles - has-output-file-tracing-root', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: { app: new FileRef(join(__dirname, 'app')), // This will silence the multiple lockfiles warning. @@ -22,16 +22,11 @@ describe('multiple-lockfiles - has-output-file-tracing-root', () => { }, // So that ../package-lock.json doesn't leave the isolated testDir subDir: 'test', - skipDeployment: true, // The workspace file would suppress the warning itself, so the test // wouldn't be exercising `outputFileTracingRoot`. deleteWorkspaceFile: true, }) - if (skipped) { - return - } - it('should not have multiple lockfiles warnings', async () => { expect(next.cliOutput).not.toMatch( /We detected multiple lockfiles and selected the directory of .+ as the root directory\./ diff --git a/test/e2e/app-dir/multiple-lockfiles/multiple-lockfiles-with-turbo-root.test.ts b/test/e2e/app-dir/multiple-lockfiles/multiple-lockfiles-with-turbo-root.test.ts index 9ce9af3af040..119c9f7c4b2b 100644 --- a/test/e2e/app-dir/multiple-lockfiles/multiple-lockfiles-with-turbo-root.test.ts +++ b/test/e2e/app-dir/multiple-lockfiles/multiple-lockfiles-with-turbo-root.test.ts @@ -2,7 +2,7 @@ import { join } from 'path' import { FileRef, nextTestSetup } from 'e2e-utils' describe('multiple-lockfiles - has-turbo-root', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: { app: new FileRef(join(__dirname, 'app')), // This will silence the multiple lockfiles warning. @@ -22,16 +22,11 @@ describe('multiple-lockfiles - has-turbo-root', () => { }, // So that ../package-lock.json doesn't leave the isolated testDir subDir: 'test', - skipDeployment: true, // The workspace file would suppress the warning itself, so the test // wouldn't be exercising `turbopack.root`. deleteWorkspaceFile: true, }) - if (skipped) { - return - } - it('should not have multiple lockfiles warnings', async () => { expect(next.cliOutput).not.toMatch( /We detected multiple lockfiles and selected the directory of .+ as the root directory\./ diff --git a/test/e2e/app-dir/segment-config-ts/segment-config-ts.test.ts b/test/e2e/app-dir/segment-config-ts/segment-config-ts.test.ts index e36a01a4540f..540f414b0d57 100644 --- a/test/e2e/app-dir/segment-config-ts/segment-config-ts.test.ts +++ b/test/e2e/app-dir/segment-config-ts/segment-config-ts.test.ts @@ -3,7 +3,6 @@ import { nextTestSetup } from 'e2e-utils' describe('TypeScript type expressions in route segment config', () => { const { next, isNextStart } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) describe('app directory', () => { diff --git a/test/e2e/app-dir/trace-build-file/trace-build-file.test.ts b/test/e2e/app-dir/trace-build-file/trace-build-file.test.ts index 806f2da3c004..7c275d788adf 100644 --- a/test/e2e/app-dir/trace-build-file/trace-build-file.test.ts +++ b/test/e2e/app-dir/trace-build-file/trace-build-file.test.ts @@ -7,7 +7,6 @@ describe('trace-build-file', () => { const { next } = nextTestSetup({ files: __dirname, skipStart: !isNextDev, - skipDeployment: true, env: { // Enable persistent caching even when the git working directory is // dirty (e.g. when developing Next.js itself). Without this, the diff --git a/test/e2e/app-dir/turbopack-loader-content-type/turbopack-loader-content-type.test.ts b/test/e2e/app-dir/turbopack-loader-content-type/turbopack-loader-content-type.test.ts index 8d037e1668da..ee2b875cb526 100644 --- a/test/e2e/app-dir/turbopack-loader-content-type/turbopack-loader-content-type.test.ts +++ b/test/e2e/app-dir/turbopack-loader-content-type/turbopack-loader-content-type.test.ts @@ -1,13 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('turbopack-loader-content-type', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return - it('should apply loader based on contentType glob pattern', async () => { const $ = await next.render$('/') const text = $('#text').text() diff --git a/test/e2e/app-dir/webpack-loader-conditions/webpack-loader-conditions.test.ts b/test/e2e/app-dir/webpack-loader-conditions/webpack-loader-conditions.test.ts index 52f00ef05b41..ad87cb79239e 100644 --- a/test/e2e/app-dir/webpack-loader-conditions/webpack-loader-conditions.test.ts +++ b/test/e2e/app-dir/webpack-loader-conditions/webpack-loader-conditions.test.ts @@ -4,13 +4,10 @@ import { nextTestSetup } from 'e2e-utils' ;(process.env.IS_TURBOPACK_TEST ? describe : describe.skip)( 'webpack-loader-conditions', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return - it('should render correctly on server site', async () => { const res = await next.fetch('/') const html = (await res.text()).replaceAll(/<!-- -->/g, '') diff --git a/test/e2e/app-dir/webpack-loader-fs/webpack-loader-fs.test.ts b/test/e2e/app-dir/webpack-loader-fs/webpack-loader-fs.test.ts index f247c44ade27..60d395d40033 100644 --- a/test/e2e/app-dir/webpack-loader-fs/webpack-loader-fs.test.ts +++ b/test/e2e/app-dir/webpack-loader-fs/webpack-loader-fs.test.ts @@ -1,13 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('webpack-loader-fs', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return - it('should allow reading the input FS', async () => { const $ = await next.render$('/') expect($('#test').text()).toBe( diff --git a/test/e2e/app-dir/webpack-loader-import-module/webpack-loader-import-module.test.ts b/test/e2e/app-dir/webpack-loader-import-module/webpack-loader-import-module.test.ts index d2f9a7ddf9b3..c98c946cd1b6 100644 --- a/test/e2e/app-dir/webpack-loader-import-module/webpack-loader-import-module.test.ts +++ b/test/e2e/app-dir/webpack-loader-import-module/webpack-loader-import-module.test.ts @@ -1,15 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('webpack-loader-import-module', () => { - const { next, skipped, isTurbopack } = nextTestSetup({ + const { next, isTurbopack } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should support this.importModule() in a webpack loader', async () => { const $ = await next.render$('/') expect($('#title').text()).toBe('Import Module Works') diff --git a/test/e2e/app-dir/webpack-loader-module-type/webpack-loader-module-type.test.ts b/test/e2e/app-dir/webpack-loader-module-type/webpack-loader-module-type.test.ts index a63afb00be9e..31988b6b7e7c 100644 --- a/test/e2e/app-dir/webpack-loader-module-type/webpack-loader-module-type.test.ts +++ b/test/e2e/app-dir/webpack-loader-module-type/webpack-loader-module-type.test.ts @@ -1,13 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('webpack-loader-module-type', () => { - const { next, isTurbopack, skipped } = nextTestSetup({ + const { next, isTurbopack } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return - // bytes type is Turbopack-only, webpack doesn't have a direct equivalent const itTurbopackOnly = isTurbopack ? it : it.skip diff --git a/test/e2e/app-dir/webpack-loader-resolve/webpack-loader-resolve.test.ts b/test/e2e/app-dir/webpack-loader-resolve/webpack-loader-resolve.test.ts index d6e671eba40f..f4bb205927b2 100644 --- a/test/e2e/app-dir/webpack-loader-resolve/webpack-loader-resolve.test.ts +++ b/test/e2e/app-dir/webpack-loader-resolve/webpack-loader-resolve.test.ts @@ -1,16 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('webpack-loader-resolve', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - // This test is skipped because it's only expected to run in turbopack, which isn't enabled for builds - skipDeployment: true, }) - if (skipped) { - return - } - it('should support resolving absolute path via loader getResolve', async () => { const $ = await next.render$('/') expect($('#absolute').text()).toBe('abc') diff --git a/test/e2e/app-dir/webpack-loader-resource-query/webpack-loader-resource-query.test.js b/test/e2e/app-dir/webpack-loader-resource-query/webpack-loader-resource-query.test.js index 6a86fffdc412..247a6ad44d1d 100644 --- a/test/e2e/app-dir/webpack-loader-resource-query/webpack-loader-resource-query.test.js +++ b/test/e2e/app-dir/webpack-loader-resource-query/webpack-loader-resource-query.test.js @@ -1,13 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('webpack-loader-resource-query', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return - it('should pass query to loader', async () => { await next.render$('/') diff --git a/test/e2e/app-dir/webpack-loader-ts-transform/webpack-loader-ts-transform.test.ts b/test/e2e/app-dir/webpack-loader-ts-transform/webpack-loader-ts-transform.test.ts index fa1a67bd32a4..ca995abd2da4 100644 --- a/test/e2e/app-dir/webpack-loader-ts-transform/webpack-loader-ts-transform.test.ts +++ b/test/e2e/app-dir/webpack-loader-ts-transform/webpack-loader-ts-transform.test.ts @@ -1,14 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('webpack-loader-ts-transform', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - // This test is skipped because it's only expected to run in turbopack, which isn't enabled for builds - skipDeployment: true, }) - if (skipped) return - it('should accept Typescript returned from Webpack loaders', async () => { const $ = await next.render$('/') expect($('p').text()).toBe('something') diff --git a/test/e2e/app-dir/with-babel/with-babel.test.ts b/test/e2e/app-dir/with-babel/with-babel.test.ts index 11d143c4825a..7a6310aa2d0b 100644 --- a/test/e2e/app-dir/with-babel/with-babel.test.ts +++ b/test/e2e/app-dir/with-babel/with-babel.test.ts @@ -2,15 +2,10 @@ import { nextTestSetup } from 'e2e-utils' import { retry } from 'next-test-utils' describe('with babel', () => { - const { next, isNextStart, isTurbopack, skipped } = nextTestSetup({ + const { next, isNextStart, isTurbopack } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should support babel in app dir', async () => { const $ = await next.render$('/') expect($('h1').text()).toBe('hello') diff --git a/test/e2e/config-schema-check/index.test.ts b/test/e2e/config-schema-check/index.test.ts index 9e483ae0dcb3..3f58f078b36b 100644 --- a/test/e2e/config-schema-check/index.test.ts +++ b/test/e2e/config-schema-check/index.test.ts @@ -3,7 +3,7 @@ import { nextTestSetup } from 'e2e-utils' import { check } from 'next-test-utils' describe('next.config.js schema validating - defaultConfig', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: { 'pages/index.js': ` export default function Page() { @@ -16,13 +16,8 @@ describe('next.config.js schema validating - defaultConfig', () => { } `, }, - skipDeployment: true, }) - if (skipped) { - return - } - it('should validate against defaultConfig', async () => { const output = stripAnsi(next.cliOutput) @@ -31,7 +26,7 @@ describe('next.config.js schema validating - defaultConfig', () => { }) describe('next.config.js schema validating - invalid config', () => { - const { next, isNextStart, skipped } = nextTestSetup({ + const { next, isNextStart } = nextTestSetup({ files: { 'pages/index.js': ` export default function Page() { @@ -44,13 +39,8 @@ describe('next.config.js schema validating - invalid config', () => { } `, }, - skipDeployment: true, }) - if (skipped) { - return - } - it('should warn the invalid next config', async () => { await check(() => { const output = stripAnsi(next.cliOutput) diff --git a/test/e2e/import-meta-env/import-meta-env.test.ts b/test/e2e/import-meta-env/import-meta-env.test.ts index f76acb0d266e..63760c07eca9 100644 --- a/test/e2e/import-meta-env/import-meta-env.test.ts +++ b/test/e2e/import-meta-env/import-meta-env.test.ts @@ -6,13 +6,10 @@ const testFn = : describe testFn('import.meta.env', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return - it('exposes built-in environment values on the server and client', async () => { const browser = await next.browser('/docs') const expectedMode = isNextDev ? 'development' : 'production' diff --git a/test/e2e/import-meta-glob/import-meta-glob.test.ts b/test/e2e/import-meta-glob/import-meta-glob.test.ts index 412894f72192..738ffda4f54c 100644 --- a/test/e2e/import-meta-glob/import-meta-glob.test.ts +++ b/test/e2e/import-meta-glob/import-meta-glob.test.ts @@ -7,13 +7,10 @@ const testFn = : describe testFn('import-meta-glob', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return - it('should resolve lazy glob modules', async () => { const $ = await next.render$('/') const lazyKeys = JSON.parse($('#lazy-keys').text()) diff --git a/test/e2e/jsconfig-baseurl/jsconfig-baseurl.test.ts b/test/e2e/jsconfig-baseurl/jsconfig-baseurl.test.ts index 25a3b5138e03..667c1ddeca74 100644 --- a/test/e2e/jsconfig-baseurl/jsconfig-baseurl.test.ts +++ b/test/e2e/jsconfig-baseurl/jsconfig-baseurl.test.ts @@ -3,11 +3,9 @@ import stripAnsi from 'next/dist/compiled/strip-ansi' import { retry } from 'next-test-utils' describe('jsconfig.json baseurl', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return describe('default behavior', () => { it('should render the page', async () => { diff --git a/test/e2e/swc-plugins-env/index.test.ts b/test/e2e/swc-plugins-env/index.test.ts index dce816d5914b..86e4c9c2e3f9 100644 --- a/test/e2e/swc-plugins-env/index.test.ts +++ b/test/e2e/swc-plugins-env/index.test.ts @@ -1,11 +1,9 @@ import { nextTestSetup } from 'e2e-utils' describe('swc-plugins-env', () => { - const { next, skipped, isNextDev } = nextTestSetup({ + const { next, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return it('should pass correct environment to swc plugins', async () => { const $ = await next.render$('/') diff --git a/test/e2e/swc-plugins/index.test.ts b/test/e2e/swc-plugins/index.test.ts index ca78dc13c7c5..fcbafdeaacdb 100644 --- a/test/e2e/swc-plugins/index.test.ts +++ b/test/e2e/swc-plugins/index.test.ts @@ -2,14 +2,12 @@ import { nextTestSetup, isNextDev } from 'e2e-utils' describe('swcPlugins', () => { describe('supports swcPlugins', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, dependencies: { '@swc/plugin-react-remove-properties': '13.0.0', }, }) - if (skipped) return it('basic case', async () => { const html = await next.render('/') diff --git a/test/e2e/transpile-packages-typescript-foreign/index.test.ts b/test/e2e/transpile-packages-typescript-foreign/index.test.ts index f99e91a8b918..4572e28bf856 100644 --- a/test/e2e/transpile-packages-typescript-foreign/index.test.ts +++ b/test/e2e/transpile-packages-typescript-foreign/index.test.ts @@ -39,9 +39,8 @@ Module parse failed: Unexpected token`) }) describe('with transpilePackages', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, dependencies: { pkg: `file:./pkg`, }, @@ -50,10 +49,6 @@ Module parse failed: Unexpected token`) }, }) - if (skipped) { - return - } - it('should work', async () => { const $ = await next.render$('/') expect($('main').text()).toEqual('Hello 123') diff --git a/test/e2e/turbopack-import-with-type/index.test.ts b/test/e2e/turbopack-import-with-type/index.test.ts index de628934f21f..2aa4e5bd33be 100644 --- a/test/e2e/turbopack-import-with-type/index.test.ts +++ b/test/e2e/turbopack-import-with-type/index.test.ts @@ -8,15 +8,10 @@ throw new Error('please dont execute me') ;(process.env.IS_TURBOPACK_TEST ? describe : describe.skip)( 'turbopack-import-with-type', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - // Testing this together on one route ensures we also avoid weird duplicate module ident things it('supports import with type: text, type: bytes, and type: json', async () => { const response = JSON.parse(await next.render('/api')) diff --git a/test/e2e/turbopack-loader-config/index.test.ts b/test/e2e/turbopack-loader-config/index.test.ts index 9eae254ef6f3..d149c30a80a7 100644 --- a/test/e2e/turbopack-loader-config/index.test.ts +++ b/test/e2e/turbopack-loader-config/index.test.ts @@ -1,17 +1,12 @@ import { nextTestSetup } from 'e2e-utils' describe('turbopack-loader-config', () => { - const { next, isTurbopack, isNextDev, skipped } = nextTestSetup({ + const { next, isTurbopack, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, // we can't set `nextConfig` inline because it contains regexes that fail to serialize, it needs // to be set in a separate module (`next.config.ts`) }) - if (skipped) { - return - } - if (!isTurbopack) { it('should only run the test in turbopack', () => {}) return diff --git a/test/e2e/typescript/typescript.test.ts b/test/e2e/typescript/typescript.test.ts index 63a265b2d5ed..56bfccb616ba 100644 --- a/test/e2e/typescript/typescript.test.ts +++ b/test/e2e/typescript/typescript.test.ts @@ -1,14 +1,12 @@ import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' describe('TypeScript Features', () => { - const { next, isTurbopack, skipped } = nextTestSetup({ + const { next, isTurbopack } = nextTestSetup({ files: __dirname, dependencies: { sass: 'latest', }, - skipDeployment: true, }) - if (skipped) return it('should render the page', async () => { const $ = await next.render$('/hello') From 77fa82756a2128ecfa66e22f91f4fb430b53a341 Mon Sep 17 00:00:00 2001 From: Jamiboy Mohammad <jamiboym@gmail.com> Date: Tue, 15 Sep 2026 10:29:06 -0700 Subject: [PATCH 12/14] test: enable verified pages-router deploy tests (#98526) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Enable the same 12 previously selected deployment-test scopes across 12 pages-router test files, now in a stack rooted on canary. Remove 11 `skipDeployment` options and their obsolete skip guards. Remove the selected suite’s deploy-only placeholder return. Other mode, bundler, middleware, and Cache Components exclusions remain in place. This preserves the selection with passing evidence from the previous deployment runs. No additional candidate scopes are enabled; excluded variants are not counted as deployment coverage. ## Verification - All selected test registration names and assertion bodies match the previous enabled revision, checked by AST comparison. - Verified that the canary diff contains only the inventoried exclusions and their obsolete skip plumbing; other exclusions are preserved. - Formatting and lint passed; 77 gate infrastructure unit tests passed. - Full local bootstrap was blocked by missing package-level dependencies in the temporary worktree. Fresh deployment execution on these rewritten commits remains to be verified in CI. <details> <summary>Preserved scope inventory (12)</summary> - ID 222: `test/e2e/404-page-custom-error/404-page-custom-error.test.ts` β€” `describe('Default 404 Page with custom _error', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should respond to 404 correctly', async () => { const res = await next.fetch('/404') expect(res.status).toBe(404) expect(await res.text()).toContain('This page could not be found') }) it('should render error correctly', async () => { const text = await next.render('/err') expect(text).toContain(isNextDev ? 'oops' : 'Internal Server Error') }) it('should render index page normal', async () => { const html = await next.render('/') expect(html).toContain('hello from index') }) ;(isNextStart ? it : it.skip)( 'should set pages404 in routes-manifest correctly', async () => { const data = JSON.parse(await next.readFile('.next/routes-manifest.json')) expect(data.pages404).toBe(true) } ) ;(isNextStart ? it : it.skip)('should have output 404.html', async () => { const pagesManifest = await next.readJSON( '.next/server/pages-manifest.json' ) const page = pagesManifest['/404'] expect(page.endsWith('.html')).toBe(true) }) })` - ID 223: `test/e2e/404-page/404-page.test.ts` β€” `describe('404 Page Support', () => { const { next } = nextTestSetup({ files: __dirname, }) const gip404Err = /`pages\/404` can not have getInitialProps\/getServerSideProps/ it('should use pages/404', async () => { const html = await next.render('/abc') expect(html).toContain('custom 404 page') }) it('should set correct status code with pages/404', async () => { const res = await next.fetch('/abc') expect(res.status).toBe(404) }) it('should use pages/404 for .d.ts file', async () => { const html = await next.render('/invalidExtension') expect(html).toContain('custom 404 page') }) it('should not error when visited directly', async () => { const res = await next.fetch('/404') expect(res.status).toBe(404) expect(await res.text()).toContain('custom 404 page') }) it('should render _error for a 500 error still', async () => { const html = await next.render('/err') expect(html).not.toContain('custom 404 page') expect(html).toContain(isNextDev ? 'oops' : 'Internal Server Error') }) if (isNextStart) { it('should output 404.html during build', async () => { const manifest = await next.readJSON('.next/server/pages-manifest.json') const page = manifest['/404'] expect(page.endsWith('.html')).toBe(true) }) it('should still output 404.js anyway', async () => { expect(await next.hasFile('.next/server/pages/404.js')).toBe(true) }) it('should add /404 to pages-manifest correctly', async () => { const manifest = await next.readJSON('.next/server/pages-manifest.json') expect('/404' in manifest).toBe(true) }) } if (isNextDev) { it('falls back to _error correctly without pages/404', async () => { const original404 = await next.readFile('pages/404.js') try { await next.deleteFile('pages/404.js') await retry(async () => { const res = await next.fetch('/abc') expect(res.status).toBe(404) expect(await res.text()).toContain('This page could not be found') }) } finally { await next.patchFile('pages/404.js', original404) } }) it('shows error with getInitialProps in pages/404 dev', async () => { const original404 = await next.readFile('pages/404.js') try { await next.patchFile( 'pages/404.js', ` const page = () => 'custom 404 page' page.getInitialProps = () => ({ a: 'b' }) export default page ` ) await next.render('/abc') await retry(async () => { expect(next.cliOutput).toMatch(gip404Err) }) } finally { await next.patchFile('pages/404.js', original404) } }) it('does not show error with getStaticProps in pages/404 dev', async () => { const original404 = await next.readFile('pages/404.js') const getOutput = next.getCliOutputFromHere() try { await next.patchFile( 'pages/404.js', ` const page = () => 'custom 404 page' export const getStaticProps = () => ({ props: { a: 'b' } }) export default page ` ) await next.render('/abc') await retry(async () => { const html = await next.render('/abc') expect(html).toContain('custom 404 page') }) expect(getOutput()).not.toMatch(gip404Err) } finally { await next.patchFile('pages/404.js', original404) } }) it('shows error with getServerSideProps in pages/404 dev', async () => { const original404 = await next.readFile('pages/404.js') try { await next.patchFile( 'pages/404.js', ` const page = () => 'custom 404 page' export const getServerSideProps = () => ({ props: { a: 'b' } }) export default page ` ) await next.render('/abc') await retry(async () => { expect(next.cliOutput).toMatch(gip404Err) }) } finally { await next.patchFile('pages/404.js', original404) } }) } })` - ID 225: `test/e2e/500-page/500-page.test.ts` β€” `describe('500 Page Support', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should use pages/500', async () => { const html = await next.render('/500') expect(html).toContain('custom 500 page') }) it('should set correct status code with pages/500', async () => { const res = await next.fetch('/500') expect(res.status).toBe(500) }) it('should not error when visited directly', async () => { const res = await next.fetch('/500') expect(res.status).toBe(500) expect(await res.text()).toContain('custom 500 page') }) if (isNextStart) { it('should output 500.html during build', async () => { const manifest = await next.readJSON('.next/server/pages-manifest.json') const page = manifest['/500'] expect(page.endsWith('.html')).toBe(true) }) it('should add /500 to pages-manifest correctly', async () => { const manifest = await next.readJSON('.next/server/pages-manifest.json') expect('/500' in manifest).toBe(true) }) } if (isNextDev) { it('shows error with getInitialProps in pages/500 dev', async () => { const original500 = await next.readFile('pages/500.js') try { await next.patchFile( 'pages/500.js', ` const page = () => 'custom 500 page' page.getInitialProps = () => ({ a: 'b' }) export default page ` ) await next.render('/500') await retry(async () => { expect(next.cliOutput).toMatch( /`pages\/500` can not have getInitialProps\/getServerSideProps/ ) }) } finally { await next.patchFile('pages/500.js', original500) } }) it('does not show error with getStaticProps in pages/500 dev', async () => { const original500 = await next.readFile('pages/500.js') const outputBefore = next.cliOutput.length try { await next.patchFile( 'pages/500.js', ` const page = () => 'custom 500 page' export const getStaticProps = () => ({ props: { a: 'b' } }) export default page ` ) await next.render('/abc') await retry(async () => { expect(next.cliOutput.slice(outputBefore)).not.toMatch( /`pages\/500` can not have getInitialProps\/getServerSideProps/ ) }) } finally { await next.patchFile('pages/500.js', original500) } }) it('shows error with getServerSideProps in pages/500 dev', async () => { const original500 = await next.readFile('pages/500.js') try { await next.patchFile( 'pages/500.js', ` const page = () => 'custom 500 page' export const getServerSideProps = () => ({ props: { a: 'b' } }) export default page ` ) await next.render('/500') await retry(async () => { expect(next.cliOutput).toMatch( /`pages\/500` can not have getInitialProps\/getServerSideProps/ ) }) } finally { await next.patchFile('pages/500.js', original500) } }) } })` - ID 227: `test/e2e/api-resolver-query-writeable/api-resolver-query-writeable.test.ts` β€” `describe('api-resolver-query-writeable', () => { const { next } = nextTestSetup({ files: __dirname, startCommand: 'node server.js', serverReadyPattern: /Next mode: (production|development)/, dependencies: { 'get-port': '5.1.1', express: '5.1.0', }, }) it('should allow req.query to be writable and reflect changes made in the API handler', async () => { const res = await next.fetch('/api?hello=yes', { headers: { 'Content-Type': 'application/json; charset=utf-8', }, }) if (!res.ok) { throw new Error('Fetch failed') } const data = await res.json() expect(data).toEqual({ query: { hello: 'yes', changed: 'yes' } }) }) })` - ID 230: `test/e2e/app-document/client.test.ts` β€” `describe('Document and App - Client side', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) it('should share module state with pages', async () => { const browser = await next.browser('/shared') const text = await browser.elementByCss('#currentstate').text() expect(text).toBe('UPDATED CLIENT') }) if (isNextDev) { it('should detect the changes to pages/_app.js and display it', async () => { const appPath = 'pages/_app.js' const originalContent = await next.readFile(appPath) try { const browser = await next.browser('/') const text = await browser.elementByCss('#hello-hmr').text() expect(text).toBe('Hello HMR') // change the content const editedContent = originalContent.replace('Hello HMR', 'Hi HMR') await next.patchFile(appPath, editedContent) await retry(async () => expect(await browser.elementByCss('body').text()).toContain('Hi HMR') ) // add the original content await next.patchFile(appPath, originalContent) await retry(async () => expect(await browser.elementByCss('body').text()).toContain( 'Hello HMR' ) ) } finally { await next.patchFile(appPath, originalContent) } }) it('should detect the changes to pages/_document.js and display it', async () => { const appPath = 'pages/_document.js' const originalContent = await next.readFile(appPath) try { const browser = await next.browser('/') const text = await browser.elementByCss('#hello-hmr').text() expect(text).toBe('Hello HMR') const editedContent = originalContent.replace( 'Hello Document HMR', 'Hi Document HMR' ) // change the content await next.patchFile(appPath, editedContent) await retry(async () => expect(await browser.elementByCss('body').text()).toContain( 'Hi Document HMR' ) ) // add the original content await next.patchFile(appPath, originalContent) await retry(async () => expect(await browser.elementByCss('body').text()).toContain( 'Hello Document HMR' ) ) } finally { await next.patchFile(appPath, originalContent) } }) it('should keep state between page navigations', async () => { const browser = await next.browser('/') const randomNumber = await browser.elementByCss('#random-number').text() const switchedRandomNumer = await browser .elementByCss('#about-link') .click() .waitForElementByCss('.page-about') .elementByCss('#random-number') .text() expect(switchedRandomNumer).toBe(randomNumber) await browser.close() }) } })` - ID 250: `test/e2e/disable-js/disable-js.test.ts` β€” `describe('disabled runtime JS', () => { const { next, isNextDev, isNextStart } = nextTestSetup({ files: __dirname, }) it('should render the page', async () => { const html = await next.render('/') expect(html).toMatch(/Hello World/) }) it('should not have __NEXT_DATA__ script', async () => { const html = await next.render('/') const $ = cheerio.load(html) if (isNextStart) { expect($('script#__NEXT_DATA__').length).toBe(0) } if (isNextDev) { expect($('script#__NEXT_DATA__').length).toBe(1) } }) if (isNextStart) { it('should not have scripts', async () => { const html = await next.render('/') const $ = cheerio.load(html) expect($('script[src]').length).toBe(0) }) it('should not have preload links', async () => { const html = await next.render('/') const $ = cheerio.load(html) expect($('link[rel=preload]').length).toBe(0) }) } if (isNextDev) { it('should have a script for each preload link', async () => { const html = await next.render('/') const $ = cheerio.load(html) const preloadLinks = $('link[rel=preload]') preloadLinks.each((idx, element) => { const url = $(element).attr('href') expect($(`script[src="${url}"]`).length).toBe(1) }) }) } })` - ID 255: `test/e2e/gip-identifier/gip-identifier.test.ts` β€” `describe('gip identifiers', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) const getNextData = async () => { const html = await next.render('/') const $ = cheerio.load(html) return JSON.parse($('#__NEXT_DATA__').text()) } it('should not have gip or appGip in NEXT_DATA for page without getInitialProps', async () => { const data = await getNextData() expect(data.gip).toBe(undefined) expect(data.appGip).toBe(undefined) }) if (isNextDev) { it('should have gip in NEXT_DATA for page with getInitialProps', async () => { await next.patchFile( 'pages/index.js', ` const Page = () => 'hi' Page.getInitialProps = () => ({ hello: 'world' }) export default Page ` ) await retry(async () => { const data = await getNextData() expect(data.gip).toBe(true) }) }) it('should have gip and appGip in NEXT_DATA for page with getInitialProps and _app with getInitialProps', async () => { await next.patchFile( 'pages/_app.js', ` const App = ({ Component, pageProps }) => <Component {...pageProps} /> App.getInitialProps = async (ctx) => { let pageProps = {} if (ctx.Component.getInitialProps) { pageProps = await ctx.Component.getInitialProps(ctx.ctx) } return { pageProps } } export default App ` ) await retry(async () => { const data = await getNextData() expect(data.gip).toBe(true) expect(data.appGip).toBe(true) }) }) it('should only have appGip in NEXT_DATA for page without getInitialProps and _app with getInitialProps', async () => { await next.patchFile('pages/index.js', `export default () => 'hi'\n`) await retry(async () => { const data = await getNextData() expect(data.gip).toBe(undefined) expect(data.appGip).toBe(true) }) }) } })` - ID 258: `test/e2e/i18n-data-fetching-redirect/redirect-from-context.test.ts` β€” `describe('i18n-data-fetching-redirect', () => { const { next } = nextTestSetup({ files: { pages: new FileRef(join(__dirname, 'app/pages')), 'next.config.js': new FileRef(join(__dirname, 'app/next.config.js')), }, dependencies: {}, }) describe('Redirect to locale from context', () => { test.each` path | locale ${'gssp-redirect'} | ${'en'} ${'gssp-redirect'} | ${'sv'} ${'gsp-blocking-redirect'} | ${'en'} ${'gsp-blocking-redirect'} | ${'sv'} ${'gsp-fallback-redirect'} | ${'en'} ${'gsp-fallback-redirect'} | ${'sv'} `('$path $locale', async ({ path, locale }) => { const browser = await next.browser(`/${locale}/${path}/from-ctx`) await check( () => browser.eval('window.location.pathname'), `/${locale}/home` ) expect(await browser.elementByCss('#router-locale').text()).toBe(locale) expect(await browser.elementByCss('#router-pathname').text()).toBe( '/home' ) expect(await browser.elementByCss('#router-as-path').text()).toBe('/home') }) test.each` path | locale ${'gssp-redirect'} | ${'en'} ${'gssp-redirect'} | ${'sv'} ${'gsp-blocking-redirect'} | ${'en'} ${'gsp-blocking-redirect'} | ${'sv'} ${'gsp-fallback-redirect'} | ${'en'} ${'gsp-fallback-redirect'} | ${'sv'} `('next/link $path $locale', async ({ path, locale }) => { const browser = await next.browser(`/${locale}`) await browser.eval('window.beforeNav = 1') await browser.elementByCss(`#to-${path}-from-ctx`).click() await check( () => browser.eval('window.location.pathname'), `/${locale}/home` ) expect(await browser.eval('window.beforeNav')).toBe(1) expect(await browser.elementByCss('#router-locale').text()).toBe(locale) expect(await browser.elementByCss('#router-pathname').text()).toBe( '/home' ) expect(await browser.elementByCss('#router-as-path').text()).toBe('/home') }) }) })` - ID 261: `test/e2e/i18n-ignore-rewrite-source-locale/rewrites-with-basepath.test.ts` β€” `describe('i18n-ignore-rewrite-source-locale with basepath', () => { const { next } = nextTestSetup({ files: __dirname, }) test.each(locales)( 'get public file by skipping locale in rewrite, locale: %s', async (locale) => { const res = await renderViaHTTP( next.url, `/basepath${locale}/rewrite-files/file.txt` ) expect(res).toContain('hello from file.txt') } ) test.each(locales)( 'call api by skipping locale in rewrite, locale: %s', async (locale) => { const res = await renderViaHTTP( next.url, `/basepath${locale}/rewrite-api/hello` ) expect(res).toContain('hello from api') } ) // build artifacts aren't available on deploy if (!(global as any).isNextDeploy) { // chunks are not written to disk with TURBOPACK ;(process.env.IS_TURBOPACK_TEST ? it.skip.each : it.each)(locales)( 'get _next/static/ files by skipping locale in rewrite, locale: %s', async (locale) => { const chunks = ( await fs.readdir( path.join(next.testDir, next.distDir, 'static', 'chunks') ) ).filter((f) => f.endsWith('.js')) await Promise.all( chunks.map(async (file) => { const res = await fetchViaHTTP( next.url, `/basepath${locale}/rewrite-files/_next/static/chunks/${file}` ) // eslint-disable-next-line jest/no-standalone-expect expect(res.status).toBe(200) }) ) } ) } })` - ID 267: `test/e2e/legacy-link-behavior/index.test.ts` β€” `describe('Link with legacyBehavior', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) describe('if the child is an <a> tag', () => { it('forwards the href attribute', async () => { const $ = await next.render$('/') const $a = $('a[href="/about"]') expect($a.text()).toBe('About') expect($a.attr('href')).toBe('/about') }) it('navigates correctly', async () => { const browser = await next.browser('/') await browser.elementByCss('a[href="/about"]').click() const title = await browser.elementByCss('#about-page').text() expect(title).toBe('About Page') }) }) it('works if the child is a number', async () => { const browser = await next.browser('/child-is-a-number') await browser.elementByCss('a[href="/about"]').click() const title = await browser.elementByCss('h1').text() expect(title).toBe('About Page') }) it('works if the child is a string', async () => { const browser = await next.browser('/child-is-a-string') await browser.elementByCss('a[href="/about"]').click() const title = await browser.elementByCss('h1').text() expect(title).toBe('About Page') }) it('errors when calling onClick without the event', async () => { const browser = await next.browser('/invalid-onclick') expect(await browser.elementByCss('#errors').text()).toBe('0') await browser.elementByCss('#custom-button').click() expect(await browser.elementByCss('#errors').text()).toBe('1') }) it('should show a deprecation warning', async () => { const browser = await next.browser('/') await retry(async () => { const logs = await browser.log() const errors = logs.filter((log) => log.source === 'error') if (isNextDev) { expect(errors).toEqual([ { message: '`legacyBehavior` is deprecated and will be removed in a future release. A codemod is available to upgrade your components:\n\n' + 'npx @next/codemod@latest new-link .\n\n' + 'Learn more: https://nextjs.org/docs/app/building-your-application/upgrading/codemods#remove-a-tags-from-link-components', source: 'error', }, ]) } else { expect(errors).toEqual([]) } }) }) describe('passHref', () => { const expectHrefToBeForwardedInSSR = async (path: string) => { const $ = await next.render$(path) const $a = $('a[href="/about"]') expect($a.text()).toBe('About') expect($a.attr('href')).toBe('/about') } const expectLinkClickToNavigate = async (path: string) => { const browser = await next.browser(path) if (isNextDev) { // We expect a deprecation warning (in a collapsed redbox), but no other errors (e.g. no errors thrown by Link) await openRedbox(browser) expect(await createRedboxSnapshot(browser, next)).toEqual( expect.objectContaining<Partial<ErrorSnapshot>>({ label: 'Console Error', description: expect.stringContaining( `\`legacyBehavior\` is deprecated and will be removed in a future release.` ), }) ) await browser.locateRedbox().press('Escape') // Close redbox so we can click the link } await browser.elementByCss('a[href="/about"]').click() const title = await browser.elementByCss('h1').text() expect(title).toBe('About Page') } describe('with no prefech config', () => { it('forwards the href attribute', async () => { await expectHrefToBeForwardedInSSR('/passHref/default') }) it('navigates correctly (failing)', async () => { if (isNextDev) { // FIXME(NAR-876): false positive due to debug info blocking the child // await expectLinkClickToNavigate('/passHref/default') const browser = await next.browser('/passHref/default') await expect(browser).toDisplayRedbox(` { "description": "\`<Link legacyBehavior>\` received a direct child that is either a Server Component, or JSX that was loaded with React.lazy(). This is not supported. Either remove legacyBehavior, or make the direct child a Client Component that renders the Link's \`<a>\` tag.", "environmentLabel": null, "label": "Runtime Error", "source": "app/passHref/default/page.tsx (7:7) @ Page > 7 | <Link href="/about" legacyBehavior passHref> | ^", "stack": [ "Page app/passHref/default/page.tsx (7:7)", ], } `) } else { await expectLinkClickToNavigate('/passHref/default') } }) }) describe('with runtime prefetch', () => { it('forwards the href attribute', async () => { await expectHrefToBeForwardedInSSR('/passHref/runtime') }) it('navigates correctly (failing)', async () => { if (isNextDev) { // FIXME(NAR-876): false positive due to debug info blocking the child // await expectLinkClickToNavigate('/passHref/runtime') const browser = await next.browser('/passHref/runtime') await expect(browser).toDisplayRedbox(` { "description": "\`<Link legacyBehavior>\` received a direct child that is either a Server Component, or JSX that was loaded with React.lazy(). This is not supported. Either remove legacyBehavior, or make the direct child a Client Component that renders the Link's \`<a>\` tag.", "environmentLabel": null, "label": "Runtime Error", "source": "app/passHref/runtime/page.tsx (9:7) @ Page > 9 | <Link href="/about" legacyBehavior passHref> | ^", "stack": [ "Page app/passHref/runtime/page.tsx (9:7)", ], } `) } else { await expectLinkClickToNavigate('/passHref/runtime') } }) }) describe('in dynamic code', () => { it('forwards the href attribute', async () => { await expectHrefToBeForwardedInSSR('/passHref/dynamic') }) it('navigates correctly', async () => { await expectLinkClickToNavigate('/passHref/dynamic') }) }) }) })` - ID 270: `test/e2e/next-link-errors/next-link-errors.test.ts` β€” `describe('next-link', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) it('errors on invalid href', async () => { const browser = await next.browser('/invalid-href') if (isNextDev) { await expect(browser).toDisplayRedbox(` { "description": "Failed prop type: The prop \`href\` expects a \`string\` or \`object\` in \`<Link>\`, but got \`undefined\` instead. Open your browser's console to view the Component stack trace.", "environmentLabel": null, "label": "Runtime Error", "source": "app/invalid-href/page.js (6:10) @ Hello > 6 | return <Link>Hello, Dave!</Link> | ^", "stack": [ "Hello app/invalid-href/page.js (6:10)", ], } `) } // Client errors show "This page couldn\u2019t load" expect(await browser.elementByCss('body').text()).toContain( 'This page couldn\u2019t load' ) }) it('invalid `prefetch` causes runtime error (dev-only)', async () => { const browser = await next.browser('/invalid-prefetch') if (isNextDev) { await expect(browser).toDisplayRedbox(` { "description": "Failed prop type: The prop \`prefetch\` expects a \`boolean | "auto"\` in \`<Link>\`, but got \`string\` instead. Open your browser's console to view the Component stack trace.", "environmentLabel": null, "label": "Runtime Error", "source": "app/invalid-prefetch/page.js (7:5) @ Hello > 7 | <Link prefetch="unknown" href="https://nextjs.org/"> | ^", "stack": [ "Hello app/invalid-prefetch/page.js (7:5)", ], } `) // Client errors show "This page couldn\u2019t load" expect(await browser.elementByCss('body').text()).toContain( 'This page couldn\u2019t load' ) } else { expect(await browser.elementByCss('body').text()).toMatchInlineSnapshot( `"Link with unknown \`prefetch\` renders in prod."` ) } }) })` - ID 273: `test/e2e/pages-performance-mark/index.test.ts` β€” `describe('pages performance mark', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should render the page correctly without crashing with performance mark', async () => { const browser = await next.browser('/') expect(await browser.elementByCss('h1').text()).toBe('home') }) })` </details> <details> <summary>Deployment evidence for the additional scopes</summary> - `test/e2e/404-page-custom-error/404-page-custom-error.test.ts` β€” `describe('Default 404 Page with custom _error', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426785205/job/102715905641), [cache](https://github.com/vercel/next.js/actions/runs/34426785205/job/102715905524). - `test/e2e/404-page/404-page.test.ts` β€” `describe('404 Page Support', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426785205/job/102715905631), [cache](https://github.com/vercel/next.js/actions/runs/34426785205/job/102715905599). - `test/e2e/i18n-ignore-rewrite-source-locale/rewrites-with-basepath.test.ts` β€” `describe('i18n-ignore-rewrite-source-locale with basepath', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426785205/job/102715905696), [cache](https://github.com/vercel/next.js/actions/runs/34426785205/job/102715905562). </details> <!-- NEXT_JS_LLM --> --- .../404-page-custom-error/404-page-custom-error.test.ts | 1 - test/e2e/404-page/404-page.test.ts | 1 - test/e2e/500-page/500-page.test.ts | 4 +--- .../api-resolver-query-writeable.test.ts | 7 +------ test/e2e/app-document/client.test.ts | 1 - test/e2e/disable-js/disable-js.test.ts | 4 +--- test/e2e/gip-identifier/gip-identifier.test.ts | 4 +--- .../redirect-from-context.test.ts | 6 ------ .../rewrites-with-basepath.test.ts | 7 +------ test/e2e/legacy-link-behavior/index.test.ts | 7 +------ test/e2e/next-link-errors/next-link-errors.test.ts | 5 +---- test/e2e/pages-performance-mark/index.test.ts | 7 +------ 12 files changed, 8 insertions(+), 46 deletions(-) diff --git a/test/e2e/404-page-custom-error/404-page-custom-error.test.ts b/test/e2e/404-page-custom-error/404-page-custom-error.test.ts index fc7f83bdd2a0..2854e3bf52d2 100644 --- a/test/e2e/404-page-custom-error/404-page-custom-error.test.ts +++ b/test/e2e/404-page-custom-error/404-page-custom-error.test.ts @@ -10,7 +10,6 @@ const shouldSkip = () => { const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) it('should respond to 404 correctly', async () => { diff --git a/test/e2e/404-page/404-page.test.ts b/test/e2e/404-page/404-page.test.ts index 90a3355d5bd8..aa1e2a640ef9 100644 --- a/test/e2e/404-page/404-page.test.ts +++ b/test/e2e/404-page/404-page.test.ts @@ -4,7 +4,6 @@ import { retry } from 'next-test-utils' describe('404 Page Support', () => { const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) const gip404Err = diff --git a/test/e2e/500-page/500-page.test.ts b/test/e2e/500-page/500-page.test.ts index 92e00dcce530..3bd01124f3fc 100644 --- a/test/e2e/500-page/500-page.test.ts +++ b/test/e2e/500-page/500-page.test.ts @@ -2,11 +2,9 @@ import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' import { retry } from 'next-test-utils' describe('500 Page Support', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return it('should use pages/500', async () => { const html = await next.render('/500') diff --git a/test/e2e/api-resolver-query-writeable/api-resolver-query-writeable.test.ts b/test/e2e/api-resolver-query-writeable/api-resolver-query-writeable.test.ts index 19a56e9db80b..8b928effa2a0 100644 --- a/test/e2e/api-resolver-query-writeable/api-resolver-query-writeable.test.ts +++ b/test/e2e/api-resolver-query-writeable/api-resolver-query-writeable.test.ts @@ -1,9 +1,8 @@ import { nextTestSetup } from 'e2e-utils' describe('api-resolver-query-writeable', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, startCommand: 'node server.js', serverReadyPattern: /Next mode: (production|development)/, dependencies: { @@ -12,10 +11,6 @@ describe('api-resolver-query-writeable', () => { }, }) - if (skipped) { - return - } - it('should allow req.query to be writable and reflect changes made in the API handler', async () => { const res = await next.fetch('/api?hello=yes', { headers: { diff --git a/test/e2e/app-document/client.test.ts b/test/e2e/app-document/client.test.ts index 5b68d879f413..49d6096bca93 100644 --- a/test/e2e/app-document/client.test.ts +++ b/test/e2e/app-document/client.test.ts @@ -4,7 +4,6 @@ import { nextTestSetup } from 'e2e-utils' describe('Document and App - Client side', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) it('should share module state with pages', async () => { diff --git a/test/e2e/disable-js/disable-js.test.ts b/test/e2e/disable-js/disable-js.test.ts index 21eb2905f493..925a715c309f 100644 --- a/test/e2e/disable-js/disable-js.test.ts +++ b/test/e2e/disable-js/disable-js.test.ts @@ -2,11 +2,9 @@ import { nextTestSetup } from 'e2e-utils' import cheerio from 'cheerio' describe('disabled runtime JS', () => { - const { next, isNextDev, isNextStart, skipped } = nextTestSetup({ + const { next, isNextDev, isNextStart } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return it('should render the page', async () => { const html = await next.render('/') diff --git a/test/e2e/gip-identifier/gip-identifier.test.ts b/test/e2e/gip-identifier/gip-identifier.test.ts index 2abc71ec9081..061c3cce6e6d 100644 --- a/test/e2e/gip-identifier/gip-identifier.test.ts +++ b/test/e2e/gip-identifier/gip-identifier.test.ts @@ -3,11 +3,9 @@ import { retry } from 'next-test-utils' import cheerio from 'cheerio' describe('gip identifiers', () => { - const { next, isNextDev, skipped } = nextTestSetup({ + const { next, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return const getNextData = async () => { const html = await next.render('/') diff --git a/test/e2e/i18n-data-fetching-redirect/redirect-from-context.test.ts b/test/e2e/i18n-data-fetching-redirect/redirect-from-context.test.ts index 012c662f82fa..8e6bb0b456c9 100644 --- a/test/e2e/i18n-data-fetching-redirect/redirect-from-context.test.ts +++ b/test/e2e/i18n-data-fetching-redirect/redirect-from-context.test.ts @@ -3,12 +3,6 @@ import { FileRef, nextTestSetup } from 'e2e-utils' import { check } from 'next-test-utils' describe('i18n-data-fetching-redirect', () => { - // TODO: investigate tests failures on deploy - if ((global as any).isNextDeploy) { - it('should skip temporarily', () => {}) - return - } - const { next } = nextTestSetup({ files: { pages: new FileRef(join(__dirname, 'app/pages')), diff --git a/test/e2e/i18n-ignore-rewrite-source-locale/rewrites-with-basepath.test.ts b/test/e2e/i18n-ignore-rewrite-source-locale/rewrites-with-basepath.test.ts index ce28cb548de0..cb9934ce6224 100644 --- a/test/e2e/i18n-ignore-rewrite-source-locale/rewrites-with-basepath.test.ts +++ b/test/e2e/i18n-ignore-rewrite-source-locale/rewrites-with-basepath.test.ts @@ -6,15 +6,10 @@ import fs from 'fs-extra' const locales = ['', '/en', '/sv', '/nl'] describe('i18n-ignore-rewrite-source-locale with basepath', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - test.each(locales)( 'get public file by skipping locale in rewrite, locale: %s', async (locale) => { diff --git a/test/e2e/legacy-link-behavior/index.test.ts b/test/e2e/legacy-link-behavior/index.test.ts index c34c5a3543a3..751da05fe77d 100644 --- a/test/e2e/legacy-link-behavior/index.test.ts +++ b/test/e2e/legacy-link-behavior/index.test.ts @@ -6,15 +6,10 @@ import { } from '../../lib/add-redbox-matchers' describe('Link with legacyBehavior', () => { - const { next, isNextDev, skipped } = nextTestSetup({ + const { next, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return it('should skip', () => {}) - } - describe('if the child is an <a> tag', () => { it('forwards the href attribute', async () => { const $ = await next.render$('/') diff --git a/test/e2e/next-link-errors/next-link-errors.test.ts b/test/e2e/next-link-errors/next-link-errors.test.ts index 852842a6ba71..a13b0d7b07e8 100644 --- a/test/e2e/next-link-errors/next-link-errors.test.ts +++ b/test/e2e/next-link-errors/next-link-errors.test.ts @@ -1,13 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('next-link', () => { - const { skipped, next, isNextDev } = nextTestSetup({ + const { next, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return - it('errors on invalid href', async () => { const browser = await next.browser('/invalid-href') diff --git a/test/e2e/pages-performance-mark/index.test.ts b/test/e2e/pages-performance-mark/index.test.ts index 72f7e31dc726..ca591c192cdb 100644 --- a/test/e2e/pages-performance-mark/index.test.ts +++ b/test/e2e/pages-performance-mark/index.test.ts @@ -3,15 +3,10 @@ import { nextTestSetup } from 'e2e-utils' // This test case doesn't indicate rendering duplicate head in _document is valid, // but it's a way to reproduce the performance mark crashing. describe('pages performance mark', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should render the page correctly without crashing with performance mark', async () => { const browser = await next.browser('/') expect(await browser.elementByCss('h1').text()).toBe('home') From 2ad16804ac642ba661230713558711f97e9e5866 Mon Sep 17 00:00:00 2001 From: Jamiboy Mohammad <jamiboym@gmail.com> Date: Tue, 15 Sep 2026 10:29:07 -0700 Subject: [PATCH 13/14] test: enable verified assets deploy tests (#98527) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Enable the same 11 previously selected deployment-test scopes across 11 assets test files, now in a stack rooted on canary. Remove 11 `skipDeployment` options and their obsolete skip guards. Other mode, bundler, middleware, and Cache Components exclusions remain in place. This preserves the selection with passing evidence from the previous deployment runs. No additional candidate scopes are enabled; excluded variants are not counted as deployment coverage. ## Verification - All selected test registration names and assertion bodies match the previous enabled revision, checked by AST comparison. - Verified that the canary diff contains only the inventoried exclusions and their obsolete skip plumbing; other exclusions are preserved. - Formatting and lint passed; 77 gate infrastructure unit tests passed. - Full local bootstrap was blocked by missing package-level dependencies in the temporary worktree. Fresh deployment execution on these rewritten commits remains to be verified in CI. <details> <summary>Preserved scope inventory (11)</summary> - ID 286: `test/e2e/app-dir/app-css-pageextensions/index.test.ts` β€” `describe('app dir - css with pageextensions', () => { const { next } = nextTestSetup({ files: __dirname, dependencies: { '@picocss/pico': '1.5.7', sass: 'latest', }, }) describe('css support with pageextensions', () => { describe('page in app directory with pageextention, css should work', () => { it('should support global css inside layout', async () => { const browser = await next.browser('/css-pageextensions') expect( await browser.eval( `window.getComputedStyle(document.querySelector('h1')).color` ) ).toBe('rgb(255, 0, 0)') }) }) }) })` - ID 292: `test/e2e/app-dir/dynamic-css/index.test.ts` β€” `describe('app dir - dynamic css', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should preload all chunks of dynamic component during SSR', async () => { const $ = await next.render$('/ssr') const cssLinks = $('link[rel="stylesheet"][data-precedence="dynamic"]') expect(cssLinks.attr('href')).toContain('.css') const preloadJsChunks = $('link[rel="preload"]') expect(preloadJsChunks.attr('as')).toBe('script') expect(preloadJsChunks.attr('fetchpriority')).toContain(`low`) }) it('should only apply corresponding css for page loaded that /ssr', async () => { const browser = await next.browser('/ssr') await retry(async () => { expect( await browser.eval( `window.getComputedStyle(document.querySelector('.text')).color` ) ).toBe('rgb(255, 0, 0)') // Default border width, which is not effected by bar.css that is not loaded in /ssr expect( await browser.eval( `window.getComputedStyle(document.querySelector('.text')).borderWidth` ) ).toBe('0px') }) }) it('should only apply corresponding css for page loaded in edge runtime', async () => { const browser = await next.browser('/ssr/edge') await retry(async () => { expect( await browser.eval( `window.getComputedStyle(document.querySelector('.text')).color` ) ).toBe('rgb(255, 0, 0)') // Default border width, which is not effected by bar.css that is not loaded in /ssr expect( await browser.eval( `window.getComputedStyle(document.querySelector('.text')).borderWidth` ) ).toBe('0px') }) }) it('should only apply corresponding css for page loaded that /another', async () => { const browser = await next.browser('/another') await retry(async () => { expect( await browser.eval( `window.getComputedStyle(document.querySelector('.text')).color` ) ).not.toBe('rgb(255, 0, 0)') // Default border width, which is not effected by bar.css that is not loaded in /ssr expect( await browser.eval( `window.getComputedStyle(document.querySelector('.text')).borderWidth` ) ).toBe('1px') }) }) it('should not throw with accessing to ALS in preload css', async () => { const output = next.cliOutput expect(output).not.toContain('was called outside a request scope') }) })` - ID 293: `test/e2e/app-dir/emotion-js/index.test.ts` β€” `describe('app dir - emotion-js', () => { const { next } = nextTestSetup({ files: __dirname, dependencies: { '@emotion/react': 'latest', '@emotion/cache': 'latest', }, }) it('should render emotion-js css with compiler.emotion option correctly', async () => { const browser = await next.browser('/') const el = browser.elementByCss('h1') expect(await el.text()).toBe('Blue') await check( async () => await browser.eval( `window.getComputedStyle(document.querySelector('h1')).color` ), 'rgb(0, 0, 255)' ) const el2 = browser.elementByCss('p') expect(await el2.text()).toBe('Red') await check( async () => await browser.eval( `window.getComputedStyle(document.querySelector('p')).color` ), 'rgb(255, 0, 0)' ) }) })` - ID 294: `test/e2e/app-dir/global-error/with-style-import/index.test.ts` β€” `describe('app dir - global error - with style import', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) it('should render global error with correct styles', async () => { const browser = await next.browser('/') if (isNextDev) { await testDev(browser, /Root Layout Error/) return } const h2 = await browser.elementByCss('h2') expect(await h2.getComputedCss('color')).toBe('rgb(255, 255, 0)') // yellow }) })` - ID 301: `test/e2e/app-dir/not-found/css-precedence/index.test.ts` β€” `describe('not-found app dir css', () => { const { next } = nextTestSetup({ files: __dirname, dependencies: { sass: 'latest', }, }) it('should load css while navigation between not-found and page', async () => { const browser = await next.browser('/') await check( async () => await browser.eval( `window.getComputedStyle(document.querySelector('#go-to-404')).backgroundColor` ), 'rgb(0, 128, 0)' ) await browser.elementByCss('#go-to-404').click() await browser.waitForElementByCss('#go-to-index') await check( async () => await browser.eval( `window.getComputedStyle(document.querySelector('#go-to-index')).backgroundColor` ), 'rgb(0, 128, 0)' ) await browser.elementByCss('#go-to-index').click() await browser.waitForElementByCss('#go-to-404') await check( async () => await browser.eval( `window.getComputedStyle(document.querySelector('#go-to-404')).backgroundColor` ), 'rgb(0, 128, 0)' ) }) })` - ID 308: `test/e2e/app-dir/turbopack-postcss-multiple-configs/turbopack-postcss-multiple-configs.test.ts` β€” `describe('turbopack-postcss-multiple-configs', () => { const { next, isTurbopack } = nextTestSetup({ files: __dirname, // Per-directory PostCSS config resolution is a Turbopack-only feature // (turbopackLocalPostcssConfig). Webpack does not support this feature and // does not accept function-valued PostCSS plugins, so skip non-Turbopack runs. skipStart: true, }) if (!isTurbopack) { it('should only run with Turbopack', () => {}) return } beforeAll(async () => { await next.start() }) // Each directory's postcss.config.js passes a unique color option to the // shared plugin, which replaces `color: red` with the given color. // In production mode the CSS minifier may shorten named colors to hex // (e.g. blue β†’ #00f), so we match on patterns that cover both forms. const DIR_COLORS: Record<number, string | RegExp> = { 1: /blue|#00f/, 2: /purple|#800080/, 3: /orange|#ffa500/, 4: /cyan|#0ff/, 5: /magenta|#f0f/, } const DIRS = 5 const FILES_PER_DIR = 3 it('should render all elements with CSS module classes applied', async () => { const $ = await next.render$('/') for (let dir = 1; dir <= DIRS; dir++) { for (let file = 1; file <= FILES_PER_DIR; file++) { const padded = String(file).padStart(2, '0') const id = `dir${dir}-file${padded}` const el = $(`#${id}`) expect(el.length).toBe(1) expect(el.text().trim()).toBe(`dir${dir} file${padded}`) expect(el.attr('class')).toBeTruthy() } } }) it('should apply per-directory PostCSS transforms with distinct colors', async () => { const cssContent = await collectCss(next) // Each directory's PostCSS config passes a unique color option. // Verify every expected color appears in the output. for (const [, pattern] of Object.entries(DIR_COLORS)) { expect(cssContent).toMatch(pattern) } // No original `color: red` should remain β€” all were transformed. expect(cssContent).not.toMatch(/color\s*:\s*red/) // The old hardcoded green should NOT appear, proving options are used. expect(cssContent).not.toMatch(/green|#0f0|#008000/) }) })` - ID 317: `test/e2e/image-optimizer/image-optimizer.test.ts` β€” `describe('Server support for trailingSlash in next.config.js', () => { const { next } = nextTestSetup({ files: join(__dirname, 'app'), nextConfig: { trailingSlash: true, images: { imageSizes: [8, 16, 32, 48, 64, 96, 128, 256, 384], qualities: [70, 75], }, }, }) it('should return successful response for original loader', async () => { const query = { url: '/test.png', w: 8, q: 70 } const res = await next.fetch(`/_next/image/?${toQueryString(query)}`) expect(res.status).toBe(200) }) })` - ID 329: `test/e2e/next-image-legacy/default/default-static.test.ts` β€” `describe('Static Image Component Tests', () => { const { next, isTurbopack } = nextTestSetup({ files: __dirname, }) let browser: Playwright let html: string beforeAll(async () => { html = await next.render('/static-img') browser = await next.browser('/static-img') }) it('Should allow an image with a static src to omit height and width', async () => { expect(await browser.elementById('basic-static')).toBeTruthy() expect(await browser.elementById('blur-png')).toBeTruthy() expect(await browser.elementById('blur-webp')).toBeTruthy() expect(await browser.elementById('blur-avif')).toBeTruthy() expect(await browser.elementById('blur-jpg')).toBeTruthy() expect(await browser.elementById('static-svg')).toBeTruthy() expect(await browser.elementById('static-gif')).toBeTruthy() expect(await browser.elementById('static-bmp')).toBeTruthy() expect(await browser.elementById('static-ico')).toBeTruthy() expect(await browser.elementById('static-unoptimized')).toBeTruthy() }) ;(isNextStart ? it : it.skip)( 'Should use immutable cache-control header for static import', async () => { await browser.eval( `document.getElementById("basic-static").scrollIntoView()` ) await new Promise((resolve) => setTimeout(resolve, 1000)) const url = await browser.eval( `document.getElementById("basic-static").src` ) const res = await fetch(url) expect(res.headers.get('cache-control')).toBe( 'public, max-age=315360000, immutable' ) } ) ;(isNextStart ? it : it.skip)( 'Should use immutable cache-control header even when unoptimized', async () => { await browser.eval( `document.getElementById("static-unoptimized").scrollIntoView()` ) await new Promise((resolve) => setTimeout(resolve, 1000)) const url = await browser.eval( `document.getElementById("static-unoptimized").src` ) const res = await fetch(url) expect(res.headers.get('cache-control')).toBe( 'public, max-age=31536000, immutable' ) } ) it('Should automatically provide an image height and width', async () => { expect(html).toContain('width:400px;height:300px') }) it('Should allow provided width and height to override intrinsic', async () => { expect(html).toContain('width:200px;height:200px') expect(html).not.toContain('width:400px;height:400px') }) it('Should add a blur placeholder to statically imported jpg', async () => { const $ = cheerio.load(html) const style = $('#basic-static').attr('style') if (isNextDev && !isTurbopack) { // In webpack dev, `next/legacy/image` emits a dynamic blur URL via the // image optimizer route instead of an inlined base64 data URL, to avoid // slowing down the dev server (see // `packages/next/src/build/webpack/loaders/next-image-loader/blur.ts`). expect(replaceBlurUrl(style)).toMatchInlineSnapshot( `"position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("<REPLACED_BLUR_URL>")"` ) } else { expect(replaceDataUrl(style)).toMatchInlineSnapshot( `"position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("data:<REPLACED>")"` ) } }) it('Should add a blur placeholder to statically imported png', async () => { const $ = cheerio.load(html) const style = $('#basic-static')[2].attribs.style if (isTurbopack) { expect(style).toMatchInlineSnapshot( `"position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAICAYAAAA870V8AAAARUlEQVR42l3MoQ0AQQhE0XG7xWwIJSBIKBRJOZRBEXOWnPjimQ8AXC3ce+nuPOcQEcHuppkRVcWZYWYSIkJV5XvvN9j4AFZHJTnjDHb/AAAAAElFTkSuQmCC")"` ) } else if (isNextDev) { // In webpack dev, `next/legacy/image` emits a dynamic blur URL via the // image optimizer route instead of an inlined base64 data URL, to avoid // slowing down the dev server (see // `packages/next/src/build/webpack/loaders/next-image-loader/blur.ts`). expect(replaceBlurUrl(style)).toMatchInlineSnapshot( `"position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("<REPLACED_BLUR_URL>")"` ) } else { // In webpack start, the exact base64 output of the blur placeholder // depends on the environment's sharp/libvips version, so normalize the // data URL contents to only assert the data URL prefix. expect(replaceDataUrl(style)).toMatchInlineSnapshot( `"position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("data:<REPLACED>")"` ) } }) it('should load direct imported image', async () => { const src = await browser.elementById('basic-static').getAttribute('src') expect(src).toMatch( /_next\/image\?url=%2F_next%2Fstatic%2F(immutable%2F)?media%2Ftest-rect(.+)\.jpg&w=828&q=75/ ) const fullSrc = new URL(src, next.url) const res = await fetch(fullSrc) expect(res.status).toBe(200) }) it('should load staticprops imported image', async () => { const src = await browser .elementById('basic-staticprop') .getAttribute('src') expect(src).toMatch( /_next\/image\?url=%2F_next%2Fstatic%2F(immutable%2F)?media%2Fexif-rotation(.+)\.jpg&w=256&q=75/ ) const fullSrc = new URL(src, next.url) const res = await fetch(fullSrc) expect(res.status).toBe(200) }) })` - ID 345: `test/e2e/next-image-svgo-webpack/svgo-webpack.test.ts` β€” `describe('svgo-webpack loader', () => { const { next } = nextTestSetup({ files: __dirname, dependencies: { '@svgr/webpack': '8.1.0', }, }) it('should render an SVG that is transformed by @svgr/webpack into a React component (pages router)', async () => { const browser = await next.browser('/pages') expect(await browser.elementByCss('svg')).toBeDefined() }) it('should render an SVG that is transformed by @svgr/webpack into a React component (app router)', async () => { const browser = await next.browser('/') expect(await browser.elementByCss('svg')).toBeDefined() }) })` - ID 347: `test/e2e/styled-jsx-dynamic/index.test.ts` β€” `describe('styled-jsx dynamic styles SSR', () => { const { next } = nextTestSetup({ files: __dirname, }) // Dynamic styled-jsx (with interpolated expressions) produces numeric class // names at runtime via the DJB2 hash in styled-jsx's computeId function. // This pattern matches production deployments where all jsx class names // are numeric (e.g. jsx-2267428885) rather than hex (jsx-f36313d9f07883b7). it('should contain dynamic styled-jsx styles during SSR', async () => { const html = await next.render('/') // Dynamic styled-jsx produces numeric class names at runtime const numericClasses = html.match(/\bjsx-\d+\b/g) || [] console.log('Numeric jsx classes:', [...new Set(numericClasses)]) expect(numericClasses.length).toBeGreaterThan(0) // All dynamic styles should be present as inline <style> tags expect(html).toMatch(/color:.*?green/) // main page expect(html).toMatch(/color:.*?blue/) // DynamicStyled expect(html).toMatch(/background-color:.*?navy/) // header expect(html).toMatch(/color:.*?purple/) // footer }) })` - ID 348: `test/e2e/styled-jsx/index.test.ts` β€” `describe('styled-jsx', () => { const { next } = nextTestSetup({ files: __dirname, dependencies: { 'styled-jsx': '5.0.0', // styled-jsx on user side }, }) it('should contain styled-jsx styles during SSR', async () => { const html = await next.render('/') expect(html).toMatch(/color:.*?red/) expect(html).toMatch(/color:.*?cyan/) }) it('should render styles during CSR', async () => { const browser = await next.browser('/') const color = await browser.eval( `getComputedStyle(document.querySelector('button')).color` ) expect(color).toMatch('0, 255, 255') }) it('should render styles inside TypeScript', async () => { const browser = await next.browser('/typescript') const color = await browser.eval( `getComputedStyle(document.querySelector('button')).color` ) expect(color).toMatch('255, 0, 0') }) })` </details> <details> <summary>Deployment evidence for the additional scopes</summary> - `test/e2e/image-optimizer/image-optimizer.test.ts` β€” `describe('Server support for trailingSlash in next.config.js', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426784928/job/102715630953), [cache](https://github.com/vercel/next.js/actions/runs/34426784928/job/102715630856). - `test/e2e/next-image-legacy/default/default-static.test.ts` β€” `describe('Static Image Component Tests', () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426784928/job/102715630923), [cache](https://github.com/vercel/next.js/actions/runs/34426784928/job/102715630827). </details> <!-- NEXT_JS_LLM --> --- test/e2e/app-dir/app-css-pageextensions/index.test.ts | 7 +------ test/e2e/app-dir/dynamic-css/index.test.ts | 7 +------ test/e2e/app-dir/emotion-js/index.test.ts | 7 +------ .../app-dir/global-error/with-style-import/index.test.ts | 7 +------ test/e2e/app-dir/not-found/css-precedence/index.test.ts | 7 +------ .../turbopack-postcss-multiple-configs.test.ts | 5 +---- test/e2e/image-optimizer/image-optimizer.test.ts | 4 +--- test/e2e/next-image-legacy/default/default-static.test.ts | 4 +--- test/e2e/next-image-svgo-webpack/svgo-webpack.test.ts | 1 - test/e2e/styled-jsx-dynamic/index.test.ts | 1 - test/e2e/styled-jsx/index.test.ts | 7 +------ 11 files changed, 9 insertions(+), 48 deletions(-) diff --git a/test/e2e/app-dir/app-css-pageextensions/index.test.ts b/test/e2e/app-dir/app-css-pageextensions/index.test.ts index 50e3a9ca3f37..4b61d2728462 100644 --- a/test/e2e/app-dir/app-css-pageextensions/index.test.ts +++ b/test/e2e/app-dir/app-css-pageextensions/index.test.ts @@ -1,19 +1,14 @@ import { nextTestSetup } from 'e2e-utils' describe('app dir - css with pageextensions', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, dependencies: { '@picocss/pico': '1.5.7', sass: 'latest', }, }) - if (skipped) { - return - } - describe('css support with pageextensions', () => { describe('page in app directory with pageextention, css should work', () => { it('should support global css inside layout', async () => { diff --git a/test/e2e/app-dir/dynamic-css/index.test.ts b/test/e2e/app-dir/dynamic-css/index.test.ts index f7bf90245a74..7ba4a530d137 100644 --- a/test/e2e/app-dir/dynamic-css/index.test.ts +++ b/test/e2e/app-dir/dynamic-css/index.test.ts @@ -2,15 +2,10 @@ import { nextTestSetup } from 'e2e-utils' import { retry } from 'next-test-utils' describe('app dir - dynamic css', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should preload all chunks of dynamic component during SSR', async () => { const $ = await next.render$('/ssr') const cssLinks = $('link[rel="stylesheet"][data-precedence="dynamic"]') diff --git a/test/e2e/app-dir/emotion-js/index.test.ts b/test/e2e/app-dir/emotion-js/index.test.ts index 6a710435ba5f..57a4b57db369 100644 --- a/test/e2e/app-dir/emotion-js/index.test.ts +++ b/test/e2e/app-dir/emotion-js/index.test.ts @@ -2,19 +2,14 @@ import { nextTestSetup } from 'e2e-utils' import { check } from 'next-test-utils' describe('app dir - emotion-js', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, dependencies: { '@emotion/react': 'latest', '@emotion/cache': 'latest', }, }) - if (skipped) { - return - } - it('should render emotion-js css with compiler.emotion option correctly', async () => { const browser = await next.browser('/') const el = browser.elementByCss('h1') diff --git a/test/e2e/app-dir/global-error/with-style-import/index.test.ts b/test/e2e/app-dir/global-error/with-style-import/index.test.ts index 7e0330af0dfc..50f4f0c823c3 100644 --- a/test/e2e/app-dir/global-error/with-style-import/index.test.ts +++ b/test/e2e/app-dir/global-error/with-style-import/index.test.ts @@ -7,15 +7,10 @@ async function testDev(browser, errorRegex) { } describe('app dir - global error - with style import', () => { - const { next, isNextDev, skipped } = nextTestSetup({ + const { next, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should render global error with correct styles', async () => { const browser = await next.browser('/') diff --git a/test/e2e/app-dir/not-found/css-precedence/index.test.ts b/test/e2e/app-dir/not-found/css-precedence/index.test.ts index 2fa7096021c7..1c9982a12510 100644 --- a/test/e2e/app-dir/not-found/css-precedence/index.test.ts +++ b/test/e2e/app-dir/not-found/css-precedence/index.test.ts @@ -2,18 +2,13 @@ import { nextTestSetup } from 'e2e-utils' import { check } from 'next-test-utils' describe('not-found app dir css', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, dependencies: { sass: 'latest', }, }) - if (skipped) { - return - } - it('should load css while navigation between not-found and page', async () => { const browser = await next.browser('/') await check( diff --git a/test/e2e/app-dir/turbopack-postcss-multiple-configs/turbopack-postcss-multiple-configs.test.ts b/test/e2e/app-dir/turbopack-postcss-multiple-configs/turbopack-postcss-multiple-configs.test.ts index 758a496d714c..0b938649011f 100644 --- a/test/e2e/app-dir/turbopack-postcss-multiple-configs/turbopack-postcss-multiple-configs.test.ts +++ b/test/e2e/app-dir/turbopack-postcss-multiple-configs/turbopack-postcss-multiple-configs.test.ts @@ -1,17 +1,14 @@ import { nextTestSetup } from 'e2e-utils' describe('turbopack-postcss-multiple-configs', () => { - const { next, isTurbopack, skipped } = nextTestSetup({ + const { next, isTurbopack } = nextTestSetup({ files: __dirname, // Per-directory PostCSS config resolution is a Turbopack-only feature // (turbopackLocalPostcssConfig). Webpack does not support this feature and // does not accept function-valued PostCSS plugins, so skip non-Turbopack runs. skipStart: true, - skipDeployment: true, }) - if (skipped) return - if (!isTurbopack) { it('should only run with Turbopack', () => {}) return diff --git a/test/e2e/image-optimizer/image-optimizer.test.ts b/test/e2e/image-optimizer/image-optimizer.test.ts index cda4b5eb996f..f7436ca971e4 100644 --- a/test/e2e/image-optimizer/image-optimizer.test.ts +++ b/test/e2e/image-optimizer/image-optimizer.test.ts @@ -263,7 +263,7 @@ describe('Image Optimizer', () => { } }) describe('Server support for trailingSlash in next.config.js', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: join(__dirname, 'app'), nextConfig: { trailingSlash: true, @@ -272,9 +272,7 @@ describe('Image Optimizer', () => { qualities: [70, 75], }, }, - skipDeployment: true, }) - if (skipped) return it('should return successful response for original loader', async () => { const query = { url: '/test.png', w: 8, q: 70 } diff --git a/test/e2e/next-image-legacy/default/default-static.test.ts b/test/e2e/next-image-legacy/default/default-static.test.ts index d2e0d6b520ec..a088d6190aa2 100644 --- a/test/e2e/next-image-legacy/default/default-static.test.ts +++ b/test/e2e/next-image-legacy/default/default-static.test.ts @@ -44,11 +44,9 @@ describe('Build Error Tests', () => { }) describe('Static Image Component Tests', () => { - const { next, isTurbopack, skipped } = nextTestSetup({ + const { next, isTurbopack } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return let browser: Playwright let html: string diff --git a/test/e2e/next-image-svgo-webpack/svgo-webpack.test.ts b/test/e2e/next-image-svgo-webpack/svgo-webpack.test.ts index 4b03f9e7651c..63182ce879f2 100644 --- a/test/e2e/next-image-svgo-webpack/svgo-webpack.test.ts +++ b/test/e2e/next-image-svgo-webpack/svgo-webpack.test.ts @@ -3,7 +3,6 @@ import { nextTestSetup } from 'e2e-utils' describe('svgo-webpack loader', () => { const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, dependencies: { '@svgr/webpack': '8.1.0', }, diff --git a/test/e2e/styled-jsx-dynamic/index.test.ts b/test/e2e/styled-jsx-dynamic/index.test.ts index adbd235b296a..2b03d6b897de 100644 --- a/test/e2e/styled-jsx-dynamic/index.test.ts +++ b/test/e2e/styled-jsx-dynamic/index.test.ts @@ -3,7 +3,6 @@ import { nextTestSetup } from 'e2e-utils' describe('styled-jsx dynamic styles SSR', () => { const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) // Dynamic styled-jsx (with interpolated expressions) produces numeric class diff --git a/test/e2e/styled-jsx/index.test.ts b/test/e2e/styled-jsx/index.test.ts index 03a37f410767..76388ca1d0fa 100644 --- a/test/e2e/styled-jsx/index.test.ts +++ b/test/e2e/styled-jsx/index.test.ts @@ -1,18 +1,13 @@ import { nextTestSetup } from 'e2e-utils' describe('styled-jsx', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, dependencies: { 'styled-jsx': '5.0.0', // styled-jsx on user side }, }) - if (skipped) { - return - } - it('should contain styled-jsx styles during SSR', async () => { const html = await next.render('/') expect(html).toMatch(/color:.*?red/) From 3dbf9219056f25666508b6ac342a9c1122d4591a Mon Sep 17 00:00:00 2001 From: Jamiboy Mohammad <jamiboym@gmail.com> Date: Tue, 15 Sep 2026 10:29:07 -0700 Subject: [PATCH 14/14] test: enable verified runtime deploy tests (#98457) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Enable the same 9 previously selected deployment-test scopes across 9 runtime test files, now in a stack rooted on canary. Remove 8 `skipDeployment` options and their obsolete skip guards. Enable only the selected middleware cookie assertion; the neighboring query assertion remains excluded. Other mode, bundler, middleware, and Cache Components exclusions remain in place. This preserves the selection with passing evidence from the previous deployment runs. No additional candidate scopes are enabled; excluded variants are not counted as deployment coverage. The worker-react-refresh scope (ID 411) remains deployment-excluded pending fixture dependency changes: React 19.3.0 conflicts with @react-three/fiber 9.7.0’s React <19.3 peer range. Both deploy variants fail during npm installation, before the assertion executes. It is tracked as a fixable candidate, not a permanent local-only test. ## Verification - All selected test registration names and assertion bodies match the previous enabled revision, checked by AST comparison. - Verified that the canary diff contains only the inventoried exclusions and their obsolete skip plumbing; other exclusions are preserved. - Formatting and lint passed; 77 gate infrastructure unit tests passed. - Full local bootstrap was blocked by missing package-level dependencies in the temporary worktree. Fresh deployment execution on these rewritten commits remains to be verified in CI. <details> <summary>Preserved scope inventory (9)</summary> - ID 350: `test/e2e/app-dir/app-edge-root-layout/index.test.ts` β€” `describe('app-dir edge runtime root layout', () => { const { next, isNextStart } = nextTestSetup({ files: __dirname, }) it('should not emit metadata files into bad paths', async () => { await next.fetch('/favicon.ico') // issue: If metadata files are not filter out properly with image-loader, // an incorrect static/media folder will be generated // Check that the static folder is not generated const incorrectGeneratedStaticFolder = await next.hasFile('static') expect(incorrectGeneratedStaticFolder).toBe(false) }) if (isNextStart) { it('should mark static contain metadata routes as edge functions', async () => { const middlewareManifest = await next.readFile( '.next/server/middleware-manifest.json' ) expect(middlewareManifest).not.toContain('/favicon') }) } })` - ID 353: `test/e2e/app-dir/binary/rsc-binary.test.ts` β€” `describe('RSC binary serialization', () => { const { next } = nextTestSetup({ files: __dirname, dependencies: { 'server-only': 'latest', }, }) afterEach(async () => { await next.stop() }) it('should correctly encode/decode binaries and hydrate', async function () { const browser = await next.browser('/') await check(async () => { const content = await browser.elementByCss('body').text() return content.includes('utf8 binary: hello') && content.includes('arbitrary binary: 255,0,1,2,3') && content.includes('hydrated: true') ? 'success' : 'fail' }, 'success') }) })` - ID 355: `test/e2e/app-dir/interception-middleware-rewrite/interception-middleware-rewrite.test.ts` β€” `describe('interception-middleware-rewrite', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should support intercepting routes with a middleware rewrite', async () => { const browser = await next.browser('/') await check(() => browser.elementByCss('#children').text(), 'root') await check( () => browser .elementByCss('[href="/feed"]') .click() .elementByCss('#modal') .text(), 'intercepted' ) await check( () => browser.refresh().elementByCss('#children').text(), 'not intercepted' ) await check(() => browser.elementByCss('#modal').text(), 'default') }) it('should continue to work after using browser back button and following another intercepting route', async () => { const browser = await next.browser('/') await check(() => browser.elementById('children').text(), 'root') await browser.elementByCss('[href="/photos/1"]').click() await check( () => browser.elementById('modal').text(), 'Intercepted Photo ID: 1' ) await browser.back() await browser.elementByCss('[href="/photos/2"]').click() await check( () => browser.elementById('modal').text(), 'Intercepted Photo ID: 2' ) }) it('should continue to show the intercepted page when revisiting it', async () => { const browser = await next.browser('/') await check(() => browser.elementById('children').text(), 'root') await browser.elementByCss('[href="/photos/1"]').click() // we should be showing the modal and not the page await check( () => browser.elementById('modal').text(), 'Intercepted Photo ID: 1' ) await browser.refresh() // page should show after reloading the browser await check( () => browser.elementById('children').text(), 'Page Photo ID: 1' ) // modal should no longer be showing await check(() => browser.elementById('modal').text(), 'default') await browser.back() // revisit the same page that was intercepted await browser.elementByCss('[href="/photos/1"]').click() // ensure that we're still showing the modal and not the page await check( () => browser.elementById('modal').text(), 'Intercepted Photo ID: 1' ) // page content should not have changed await check(() => browser.elementById('children').text(), 'root') }) })` - ID 360: `test/e2e/app-dir/middleware-matching/index.test.ts` β€” `describe('app dir - middleware with custom matcher', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should match /:id (without asterisk)', async () => { const browser = await next.browser('/chat/123') expect(await browser.elementByCss('p').text()).toBe('Home') }) })` - ID 361: `test/e2e/app-dir/node-extensions/node-extensions.random.test.ts` β€” `describe('Cache Components', () => { const { next } = nextTestSetup({ files: __dirname + '/fixtures/random/cache-components', }) it('should not error when accessing middlware that use Math.random()', async () => { let res: Awaited<ReturnType<typeof next.fetch>>, $: Awaited<ReturnType<typeof next.render$>> res = await next.fetch('/rewrite') expect(res.status).toBe(200) $ = await next.render$('/rewrite') expect($('[data-testid="content"]').text()).toBe('rewritten') }) it('should not error when accessing pages that use Math.random() in App Router', async () => { let res, $ res = await next.fetch('/app/prerendered/unstable-cache') expect(res.status).toBe(200) $ = await next.render$('/app/prerendered/unstable-cache') expect($('li').length).toBe(2) res = await next.fetch('/app/prerendered/use-cache') expect(res.status).toBe(200) $ = await next.render$('/app/prerendered/use-cache') expect($('li').length).toBe(2) res = await next.fetch('/app/rendered/uncached') expect(res.status).toBe(200) $ = await next.render$('/app/rendered/uncached') expect($('li').length).toBe(2) res = await next.fetch('/app/rendered/unstable-cache') expect(res.status).toBe(200) $ = await next.render$('/app/rendered/unstable-cache') expect($('li').length).toBe(2) res = await next.fetch('/app/rendered/use-cache') expect(res.status).toBe(200) $ = await next.render$('/app/rendered/use-cache') expect($('li').length).toBe(2) }) it('should not error when accessing routes that use Math.random() in App Router', async () => { let res, body res = await next.fetch('/app/prerendered/uncached/api') expect(res.status).toBe(200) body = await res.json() expect(body).toEqual({ rand1: expect.any(Number), rand2: expect.any(Number), }) res = await next.fetch('/app/prerendered/unstable-cache/api') expect(res.status).toBe(200) body = await res.json() expect(body).toEqual({ rand1: expect.any(Number), rand2: expect.any(Number), }) res = await next.fetch('/app/prerendered/use-cache/api') expect(res.status).toBe(200) body = await res.json() expect(body).toEqual({ rand1: expect.any(Number), rand2: expect.any(Number), }) res = await next.fetch('/app/rendered/uncached/api') expect(res.status).toBe(200) body = await res.json() expect(body).toEqual({ rand1: expect.any(Number), rand2: expect.any(Number), }) res = await next.fetch('/app/rendered/unstable-cache/api') expect(res.status).toBe(200) body = await res.json() expect(body).toEqual({ rand1: expect.any(Number), rand2: expect.any(Number), }) res = await next.fetch('/app/rendered/use-cache/api') expect(res.status).toBe(200) body = await res.json() expect(body).toEqual({ rand1: expect.any(Number), rand2: expect.any(Number), }) }) it('should not error when accessing pages that use Math.random() in Pages Router', async () => { let res, $ res = await next.fetch('/pages/gip/random') expect(res.status).toBe(200) $ = await next.render$('/pages/gip/random') expect($('li').length).toBe(2) res = await next.fetch('/pages/gssp/random') expect(res.status).toBe(200) $ = await next.render$('/pages/gssp/random') expect($('li').length).toBe(2) res = await next.fetch('/pages/gsp/random') expect(res.status).toBe(200) $ = await next.render$('/pages/gsp/random') expect($('li').length).toBe(2) }) it('should not error when accessing routes that use Math.random() in Pages Router', async () => { let res, body res = await next.fetch('/api/random') expect(res.status).toBe(200) body = await res.json() expect(body).toEqual({ rand1: expect.any(Number), rand2: expect.any(Number), }) expect(body.rand1).not.toBe(body.rand2) const first1 = body.rand1 const first2 = body.rand2 res = await next.fetch('/api/random') body = await res.json() expect(body.rand1).not.toBe(body.rand2) expect(body.rand1).not.toBe(first1) expect(body.rand2).not.toBe(first2) }) })` - ID 370: `test/e2e/app-dir/webpack-loader-binary/webpack-loader-binary.test.ts` β€” `describe('webpack-loader-ts-transform', () => { const { next } = nextTestSetup({ files: __dirname, }) it('should allow passing binary assets to and from a Webpack loader', async () => { const $ = await next.render$('/') expect($('#text').text()).toBe('Got a buffer of 18 bytes') expect($('#binary').text()).toBe('Got a buffer of 6765 bytes') }) })` - ID 386: `test/e2e/edge-runtime-uses-edge-light-import-specifier-for-packages/edge-runtime-uses-edge-light-import-specifier-for-packages.test.ts` β€” `describe('edge-runtime uses edge-light import specifier for packages', () => { const { next } = nextTestSetup({ files: __dirname, packageJson: { scripts: { build: 'next build', dev: 'next dev', start: 'next start', }, }, installCommand: 'pnpm i', startCommand: (global as any).isNextDev ? 'pnpm dev' : 'pnpm start', buildCommand: 'pnpm build', }) // In case you need to test the response object it('pages/api endpoints import the correct module', async () => { const res = await next.fetch('/api/edge') const html = await res.json() expect(html).toEqual({ // edge-light is only supported in `exports` and `imports` but webpack also adds the top level `edge-light` key incorrectly. edgeLightPackage: process.env.IS_TURBOPACK_TEST ? 'import' : 'edge-light', edgeLightPackageExports: 'edge-light', }) }) it('pages import the correct module', async () => { const $ = await next.render$('/') const text = JSON.parse($('pre#result').text()) expect(text).toEqual({ // edge-light is only supported in `exports` and `imports` but webpack also adds the top level `edge-light` key incorrectly. edgeLightPackage: process.env.IS_TURBOPACK_TEST ? 'import' : 'edge-light', edgeLightPackageExports: 'edge-light', }) }) it('app-dir imports the correct module', async () => { const $ = await next.render$('/app-dir') const text = JSON.parse($('pre#result').text()) expect(text).toEqual({ // edge-light is only supported in `exports` and `imports` but webpack also adds the top level `edge-light` key incorrectly. edgeLightPackage: process.env.IS_TURBOPACK_TEST ? 'import' : 'edge-light', edgeLightPackageExports: 'edge-light', }) }) })` - ID 392: `test/e2e/middleware-custom-matchers/test/index.test.ts` β€” `it('should match has cookie on client routing', async () => { const browser = await next.browser('/routes') await browser.addCookie({ name: 'loggedIn', value: 'true' }) await browser.refresh() await browser.eval('window.__TEST_NO_RELOAD = true') await browser.elementById('has-match-3').click() const fromMiddleware = await browser.elementById('from-middleware').text() expect(fromMiddleware).toBe('true') const noReload = await browser.eval('window.__TEST_NO_RELOAD') expect(noReload).toBe(true) })` - ID 402: `test/e2e/on-request-error/skip-next-internal-error/skip-next-internal-error.test.ts` β€” `describe('on-request-error - skip-next-internal-error', () => { const { next } = nextTestSetup({ files: __dirname, }) async function assertNoNextjsInternalErrors() { const output = next.cliOutput // No navigation errors expect(output).not.toContain('NEXT_REDIRECT') expect(output).not.toContain('NEXT_NOT_FOUND') expect(output).not.toContain('BAILOUT_TO_CLIENT_SIDE_RENDERING') // No dynamic usage errors expect(output).not.toContain('DYNAMIC_SERVER_USAGE') // No react postpone errors // TODO: cover PPR errors later expect(output).not.toContain('react.postpone') } describe('app router render', () => { // Server navigation errors it('should not catch server component not-found errors', async () => { await next.fetch('/server/not-found') await assertNoNextjsInternalErrors() }) it('should not catch server component redirect errors', async () => { await next.render('/server/redirect') await assertNoNextjsInternalErrors() }) // Client navigation errors it('should not catch client component not-found errors', async () => { await next.fetch('/server/not-found') await assertNoNextjsInternalErrors() }) it('should not catch client component redirect errors', async () => { await next.render('/client/redirect') await assertNoNextjsInternalErrors() }) // Dynamic usage it('should not catch server component dynamic usage errors', async () => { await next.fetch('/server/dynamic-fetch') await assertNoNextjsInternalErrors() }) it('should not catch client component dynamic usage errors', async () => { await next.fetch('/client/dynamic-fetch') await assertNoNextjsInternalErrors() }) // No SSR it('should not catch next dynamic no-ssr errors', async () => { await next.fetch('/client/no-ssr') await assertNoNextjsInternalErrors() }) // Server Actions navigation it('should not catch server action not-found errors', async () => { await next.fetch('/form/not-found') await assertNoNextjsInternalErrors() }) it('should not catch server action redirect errors', async () => { await next.fetch('/form/redirect') await assertNoNextjsInternalErrors() }) }) describe('app router API', () => { // API routes navigation errors it('should not catch server component not-found errors', async () => { await next.render('/app-route/not-found') await assertNoNextjsInternalErrors() }) it('should not catch server component redirect errors', async () => { await next.render('/app-route/redirect') await assertNoNextjsInternalErrors() }) }) })` </details> <details> <summary>Deployment evidence for the additional scopes</summary> - `test/e2e/middleware-custom-matchers/test/index.test.ts` β€” `it('should match has cookie on client routing', async () => {`: [normal](https://github.com/vercel/next.js/actions/runs/34426785013/job/102715655249), [cache](https://github.com/vercel/next.js/actions/runs/34426785013/job/102715655202). </details> <!-- NEXT_JS_LLM --> --- .../app-edge-root-layout/index.test.ts | 7 +---- test/e2e/app-dir/binary/rsc-binary.test.ts | 4 +-- .../interception-middleware-rewrite.test.ts | 8 +----- .../app-dir/middleware-matching/index.test.ts | 7 +---- .../node-extensions.random.test.ts | 7 +---- .../webpack-loader-binary.test.ts | 5 +--- ...ight-import-specifier-for-packages.test.ts | 7 +---- .../test/index.test.ts | 27 ++++++++----------- .../skip-next-internal-error.test.ts | 7 +---- 9 files changed, 19 insertions(+), 60 deletions(-) diff --git a/test/e2e/app-dir/app-edge-root-layout/index.test.ts b/test/e2e/app-dir/app-edge-root-layout/index.test.ts index ce0fec9db576..ed7054953ce6 100644 --- a/test/e2e/app-dir/app-edge-root-layout/index.test.ts +++ b/test/e2e/app-dir/app-edge-root-layout/index.test.ts @@ -1,15 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('app-dir edge runtime root layout', () => { - const { next, isNextStart, skipped } = nextTestSetup({ + const { next, isNextStart } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should not emit metadata files into bad paths', async () => { await next.fetch('/favicon.ico') // issue: If metadata files are not filter out properly with image-loader, diff --git a/test/e2e/app-dir/binary/rsc-binary.test.ts b/test/e2e/app-dir/binary/rsc-binary.test.ts index 4ab9f711b2e6..73df285f478f 100644 --- a/test/e2e/app-dir/binary/rsc-binary.test.ts +++ b/test/e2e/app-dir/binary/rsc-binary.test.ts @@ -2,14 +2,12 @@ import { nextTestSetup } from 'e2e-utils' import { check } from 'next-test-utils' describe('RSC binary serialization', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, dependencies: { 'server-only': 'latest', }, }) - if (skipped) return afterEach(async () => { await next.stop() diff --git a/test/e2e/app-dir/interception-middleware-rewrite/interception-middleware-rewrite.test.ts b/test/e2e/app-dir/interception-middleware-rewrite/interception-middleware-rewrite.test.ts index ad0cb55b323e..4ff54bd93d6b 100644 --- a/test/e2e/app-dir/interception-middleware-rewrite/interception-middleware-rewrite.test.ts +++ b/test/e2e/app-dir/interception-middleware-rewrite/interception-middleware-rewrite.test.ts @@ -2,16 +2,10 @@ import { nextTestSetup } from 'e2e-utils' import { check } from 'next-test-utils' describe('interception-middleware-rewrite', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - // TODO: remove after deployment handling is updated - skipDeployment: true, }) - if (skipped) { - return - } - it('should support intercepting routes with a middleware rewrite', async () => { const browser = await next.browser('/') diff --git a/test/e2e/app-dir/middleware-matching/index.test.ts b/test/e2e/app-dir/middleware-matching/index.test.ts index 3fbac3c58113..e1236d99104e 100644 --- a/test/e2e/app-dir/middleware-matching/index.test.ts +++ b/test/e2e/app-dir/middleware-matching/index.test.ts @@ -1,15 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('app dir - middleware with custom matcher', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should match /:id (without asterisk)', async () => { const browser = await next.browser('/chat/123') expect(await browser.elementByCss('p').text()).toBe('Home') diff --git a/test/e2e/app-dir/node-extensions/node-extensions.random.test.ts b/test/e2e/app-dir/node-extensions/node-extensions.random.test.ts index 2da9996f546a..2c342d5079a6 100644 --- a/test/e2e/app-dir/node-extensions/node-extensions.random.test.ts +++ b/test/e2e/app-dir/node-extensions/node-extensions.random.test.ts @@ -3,15 +3,10 @@ import { nextTestSetup } from 'e2e-utils' describe('Node Extensions', () => { describe('Random', () => { describe('Cache Components', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname + '/fixtures/random/cache-components', - skipDeployment: true, }) - if (skipped) { - return - } - it('should not error when accessing middlware that use Math.random()', async () => { let res: Awaited<ReturnType<typeof next.fetch>>, $: Awaited<ReturnType<typeof next.render$>> diff --git a/test/e2e/app-dir/webpack-loader-binary/webpack-loader-binary.test.ts b/test/e2e/app-dir/webpack-loader-binary/webpack-loader-binary.test.ts index e00169d31bf3..380d461e06f0 100644 --- a/test/e2e/app-dir/webpack-loader-binary/webpack-loader-binary.test.ts +++ b/test/e2e/app-dir/webpack-loader-binary/webpack-loader-binary.test.ts @@ -1,13 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('webpack-loader-ts-transform', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return - it('should allow passing binary assets to and from a Webpack loader', async () => { const $ = await next.render$('/') expect($('#text').text()).toBe('Got a buffer of 18 bytes') diff --git a/test/e2e/edge-runtime-uses-edge-light-import-specifier-for-packages/edge-runtime-uses-edge-light-import-specifier-for-packages.test.ts b/test/e2e/edge-runtime-uses-edge-light-import-specifier-for-packages/edge-runtime-uses-edge-light-import-specifier-for-packages.test.ts index 98455dcb8119..a861852cc5d0 100644 --- a/test/e2e/edge-runtime-uses-edge-light-import-specifier-for-packages/edge-runtime-uses-edge-light-import-specifier-for-packages.test.ts +++ b/test/e2e/edge-runtime-uses-edge-light-import-specifier-for-packages/edge-runtime-uses-edge-light-import-specifier-for-packages.test.ts @@ -1,7 +1,7 @@ import { nextTestSetup } from 'e2e-utils' describe('edge-runtime uses edge-light import specifier for packages', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, packageJson: { scripts: { @@ -13,13 +13,8 @@ describe('edge-runtime uses edge-light import specifier for packages', () => { installCommand: 'pnpm i', startCommand: (global as any).isNextDev ? 'pnpm dev' : 'pnpm start', buildCommand: 'pnpm build', - skipDeployment: true, }) - if (skipped) { - return - } - // In case you need to test the response object it('pages/api endpoints import the correct module', async () => { const res = await next.fetch('/api/edge') diff --git a/test/e2e/middleware-custom-matchers/test/index.test.ts b/test/e2e/middleware-custom-matchers/test/index.test.ts index 6ed8cf2a36bf..9733868cca57 100644 --- a/test/e2e/middleware-custom-matchers/test/index.test.ts +++ b/test/e2e/middleware-custom-matchers/test/index.test.ts @@ -159,22 +159,17 @@ describe('Middleware custom matchers', () => { } ) - itif(!isModeDeploy)( - 'should match has cookie on client routing', - async () => { - const browser = await next.browser('/routes') - await browser.addCookie({ name: 'loggedIn', value: 'true' }) - await browser.refresh() - await browser.eval('window.__TEST_NO_RELOAD = true') - await browser.elementById('has-match-3').click() - const fromMiddleware = await browser - .elementById('from-middleware') - .text() - expect(fromMiddleware).toBe('true') - const noReload = await browser.eval('window.__TEST_NO_RELOAD') - expect(noReload).toBe(true) - } - ) + it('should match has cookie on client routing', async () => { + const browser = await next.browser('/routes') + await browser.addCookie({ name: 'loggedIn', value: 'true' }) + await browser.refresh() + await browser.eval('window.__TEST_NO_RELOAD = true') + await browser.elementById('has-match-3').click() + const fromMiddleware = await browser.elementById('from-middleware').text() + expect(fromMiddleware).toBe('true') + const noReload = await browser.eval('window.__TEST_NO_RELOAD') + expect(noReload).toBe(true) + }) } runTests() }) diff --git a/test/e2e/on-request-error/skip-next-internal-error/skip-next-internal-error.test.ts b/test/e2e/on-request-error/skip-next-internal-error/skip-next-internal-error.test.ts index b34f2e4a1c7c..ae27c6a2d414 100644 --- a/test/e2e/on-request-error/skip-next-internal-error/skip-next-internal-error.test.ts +++ b/test/e2e/on-request-error/skip-next-internal-error/skip-next-internal-error.test.ts @@ -1,15 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('on-request-error - skip-next-internal-error', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - async function assertNoNextjsInternalErrors() { const output = next.cliOutput // No navigation errors