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 `
`, ``, 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 ``, ``, and other head tags for your 404 page:
> **Good to know**: Next.js automatically injects `` for pages that return a 404 status code, including `global-not-found.js` pages.
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
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 {
- 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])}/`,
- `^/(?${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 ? '(?[^/]{1,})' : ''}`
+ () =>
+ `^${escapedBasePath}[/]?${shouldLocalize ? '(?[^/]{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('(?:/)?$')),
- '(?\\.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('(?:/)?$')),
- '(?\\.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:
*
* ```
- * (?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/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/packages/next/src/shared/lib/router/router.ts b/packages/next/src/shared/lib/router/router.ts
index 75c19d1b160b..ee411f6dad90 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)) {
@@ -2073,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
}
@@ -2173,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 &&
@@ -2416,7 +2498,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 +2635,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/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/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/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) {
+ return (
+ <>
+
+ )
+}
+
+export default function Root({ children }: { children: ReactNode }) {
+ return (
+
+
+ Loading shared content}>
+
+
+
+
+ {children}
+
+
+ )
+}
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/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-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-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) and should have crossorigin attribute
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/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-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/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-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-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/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/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/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/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-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/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/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/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/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/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/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/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/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/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/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/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>,
$: Awaited>
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/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/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/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/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 */}
+
{title}
+ {/*
+ 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
+ >
+ )
+}
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/parallel-routes-and-interception-from-root/next.config.js b/test/e2e/app-dir/pages-shallow-app-router-marker/next.config.js
similarity index 100%
rename from test/e2e/app-dir/parallel-routes-and-interception-from-root/next.config.js
rename to test/e2e/app-dir/pages-shallow-app-router-marker/next.config.js
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
+
+ >
+ )
+}
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/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/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 (
+