diff --git a/docs/01-app/03-api-reference/03-file-conventions/not-found.mdx b/docs/01-app/03-api-reference/03-file-conventions/not-found.mdx index edcc3692112b..0309f4fd383f 100644 --- a/docs/01-app/03-api-reference/03-file-conventions/not-found.mdx +++ b/docs/01-app/03-api-reference/03-file-conventions/not-found.mdx @@ -182,7 +182,7 @@ If you need to use Client Component hooks like `usePathname` to display content ### Metadata -For `global-not-found.js`, you can export a `metadata` object or a [`generateMetadata`](/docs/app/api-reference/functions/generate-metadata) function to customize the ``, `<meta>`, and other head tags for your 404 page: +Both the root `app/not-found.js` and `app/global-not-found.js` support exporting a `metadata` object or a [`generateMetadata`](/docs/app/api-reference/functions/generate-metadata) function to customize the `<title>`, `<meta>`, and other head tags for your 404 page: > **Good to know**: Next.js automatically injects `<meta name="robots" content="noindex" />` for pages that return a 404 status code, including `global-not-found.js` pages. 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, string> ): string { - let result = destination + const captureKeys = Object.keys(hasCaptures) - // Replace numbered captures from regex ($1, $2, etc.) if (regexMatches) { - // Replace numbered groups (skip index 0 which is the full match) - for (let i = 1; i < regexMatches.length; i++) { - const value = regexMatches[i] ?? '' - result = result.replace(new RegExp(`\\$${i}`, 'g'), value) + for (let index = 1; index < regexMatches.length; index++) { + captureKeys.push(String(index)) } - - // Replace named groups ($name) if (regexMatches.groups) { - for (const [name, value] of Object.entries(regexMatches.groups)) { - result = result.replace(new RegExp(`\\$${name}`, 'g'), value ?? '') - } + captureKeys.push(...Object.keys(regexMatches.groups)) } } - // Replace named captures from has conditions - for (const [name, value] of Object.entries(hasCaptures)) { - result = result.replace(new RegExp(`\\$${name}`, 'g'), value) + if (captureKeys.length === 0) { + return destination } - return result + const capturePattern = captureKeys + .sort((first, second) => second.length - first.length) + .map((key) => { + const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + return /^\d+$/.test(key) ? `${escapedKey}(?!\\d)` : escapedKey + }) + .join('|') + + // Replace placeholders once. Captured paths can contain literal text such as + // $2 or $d$segment that must not become another substitution. + return destination.replace( + new RegExp(`\\$(${capturePattern})`, 'g'), + (placeholder, key: string) => { + if (regexMatches) { + const index = Number(key) + if ( + Number.isInteger(index) && + index > 0 && + index < regexMatches.length && + String(index) === key + ) { + return regexMatches[index] ?? '' + } + if (regexMatches.groups && Object.hasOwn(regexMatches.groups, key)) { + return regexMatches.groups[key] ?? '' + } + } + if (Object.hasOwn(hasCaptures, key)) { + return hasCaptures[key] + } + return placeholder + } + ) } /** diff --git a/packages/next/src/build/adapter/build-complete.ts b/packages/next/src/build/adapter/build-complete.ts index 38925acdd3d6..ec5af1cdd40a 100644 --- a/packages/next/src/build/adapter/build-complete.ts +++ b/packages/next/src/build/adapter/build-complete.ts @@ -2110,6 +2110,10 @@ export async function handleBuildComplete({ (page) => prerenderManifest.dynamicRoutes[page]?.fallback === false ) : undefined + const escapedBasePath = + config.basePath && config.basePath !== '/' + ? escapeStringRegexp(path.posix.join('/', config.basePath)) + : '' for (const route of routesManifest.dynamicRoutes) { // An earlier entry in this loop serves this shell. @@ -2133,6 +2137,10 @@ export async function handleBuildComplete({ // An entry for a whole run of shells matches every prefix in that run. // The destination copies the prefix that matched. // + // The prefix and RSC suffix use unnamed captures. Adapters can forward + // named captures to the application query when they bypass prerendered + // output. + // // This replacement runs on the pattern for the page, and `sourceRegex` // below prefixes the result with the base path and the locale group. That // order is deliberate. The search text anchors at `^`, and here that @@ -2143,18 +2151,24 @@ export async function handleBuildComplete({ const pagePattern = fallbackShellRun ? routeRegex.namedRegex.replace( `^/${escapeStringRegexp(fallbackShellRun.prefixes[0])}/`, - `^/(?<shellPrefix>${fallbackShellRun.prefixes - .map((prefix) => escapeStringRegexp(prefix)) - .join('|')})/` + () => + `^/(${fallbackShellRun.prefixes + .map((prefix) => escapeStringRegexp(prefix)) + .join('|')})/` ) : routeRegex.namedRegex const pagePath = fallbackShellRun - ? path.posix.join('/', '$shellPrefix', fallbackShellRun.tail) + ? path.posix.join( + '/', + shouldLocalize ? '$2' : '$1', + fallbackShellRun.tail + ) : route.page const sourceRegex = pagePattern.replace( '^', - `^${config.basePath && config.basePath !== '/' ? path.posix.join('/', config.basePath || '') : ''}[/]?${shouldLocalize ? '(?<nextLocale>[^/]{1,})' : ''}` + () => + `^${escapedBasePath}[/]?${shouldLocalize ? '(?<nextLocale>[^/]{1,})' : ''}` ) const destination = path.posix.join( @@ -2163,6 +2177,13 @@ export async function handleBuildComplete({ shouldLocalize ? '/$nextLocale' : '', pagePath ) + getDestinationQuery(route.routeKeys) + // Count capture names, not parameter names. An interception route can + // capture the same parameter with both nxtP and nxtI names. + const suffixCaptureIndex = + Object.keys(routeRegex.routeKeys).length + + (shouldLocalize ? 1 : 0) + + (fallbackShellRun ? 1 : 0) + + 1 const hasAppPages = Boolean(appPageKeys && appPageKeys.length > 0) @@ -2196,15 +2217,18 @@ export async function handleBuildComplete({ // An optional group is unsafe here. An adapter, or the router that // consumes its output, can resolve the placeholders in a destination // from the match result rather than from the pattern. A group that does - // not match is then absent from that result, and the literal text - // `$rscSuffix` stays in the destination. + // not match is then absent from that result, and the destination + // placeholder stays unresolved. dynamicRoutes.push({ source: pagePath, sourceRegex: sourceRegex.replace( new RegExp(escapeStringRegexp('(?:/)?$')), - '(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$' + '(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$' + ), + destination: destination.replace( + /($|\?)/, + (separator) => `$${suffixCaptureIndex}${separator}` ), - destination: destination?.replace(/($|\?)/, '$rscSuffix$1'), has: plainHas, missing: undefined, }) @@ -2218,9 +2242,12 @@ export async function handleBuildComplete({ source: pagePath + '.rsc', sourceRegex: sourceRegex.replace( new RegExp(escapeStringRegexp('(?:/)?$')), - '(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc)(?:/)?$' + '(\\.rsc|\\.segments/.+\\.segment\\.rsc)(?:/)?$' + ), + destination: destination.replace( + /($|\?)/, + (separator) => `$${suffixCaptureIndex}${separator}` ), - destination: destination?.replace(/($|\?)/, '$rscSuffix$1'), has: suffixedHas, missing: undefined, }) @@ -2246,7 +2273,7 @@ export async function handleBuildComplete({ source: route.page, sourceRegex: segmentRoute.source.replace( '^', - `^${config.basePath && config.basePath !== '/' ? path.posix.join('/', config.basePath || '') : ''}[/]?` + () => `^${escapedBasePath}[/]?` ), destination: path.posix.join( '/', diff --git a/packages/next/src/build/adapter/fallback-shell-runs.ts b/packages/next/src/build/adapter/fallback-shell-runs.ts index 89942220a6e5..d93351c29dfe 100644 --- a/packages/next/src/build/adapter/fallback-shell-runs.ts +++ b/packages/next/src/build/adapter/fallback-shell-runs.ts @@ -157,7 +157,7 @@ function splitShellPage( * with shells for `acme/en`, `acme/de` and `globex/en`, that pattern holds: * * ``` - * (?<shellPrefix>acme/en|acme/de|globex/en) + * (acme/en|acme/de|globex/en) * ``` * * A pattern that offered a choice per param instead, such as diff --git a/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<typeof getServerSideProps>) { + return ( + <> + <p id={`${locale}-${slug}`}>{`${locale}:${slug}`}</p> + <pre id="query">{JSON.stringify(query)}</pre> + <Link + id="next-page" + href="/legacy/two?term=next" + locale="fr" + prefetch={false} + > + French page + </Link> + </> + ) +} diff --git a/test/e2e/api-resolver-query-writeable/api-resolver-query-writeable.test.ts b/test/e2e/api-resolver-query-writeable/api-resolver-query-writeable.test.ts index 19a56e9db80b..8b928effa2a0 100644 --- a/test/e2e/api-resolver-query-writeable/api-resolver-query-writeable.test.ts +++ b/test/e2e/api-resolver-query-writeable/api-resolver-query-writeable.test.ts @@ -1,9 +1,8 @@ import { nextTestSetup } from 'e2e-utils' describe('api-resolver-query-writeable', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, startCommand: 'node server.js', serverReadyPattern: /Next mode: (production|development)/, dependencies: { @@ -12,10 +11,6 @@ describe('api-resolver-query-writeable', () => { }, }) - if (skipped) { - return - } - it('should allow req.query to be writable and reflect changes made in the API handler', async () => { const res = await next.fetch('/api?hello=yes', { headers: { diff --git a/test/e2e/app-dir/actions-allowed-origins/app-action-allowed-origins.test.ts b/test/e2e/app-dir/actions-allowed-origins/app-action-allowed-origins.test.ts index 7a055bb2fc5a..2b5b0338b41c 100644 --- a/test/e2e/app-dir/actions-allowed-origins/app-action-allowed-origins.test.ts +++ b/test/e2e/app-dir/actions-allowed-origins/app-action-allowed-origins.test.ts @@ -3,9 +3,8 @@ import { check } from 'next-test-utils' import { join } from 'path' describe('app-dir action allowed origins', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: join(__dirname, 'safe-origins'), - skipDeployment: true, dependencies: { 'server-only': 'latest', }, @@ -13,10 +12,6 @@ describe('app-dir action allowed origins', () => { forcedPort: 'random', }) - if (skipped) { - return - } - it('should pass if localhost is set as a safe origin', async function () { const browser = await next.browser('/') diff --git a/test/e2e/app-dir/actions-allowed-origins/app-action-opaque-origin.test.ts b/test/e2e/app-dir/actions-allowed-origins/app-action-opaque-origin.test.ts index b4126ae632d0..fee0326837e5 100644 --- a/test/e2e/app-dir/actions-allowed-origins/app-action-opaque-origin.test.ts +++ b/test/e2e/app-dir/actions-allowed-origins/app-action-opaque-origin.test.ts @@ -3,18 +3,13 @@ import { retry } from 'next-test-utils' import { join } from 'path' describe('app-dir action allowed from opaque origins', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: join(__dirname, 'opaque-origin'), - skipDeployment: true, env: { NEXT_TEST_ALLOW_OPAQUE_ORIGIN: '1', }, }) - if (skipped) { - return - } - it('should succeed on submission', async function () { const browser = await next.browser('/sandboxed') diff --git a/test/e2e/app-dir/adapter-route-navigation/adapter-route-navigation.test.ts b/test/e2e/app-dir/adapter-route-navigation/adapter-route-navigation.test.ts new file mode 100644 index 000000000000..549cca61ac9d --- /dev/null +++ b/test/e2e/app-dir/adapter-route-navigation/adapter-route-navigation.test.ts @@ -0,0 +1,139 @@ +import { nextTestSetup } from 'e2e-utils' +import { createRouterAct } from 'router-act' + +const basePath = '/base' +const routePath = '/many/one/two/three/four/five/six/seven/eight' +const rootParameters = [ + { team: 'acme', region: 'east' }, + { team: 'cash$2', region: 'east' }, + { team: 'cash$3', region: 'east' }, + { team: 'sparse', region: 'west' }, +] +const parameters = { + first: 'one', + second: 'two', + third: 'three', + fourth: 'four', + fifth: 'five', + sixth: 'six', + seventh: 'seven', + eighth: 'eight', + ninth: 'nine', +} + +describe.each([false, true])( + 'adapter route navigation, collapseAdapterRoutes: %s', + (collapseAdapterRoutes) => { + const { next } = nextTestSetup({ + files: __dirname, + env: { TEST_COLLAPSE_ADAPTER_ROUTES: String(collapseAdapterRoutes) }, + }) + + async function startBrowser(pathname: string) { + let act: ReturnType<typeof createRouterAct> | undefined + const browser = await next.browser(`${basePath}${pathname}`, { + permissions: [], + beforePageLoad(page) { + act = createRouterAct(page, { includeAppShellRequests: true }) + }, + }) + if (act === undefined) { + throw new Error('Router act was not initialized') + } + await browser.eval('window.__testDocument = "retained"') + return { browser, act } + } + + it.each(rootParameters)( + 'renders every parameter on a direct visit within $team/$region', + async ({ team, region }) => { + const { browser } = await startBrowser( + `/${team}/${region}${routePath}/nine?term=kept` + ) + expect( + JSON.parse(await browser.elementById('parameters').text()) + ).toEqual({ + team: encodeURIComponent(team), + region: encodeURIComponent(region), + ...parameters, + }) + expect(JSON.parse(await browser.elementById('query').text())).toEqual({ + term: 'kept', + }) + } + ) + + it.each(rootParameters)( + 'navigates within $team/$region without prefetching', + async ({ team, region }) => { + const prefix = `/${team}/${region}` + // App Router passes encoded parameter values to components. + const expectedParameters = { + team: encodeURIComponent(team), + region: encodeURIComponent(region), + ...parameters, + } + const { browser, act } = await startBrowser(`${prefix}/hub`) + expect(await browser.elementById('root-params').text()).toBe( + `${expectedParameters.team}:${expectedParameters.region}` + ) + const href = `/${expectedParameters.team}/${expectedParameters.region}${routePath}/nine?term=kept` + await act( + async () => { + await browser.elementByCss(`a[href="${basePath}${href}"]`).click() + }, + { includes: 'Article nine' } + ) + expect( + JSON.parse(await browser.elementById('parameters').text()) + ).toEqual(expectedParameters) + expect(JSON.parse(await browser.elementById('query').text())).toEqual({ + term: 'kept', + }) + expect(decodeURIComponent(new URL(await browser.url()).pathname)).toBe( + `${basePath}${prefix}${routePath}/nine` + ) + expect(await browser.eval('window.__testDocument')).toBe('retained') + } + ) + + // @force-gate prefetching + it('prefetches page content before navigation', async () => { + const { browser, act } = await startBrowser('/acme/east/hub') + const href = `/acme/east${routePath}/prefetched?term=kept` + await act( + async () => { + await browser + .elementByCss(`input[data-link-accordion="${href}"]`) + .click() + }, + { includes: 'Many parameters' } + ) + await act( + async () => { + await browser.elementByCss(`a[href="${basePath}${href}"]`).click() + }, + { includes: 'Article prefetched' } + ) + expect( + JSON.parse(await browser.elementById('parameters').text()) + ).toEqual({ + team: 'acme', + region: 'east', + ...parameters, + ninth: 'prefetched', + }) + expect(JSON.parse(await browser.elementById('query').text())).toEqual({ + term: 'kept', + }) + expect(await browser.eval('window.__testDocument')).toBe('retained') + }) + + it('returns 404 outside the base path', async () => { + for (const invalidBasePath of ['/base-other', '/bas', '/baseX']) { + const response = await next.fetch(`${invalidBasePath}/acme/east/hub`) + expect(response.status).toBe(404) + } + }) + } +) diff --git a/test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/hub/page.tsx b/test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/hub/page.tsx new file mode 100644 index 000000000000..9ed98ed0ed9f --- /dev/null +++ b/test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/hub/page.tsx @@ -0,0 +1,33 @@ +import { Suspense } from 'react' +import Link from 'next/link' +import { LinkAccordion } from '../../../../components/link-accordion' + +async function Links({ + params, +}: { + params: Promise<{ team: string; region: string }> +}) { + const { team, region } = await params + const prefix = `/${team}/${region}/many/one/two/three/four/five/six/seven/eight` + + return ( + <> + <Link href={`${prefix}/nine?term=kept`} prefetch={false}> + Navigate without prefetching + </Link> + <LinkAccordion href={`${prefix}/prefetched?term=kept`} /> + </> + ) +} + +export default function Page({ + params, +}: { + params: Promise<{ team: string; region: string }> +}) { + return ( + <Suspense fallback={<p>Loading links</p>}> + <Links params={params} /> + </Suspense> + ) +} diff --git a/test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/layout.tsx b/test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/layout.tsx new file mode 100644 index 000000000000..d5eee133695b --- /dev/null +++ b/test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/layout.tsx @@ -0,0 +1,39 @@ +import { Suspense, type ReactNode } from 'react' + +export function generateStaticParams() { + return [ + { team: 'acme', region: 'east' }, + { team: 'acme', region: 'west' }, + { team: 'sparse', region: 'east' }, + { team: 'cash$2', region: 'east' }, + { team: 'cash$3', region: 'east' }, + ] +} + +async function RootParams({ + params, +}: { + params: Promise<{ team: string; region: string }> +}) { + const { team, region } = await params + return <p id="root-params">{`${team}:${region}`}</p> +} + +export default function Root({ + children, + params, +}: { + children: ReactNode + params: Promise<{ team: string; region: string }> +}) { + return ( + <html> + <body> + <Suspense fallback={<p>Loading root parameters</p>}> + <RootParams params={params} /> + </Suspense> + {children} + </body> + </html> + ) +} diff --git a/test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/many/[first]/[second]/[third]/[fourth]/[fifth]/[sixth]/[seventh]/[eighth]/[ninth]/page.tsx b/test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/many/[first]/[second]/[third]/[fourth]/[fifth]/[sixth]/[seventh]/[eighth]/[ninth]/page.tsx new file mode 100644 index 000000000000..99f8c007c75e --- /dev/null +++ b/test/e2e/app-dir/adapter-route-navigation/app/[team]/[region]/many/[first]/[second]/[third]/[fourth]/[fifth]/[sixth]/[seventh]/[eighth]/[ninth]/page.tsx @@ -0,0 +1,33 @@ +import { Suspense } from 'react' + +// This page tests routing, not an instant-navigation guarantee. +export const instant = false + +type Props = { + params: Promise<Record<string, string>> + searchParams: Promise<Record<string, string | string[] | undefined>> +} + +async function Content({ params, searchParams }: Props) { + const parameters = await params + const query = await searchParams + + return ( + <> + <p id="article">{`Article ${parameters.ninth}`}</p> + <pre id="parameters">{JSON.stringify(parameters)}</pre> + <pre id="query">{JSON.stringify(query)}</pre> + </> + ) +} + +export default function Page(props: Props) { + return ( + <> + <h1>Many parameters</h1> + <Suspense fallback={<p>Loading article</p>}> + <Content {...props} /> + </Suspense> + </> + ) +} diff --git a/test/e2e/app-dir/adapter-route-navigation/components/link-accordion.tsx b/test/e2e/app-dir/adapter-route-navigation/components/link-accordion.tsx new file mode 100644 index 000000000000..3878ec8c7881 --- /dev/null +++ b/test/e2e/app-dir/adapter-route-navigation/components/link-accordion.tsx @@ -0,0 +1,21 @@ +'use client' + +import Link from 'next/link' +import { useState } from 'react' + +export function LinkAccordion({ href }: { href: string }) { + const [visible, setVisible] = useState(false) + + return ( + <> + <input + type="checkbox" + aria-label={`Show ${href}`} + data-link-accordion={href} + checked={visible} + onChange={() => setVisible(!visible)} + /> + {visible ? <Link href={href}>Open article</Link> : null} + </> + ) +} diff --git a/test/e2e/app-dir/adapter-route-navigation/next.config.ts b/test/e2e/app-dir/adapter-route-navigation/next.config.ts new file mode 100644 index 000000000000..a9f4e2b78c61 --- /dev/null +++ b/test/e2e/app-dir/adapter-route-navigation/next.config.ts @@ -0,0 +1,15 @@ +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + basePath: '/base', + cacheComponents: true, + partialPrefetching: false, + experimental: { + cachedNavigations: true, + optimisticRouting: true, + prefetchInlining: false, + collapseAdapterRoutes: process.env.TEST_COLLAPSE_ADAPTER_ROUTES === 'true', + }, +} + +export default nextConfig diff --git a/test/e2e/app-dir/adapter-rsc-query-leak/adapter-rsc-query-leak.test.ts b/test/e2e/app-dir/adapter-rsc-query-leak/adapter-rsc-query-leak.test.ts new file mode 100644 index 000000000000..8178331318c7 --- /dev/null +++ b/test/e2e/app-dir/adapter-rsc-query-leak/adapter-rsc-query-leak.test.ts @@ -0,0 +1,129 @@ +import { nextTestSetup } from 'e2e-utils' +import { createRouterAct } from 'router-act' + +describe.each([false, true])( + 'adapter query parameters, collapseAdapterRoutes: %s', + (collapseAdapterRoutes) => { + const { next } = nextTestSetup({ + files: __dirname, + env: { + TEST_COLLAPSE_ADAPTER_ROUTES: String(collapseAdapterRoutes), + }, + }) + + async function startBrowser(url: string) { + let act: ReturnType<typeof createRouterAct> | undefined + const browser = await next.browser(url, { + permissions: [], + beforePageLoad(page) { + act = createRouterAct(page, { includeAppShellRequests: true }) + }, + }) + if (act === undefined) { + throw new Error('Router act was not initialized') + } + return { browser, act } + } + + describe.each([ + { name: 'no search parameters', search: '', searchParams: {} }, + { + name: 'user search parameters', + search: + '?rscSuffix=first&rscSuffix=second&shellPrefix=user&term=example', + searchParams: { + rscSuffix: ['first', 'second'], + shellPrefix: 'user', + term: 'example', + }, + }, + ])('$name', ({ search, searchParams }) => { + it.each([false, true])( + 'preserves search parameters in the document, draft mode: %s', + async (draft) => { + const { browser } = await startBrowser( + `${draft ? '/draft' : '/article/one'}${search}` + ) + expect(await browser.elementById('article').text()).toBe( + `${draft ? 'Draft' : 'Published'} article one` + ) + expect(JSON.parse(await browser.elementById('query').text())).toEqual( + searchParams + ) + } + ) + + it.each([false, true])( + 'preserves search parameters without retrying navigation, draft mode: %s', + async (draft) => { + const { browser, act } = await startBrowser( + draft ? '/draft' : '/article/one' + ) + const href = `/article/two${search}` + await act( + async () => { + await browser.elementByCss(`a[href="${href}"]`).click() + }, + { includes: `${draft ? 'Draft' : 'Published'} article two` } + ) + expect(await browser.elementById('article').text()).toBe( + `${draft ? 'Draft' : 'Published'} article two` + ) + expect(JSON.parse(await browser.elementById('query').text())).toEqual( + searchParams + ) + expect(new URL(await browser.url()).pathname).toBe('/article/two') + } + ) + + it('preserves search parameters after a non-draft Server Action', async () => { + const { browser, act } = await startBrowser(`/article/one${search}`) + await act(async () => { + await browser.elementById('update-cookie').click() + }) + expect(await browser.elementById('shared-render').text()).toBe( + 'Cookie updated' + ) + expect(await browser.elementById('article').text()).toBe( + 'Published article one' + ) + expect(JSON.parse(await browser.elementById('query').text())).toEqual( + searchParams + ) + }) + + it.each(['en', 'de'])( + 'preserves search parameters in a fallback shell document for %s', + async (locale) => { + const { browser } = await startBrowser( + `/${locale}/posts/one${search}` + ) + expect(await browser.elementById('article').text()).toBe( + 'Published article one' + ) + expect(JSON.parse(await browser.elementById('query').text())).toEqual( + searchParams + ) + } + ) + + it('preserves search parameters when navigating through a fallback shell', async () => { + const { browser, act } = await startBrowser('/en/posts/one') + const href = `/en/posts/two${search}` + await act( + async () => { + await browser.elementByCss(`a[href="${href}"]`).click() + }, + { includes: 'Published article two' } + ) + expect(await browser.elementById('article').text()).toBe( + 'Published article two' + ) + expect(JSON.parse(await browser.elementById('query').text())).toEqual( + searchParams + ) + expect(new URL(await browser.url()).pathname).toBe('/en/posts/two') + }) + }) + } +) diff --git a/test/e2e/app-dir/adapter-rsc-query-leak/app/[locale]/layout.tsx b/test/e2e/app-dir/adapter-rsc-query-leak/app/[locale]/layout.tsx new file mode 100644 index 000000000000..91388b20dc29 --- /dev/null +++ b/test/e2e/app-dir/adapter-rsc-query-leak/app/[locale]/layout.tsx @@ -0,0 +1,9 @@ +import type { ReactNode } from 'react' + +export function generateStaticParams() { + return [{ locale: 'en' }, { locale: 'de' }] +} + +export default function Layout({ children }: { children: ReactNode }) { + return children +} diff --git a/test/e2e/app-dir/adapter-rsc-query-leak/app/[locale]/posts/[slug]/page.tsx b/test/e2e/app-dir/adapter-rsc-query-leak/app/[locale]/posts/[slug]/page.tsx new file mode 100644 index 000000000000..8a4c914ac14b --- /dev/null +++ b/test/e2e/app-dir/adapter-rsc-query-leak/app/[locale]/posts/[slug]/page.tsx @@ -0,0 +1 @@ +export { default } from '../../../article/[slug]/page' diff --git a/test/e2e/app-dir/adapter-rsc-query-leak/app/actions.ts b/test/e2e/app-dir/adapter-rsc-query-leak/app/actions.ts new file mode 100644 index 000000000000..9ea81ee2a12c --- /dev/null +++ b/test/e2e/app-dir/adapter-rsc-query-leak/app/actions.ts @@ -0,0 +1,8 @@ +'use server' + +import { cookies } from 'next/headers' + +export async function updateCookie() { + const cookieStore = await cookies() + cookieStore.set('query-test', 'set') +} diff --git a/test/e2e/app-dir/adapter-rsc-query-leak/app/article/[slug]/page.tsx b/test/e2e/app-dir/adapter-rsc-query-leak/app/article/[slug]/page.tsx new file mode 100644 index 000000000000..555c3e1ba319 --- /dev/null +++ b/test/e2e/app-dir/adapter-rsc-query-leak/app/article/[slug]/page.tsx @@ -0,0 +1,34 @@ +import { Suspense } from 'react' +import { draftMode } from 'next/headers' +import { connection } from 'next/server' + +type Props = { + params: Promise<{ slug: string }> + searchParams: Promise<Record<string, string | string[] | undefined>> +} + +export function generateStaticParams() { + return [{ slug: 'built' }] +} + +async function Content({ params, searchParams }: Props) { + await connection() + const { slug } = await params + const query = await searchParams + const { isEnabled } = await draftMode() + + return ( + <> + <p id="article">{`${isEnabled ? 'Draft' : 'Published'} article ${slug}`}</p> + <p id="query">{JSON.stringify(query)}</p> + </> + ) +} + +export default function Page(props: Props) { + return ( + <Suspense fallback={<p>Loading article</p>}> + <Content {...props} /> + </Suspense> + ) +} diff --git a/test/e2e/app-dir/adapter-rsc-query-leak/app/draft/route.ts b/test/e2e/app-dir/adapter-rsc-query-leak/app/draft/route.ts new file mode 100644 index 000000000000..e7f2bd16310a --- /dev/null +++ b/test/e2e/app-dir/adapter-rsc-query-leak/app/draft/route.ts @@ -0,0 +1,10 @@ +import { draftMode } from 'next/headers' +import { NextResponse, type NextRequest } from 'next/server' + +export async function GET(request: NextRequest) { + const draft = await draftMode() + draft.enable() + const destination = new URL('/article/one', request.url) + destination.search = request.nextUrl.search + return NextResponse.redirect(destination) +} diff --git a/test/e2e/app-dir/adapter-rsc-query-leak/app/layout.tsx b/test/e2e/app-dir/adapter-rsc-query-leak/app/layout.tsx new file mode 100644 index 000000000000..dae2a8746756 --- /dev/null +++ b/test/e2e/app-dir/adapter-rsc-query-leak/app/layout.tsx @@ -0,0 +1,51 @@ +import { Suspense, type ReactNode } from 'react' +import Link from 'next/link' +import { cookies } from 'next/headers' +import { connection } from 'next/server' +import { updateCookie } from './actions' + +async function SharedContent() { + await connection() + const cookieStore = await cookies() + return ( + <p id="shared-render"> + {cookieStore.has('query-test') ? 'Cookie updated' : 'Cookie absent'} + </p> + ) +} + +export default function Root({ children }: { children: ReactNode }) { + return ( + <html> + <body> + <Suspense fallback={<p>Loading shared content</p>}> + <SharedContent /> + </Suspense> + <nav> + <Link href="/article/two" prefetch={false}> + Article two + </Link> + <Link + href="/article/two?rscSuffix=first&rscSuffix=second&shellPrefix=user&term=example" + prefetch={false} + > + Article two with search parameters + </Link> + <Link href="/en/posts/two" prefetch={false}> + English article two + </Link> + <Link + href="/en/posts/two?rscSuffix=first&rscSuffix=second&shellPrefix=user&term=example" + prefetch={false} + > + English article two with search parameters + </Link> + </nav> + <form action={updateCookie}> + <button id="update-cookie">Update an ordinary cookie</button> + </form> + {children} + </body> + </html> + ) +} diff --git a/test/e2e/app-dir/adapter-rsc-query-leak/next.config.ts b/test/e2e/app-dir/adapter-rsc-query-leak/next.config.ts new file mode 100644 index 000000000000..86b2e93e41b5 --- /dev/null +++ b/test/e2e/app-dir/adapter-rsc-query-leak/next.config.ts @@ -0,0 +1,16 @@ +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + cacheComponents: true, + partialPrefetching: false, + // Exercise the prerender bypass with ordinary browser requests, not only + // draft mode. + htmlLimitedBots: /.*/, + experimental: { + cachedNavigations: true, + optimisticRouting: true, + collapseAdapterRoutes: process.env.TEST_COLLAPSE_ADAPTER_ROUTES === 'true', + }, +} + +export default nextConfig diff --git a/test/e2e/app-dir/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) <script /> and <link /> 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<ReturnType<typeof next.fetch>>, $: Awaited<ReturnType<typeof next.render$>> 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 <p id="app-page">hello from app/docs/page</p> +} 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 ( + <html> + <body>{children}</body> + </html> + ) +} 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<ReturnType<typeof next.browser>> + ) { + 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 ( + <> + <p id="pages-page">hello from pages/index</p> + <p id="tab">{router.query.tab || 'a'}</p> + <Link id="tab-b" href="/?tab=b" shallow> + tab b + </Link> + {/* + 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 `/`. + */} + <Link id="app-link" href="/docs"> + to app route + </Link> + </> + ) +} 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 <p id="app-page">hello from app/dashboard/page</p> +} 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 ( + <html> + <body>{children}</body> + </html> + ) +} 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 ( + <> + <p id="pages-page">hello from pages/index</p> + {/* + Prefetching this link evaluates the client router filter against + `/fr/legacy`, which matches the French-only redirect. + */} + <Link id="fr-legacy-link" href="/legacy" locale="fr"> + legacy (fr) + </Link> + <Link id="en-legacy-link" href="/legacy"> + legacy (en) + </Link> + </> + ) +} 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 <p id="legacy-page">hello from pages/legacy</p> +} 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 <p id="app-page">hello from app/dashboard/page</p> +} 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 ( + <html> + <body>{children}</body> + </html> + ) +} 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 <p id="app-modal-page">hello from app/modal/[id]/page</p> +} 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<ReturnType<typeof next.browser>>, + 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 ( + <> + <p id="pages-page">hello from pages/blog/[slug]</p> + {/* server-provided prop: it must survive a shallow navigation */} + <p id="slug">{slug}</p> + {/* read from the router: a shallow navigation does not re-run getServerSideProps */} + <p id="tab">{router.query.tab || 'a'}</p> + <Link id="tab-b" href={`/blog/${slug}?tab=b`} shallow> + tab b + </Link> + {/* + "route as modal": href is the current dynamic route pattern with an + extra query param, `as` is an app route. + */} + <Link + id="app-link" + href={{ + pathname: router.pathname, + query: { ...router.query, modal: 1 }, + }} + as="/dashboard" + shallow + > + to app route + </Link> + </> + ) +} 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 ( + <> + <p id="pages-page">hello from pages/index</p> + <p id="tab">{router.query.tab || 'a'}</p> + <Link id="tab-b" href="/?tab=b" shallow> + tab b + </Link> + {/* + 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`. + */} + <Link id="app-link" href="/?modal=1" as="/dashboard" shallow> + to app route + </Link> + {/* + `/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. + */} + <Link id="modal-link" href="/modal"> + to modal + </Link> + <button + id="push-pretty" + onClick={() => { + // href is the flagged pages route, `as` is an unrelated pretty URL + router.push('/modal', '/pretty') + }} + > + push /modal as /pretty + </button> + <button + id="push-pretty-rewrite" + onClick={() => { + // `/pretty` is rewritten to the flagged pages route `/modal` by a + // config rewrite. A button instead of a link so that no hover + // prefetch of `/pretty` itself writes a marker. + router.push('/pretty') + }} + > + push /pretty + </button> + </> + ) +} 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 */} + <p id="modal-page">{title}</p> + {/* + 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. + */} + <button + id="prefetch-canonical" + onClick={async () => { + await router.prefetch('/modal') + setPrefetchState('done') + }} + > + prefetch canonical + </button> + <p id="prefetch-state">{prefetchState}</p> + <Link id="hash-link" href="#section"> + to section + </Link> + <p id="section">section</p> + </> + ) +} 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 <p id="app-page">hello from app/blog/new/page</p> +} 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 ( + <html> + <body>{children}</body> + </html> + ) +} 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 ( + <> + <p id="pages-page">hello from pages/blog/[slug]</p> + {/* server-provided prop: it must survive a shallow navigation */} + <p id="slug">{slug}</p> + {/* read from the router so the value does not depend on a data fetch */} + <p id="tab">{router.query.tab || 'a'}</p> + {/* client state: a hard navigation resets it */} + <button id="counter" onClick={() => setCount((c) => c + 1)}> + {count} + </button> + <p id="prefetch-state">{prefetchState}</p> + <button + id="prefetch-new" + onClick={async () => { + // "route as modal": `href` is the current dynamic route pattern with + // another slug, `as` is the app route `app/blog/new/page.js`. The + // client router filter matches `as`, and the prefetch stores its + // marker under the `href` pathname, the route of this page. + await router.prefetch('/blog/[slug]?slug=new', '/blog/new') + setPrefetchState('done') + }} + > + prefetch /blog/new + </button> + <Link id="tab-b" href={`/blog/${slug}?tab=b`} shallow> + tab b + </Link> + </> + ) +} 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 ( + <div id="two-fallback" data-fallback> + loading two... + </div> + ) +} diff --git a/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/blocking/[one]/[two]/page.tsx b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/blocking/[one]/[two]/page.tsx new file mode 100644 index 000000000000..d6772ffd95f6 --- /dev/null +++ b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/blocking/[one]/[two]/page.tsx @@ -0,0 +1,9 @@ +export default async function Page({ + params, +}: { + params: Promise<{ one: string; two: string }> +}) { + const { two } = await params + + return <div id="two">{two}</div> +} diff --git a/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/blocking/[one]/layout.tsx b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/blocking/[one]/layout.tsx new file mode 100644 index 000000000000..ed42b3f54ccd --- /dev/null +++ b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/blocking/[one]/layout.tsx @@ -0,0 +1,24 @@ +import type { ReactNode } from 'react' + +export function generateStaticParams() { + return [{ one: 'b' }] +} + +export default async function Layout({ + children, + params, +}: { + children: ReactNode + params: Promise<{ one: string }> +}) { + const { one } = await params + + return ( + <div> + <div id="one" data-rendered-at={performance.now()}> + {one} + </div> + {children} + </div> + ) +} diff --git a/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/optional/[[...parts]]/page.tsx b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/optional/[[...parts]]/page.tsx new file mode 100644 index 000000000000..c97edee49aa3 --- /dev/null +++ b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/optional/[[...parts]]/page.tsx @@ -0,0 +1,17 @@ +export function generateStaticParams() { + return [{ parts: [] }, { parts: ['named'] }] +} + +export default async function Page({ + params, +}: { + params: Promise<{ parts?: string[] }> +}) { + const { parts } = await params + + return ( + <div id="optional" data-rendered-at={performance.now()}> + {parts?.join('/')} + </div> + ) +} diff --git a/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/prefix/[one]/[two]/loading.tsx b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/prefix/[one]/[two]/loading.tsx index 6fba33c24064..9eb1f6ec2ee9 100644 --- a/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/prefix/[one]/[two]/loading.tsx +++ b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/prefix/[one]/[two]/loading.tsx @@ -1,6 +1,6 @@ export default function Loading() { return ( - <div id="two-fallback" data-fallback> + <div id="two-fallback" data-fallback data-rendered-at={performance.now()}> loading two... </div> ) diff --git a/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/prefix/[one]/layout.tsx b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/prefix/[one]/layout.tsx index fc1a65608128..1cddd512ca80 100644 --- a/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/prefix/[one]/layout.tsx +++ b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/app/prefix/[one]/layout.tsx @@ -25,7 +25,11 @@ export default async function LayoutWrapper(props) { return ( <Suspense fallback={ - <div id="one-fallback" data-fallback> + <div + id="one-fallback" + data-fallback + data-rendered-at={performance.now()} + > loading one... </div> } diff --git a/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/pages/api/revalidate.ts b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/pages/api/revalidate.ts new file mode 100644 index 000000000000..dd8e6f4e08f7 --- /dev/null +++ b/test/e2e/app-dir/partial-fallback-shell-upgrade/fixtures/partial-prefetching-disabled/pages/api/revalidate.ts @@ -0,0 +1,22 @@ +import type { NextApiRequest, NextApiResponse } from 'next' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const pathParam = req.query['path'] + + if (!pathParam) { + return res.status(400).send(`Missing required query param "path"`) + } + + const paths = Array.isArray(pathParam) ? pathParam : [pathParam] + + try { + await Promise.all(paths.map((path) => res.revalidate(path))) + return res.json({ revalidated: true }) + } catch (err) { + console.error(err) + return res.status(500).send(`Error revalidating ${paths}`) + } +} diff --git a/test/e2e/app-dir/partial-fallback-shell-upgrade/partial-fallback-shell-upgrade.test.ts b/test/e2e/app-dir/partial-fallback-shell-upgrade/partial-fallback-shell-upgrade.test.ts index 06ed58e3a9cf..d4225fd82b59 100644 --- a/test/e2e/app-dir/partial-fallback-shell-upgrade/partial-fallback-shell-upgrade.test.ts +++ b/test/e2e/app-dir/partial-fallback-shell-upgrade/partial-fallback-shell-upgrade.test.ts @@ -9,12 +9,11 @@ type NextInstance = ReturnType<typeof nextTestSetup>['next'] function createSplitHTMLFetcher(next: NextInstance) { return async function fetchSplitHTML(pathname: string) { - let response: Awaited<ReturnType<typeof next.fetch>> | undefined + const response = await next.fetch(pathname) + expect(response.status).toBe(200) + const [staticPart, dynamicPart] = await splitResponseWithPPRSentinel( async () => { - response = await next.fetch(pathname) - expect(response.status).toBe(200) - if (!response.body) { throw new Error(`Expected a streamed response body for ${pathname}`) } @@ -24,16 +23,15 @@ function createSplitHTMLFetcher(next: NextInstance) { ) return { - response: response!, + response, dynamicPart, static$: cheerio.load(staticPart), } } } -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// The latest changes to support this behavior on deployed infra are available in the adapter, -// and are not being backported to the CLI +// The legacy Vercel builder does not implement the Cache Components shell +// eligibility and upgrade behavior asserted here. // @force-gate !deploy || adapter describe('partial-fallback-shell-upgrade', () => { const { next, isNextDev } = nextTestSetup({ @@ -216,12 +214,11 @@ describe('partial-fallback-shell-upgrade', () => { }) }) -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// The latest changes to support this behavior on deployed infra are available in the adapter, -// and are not being backported to the CLI +// The legacy Vercel builder does not implement the Cache Components shell +// eligibility and upgrade behavior asserted here. // @force-gate !deploy || adapter describe('partial-fallback-shell-upgrade - partialPrefetching disabled', () => { - const { next, isNextDev } = nextTestSetup({ + const { next, isNextDev, isNextDeploy } = nextTestSetup({ files: path.join(__dirname, 'fixtures', 'partial-prefetching-disabled'), }) @@ -286,4 +283,229 @@ describe('partial-fallback-shell-upgrade - partialPrefetching disabled', () => { 'generic shell should remain shared without partialPrefetching' ) }) + + it('shares an on-demand shell across params without generateStaticParams', async () => { + const firstResult = await fetchSplitHTML('/blocking/c/foo') + const renderedAt = firstResult.static$('#one').attr('data-rendered-at') + + expect(firstResult.static$('#one').text()).toBe('c') + expect(renderedAt).toMatch(/^\d+(?:\.\d+)?$/) + expect(firstResult.static$('#two').length).toBe(0) + expect(firstResult.static$('#two-fallback').text()).toBe('loading two...') + expect(firstResult.dynamicPart).toContain('<div id="two">foo</div>') + + if (isNextDeploy) { + await retry(async () => { + const cachedResult = await fetchSplitHTML('/blocking/c/foo') + expect(cachedResult.response.headers.get('x-vercel-cache')).toBe('HIT') + }) + } + + const secondResult = await fetchSplitHTML('/blocking/c/bar') + + expect(secondResult.static$('#one').text()).toBe('c') + expect(secondResult.static$('#one').attr('data-rendered-at')).toBe( + renderedAt + ) + expect(secondResult.static$('#two').length).toBe(0) + expect(secondResult.static$('#two-fallback').text()).toBe('loading two...') + expect(secondResult.dynamicPart).toContain('<div id="two">bar</div>') + expect(secondResult.dynamicPart).not.toContain('<div id="two">foo</div>') + + if (isNextDeploy) { + expect(secondResult.response.headers.get('x-vercel-cache')).toBe('HIT') + } + }) + + it('keeps params without generateStaticParams dynamic after explicit revalidation of an on-demand shell', async () => { + const pathname = '/blocking/d/foo' + const firstResult = await fetchSplitHTML(pathname) + const firstRenderedAt = firstResult.static$('#one').attr('data-rendered-at') + expect(firstRenderedAt).toMatch(/^\d+(?:\.\d+)?$/) + + if (isNextDeploy) { + await retry(async () => { + const cachedResult = await fetchSplitHTML(pathname) + expect(cachedResult.response.headers.get('x-vercel-cache')).toBe('HIT') + expect(cachedResult.static$('#one').attr('data-rendered-at')).toBe( + firstRenderedAt + ) + }) + } + + const revalidateResponse = await next.fetch( + `/api/revalidate?path=${encodeURIComponent(pathname)}` + ) + expect(revalidateResponse.status).toBe(200) + expect(await revalidateResponse.json()).toEqual({ revalidated: true }) + + await retry(async () => { + const revalidatedResult = await fetchSplitHTML(pathname) + const revalidatedAt = revalidatedResult + .static$('#one') + .attr('data-rendered-at') + + expect(revalidatedResult.static$('#one').text()).toBe('d') + expect(revalidatedAt).toMatch(/^\d+(?:\.\d+)?$/) + expect(revalidatedAt).not.toBe(firstRenderedAt) + expect(revalidatedResult.static$('#two').length).toBe(0) + expect(revalidatedResult.static$('#two-fallback').text()).toBe( + 'loading two...' + ) + expect(revalidatedResult.dynamicPart).toContain('<div id="two">foo</div>') + + const secondResult = await fetchSplitHTML('/blocking/d/bar') + + expect(secondResult.static$('#one').text()).toBe('d') + expect(secondResult.static$('#one').attr('data-rendered-at')).toBe( + revalidatedAt + ) + expect(secondResult.static$('#two').length).toBe(0) + expect(secondResult.static$('#two-fallback').text()).toBe( + 'loading two...' + ) + expect(secondResult.dynamicPart).toContain('<div id="two">bar</div>') + expect(secondResult.dynamicPart).not.toContain('<div id="two">foo</div>') + + if (isNextDeploy) { + expect(secondResult.response.headers.get('x-vercel-cache')).toBe('HIT') + } + }) + }) + + it('keeps params without generateStaticParams dynamic after explicit revalidation of a servable fallback shell', async () => { + const pathname = '/prefix/d/foo' + const firstResult = await fetchSplitHTML(pathname) + + expect(firstResult.static$('#one').length).toBe(0) + expect(firstResult.static$('#one-fallback').text()).toBe('loading one...') + expect(firstResult.static$('#two').length).toBe(0) + expect(firstResult.dynamicPart).toContain('<div id="one">d</div>') + expect(firstResult.dynamicPart).toContain('<div id="two">foo</div>') + + const revalidateResponse = await next.fetch( + `/api/revalidate?path=${encodeURIComponent(pathname)}` + ) + expect(revalidateResponse.status).toBe(200) + expect(await revalidateResponse.json()).toEqual({ revalidated: true }) + + const revalidatedResult = await fetchSplitHTML(pathname) + + expect(revalidatedResult.static$('#two').length).toBe(0) + expect(revalidatedResult.dynamicPart).toContain('<div id="two">foo</div>') + + const secondResult = await fetchSplitHTML('/prefix/d/bar') + + expect(secondResult.static$('#two').length).toBe(0) + expect(secondResult.dynamicPart).toContain('<div id="two">bar</div>') + expect(secondResult.dynamicPart).not.toContain('<div id="two">foo</div>') + }) + + it('reuses generated optional catch-all pages before and after explicit revalidation', async () => { + for (const [pathname, parts] of [ + ['/optional/named', 'named'], + ['/optional', ''], + ]) { + const first$ = await next.render$(pathname) + const firstRenderedAt = first$('#optional').attr('data-rendered-at') + + expect(first$('#optional').text()).toBe(parts) + expect(firstRenderedAt).toMatch(/^\d+(?:\.\d+)?$/) + + const repeated$ = await next.render$(pathname) + + expect(repeated$('#optional').text()).toBe(parts) + expect({ + pathname, + renderedAt: repeated$('#optional').attr('data-rendered-at'), + }).toEqual({ pathname, renderedAt: firstRenderedAt }) + + if (isNextDeploy) { + await retry(async () => { + const cachedResponse = await next.fetch(pathname) + expect(cachedResponse.status).toBe(200) + const cached$ = cheerio.load(await cachedResponse.text()) + expect(cachedResponse.headers.get('x-vercel-cache')).toBe('HIT') + expect(cached$('#optional').attr('data-rendered-at')).toBe( + firstRenderedAt + ) + }) + } + + const revalidateResponse = await next.fetch( + `/api/revalidate?path=${encodeURIComponent(pathname)}` + ) + expect(revalidateResponse.status).toBe(200) + expect(await revalidateResponse.json()).toEqual({ revalidated: true }) + + await retry(async () => { + const revalidated$ = await next.render$(pathname) + const revalidatedRenderedAt = + revalidated$('#optional').attr('data-rendered-at') + + expect(revalidated$('#optional').text()).toBe(parts) + expect(revalidatedRenderedAt).toMatch(/^\d+(?:\.\d+)?$/) + expect(revalidatedRenderedAt).not.toBe(firstRenderedAt) + + const cached$ = await next.render$(pathname) + + expect(cached$('#optional').text()).toBe(parts) + expect({ + pathname, + renderedAt: cached$('#optional').attr('data-rendered-at'), + }).toEqual({ pathname, renderedAt: revalidatedRenderedAt }) + }) + } + }) + + it('revalidates the shared terminal shell without resolving dynamic params', async () => { + const pathname = '/prefix/b/foo' + const firstResult = await fetchSplitHTML(pathname) + const firstRenderedAt = firstResult + .static$('[data-rendered-at]') + .attr('data-rendered-at') + + expect(firstResult.static$('[data-rendered-at]').length).toBe(1) + expect(firstRenderedAt).toMatch(/^\d+(?:\.\d+)?$/) + expect(firstResult.static$('#two').length).toBe(0) + expect(firstResult.dynamicPart).toContain('<div id="two">foo</div>') + + const repeatedResult = await fetchSplitHTML(pathname) + + expect(repeatedResult.static$('[data-rendered-at]').length).toBe(1) + expect( + repeatedResult.static$('[data-rendered-at]').attr('data-rendered-at') + ).toBe(firstRenderedAt) + expect(repeatedResult.static$('#two').length).toBe(0) + expect(repeatedResult.dynamicPart).toContain('<div id="two">foo</div>') + + const revalidateResponse = await next.fetch( + `/api/revalidate?path=${encodeURIComponent(pathname)}` + ) + expect(revalidateResponse.status).toBe(200) + expect(await revalidateResponse.json()).toEqual({ revalidated: true }) + + await retry(async () => { + const revalidatedResult = await fetchSplitHTML(pathname) + const revalidatedRenderedAt = revalidatedResult + .static$('[data-rendered-at]') + .attr('data-rendered-at') + + expect(revalidatedResult.static$('[data-rendered-at]').length).toBe(1) + expect(revalidatedRenderedAt).toMatch(/^\d+(?:\.\d+)?$/) + expect(revalidatedRenderedAt).not.toBe(firstRenderedAt) + expect(revalidatedResult.static$('#two').length).toBe(0) + expect(revalidatedResult.dynamicPart).toContain('<div id="two">foo</div>') + + const siblingResult = await fetchSplitHTML('/prefix/b/bar') + + expect(siblingResult.static$('[data-rendered-at]').length).toBe(1) + expect( + siblingResult.static$('[data-rendered-at]').attr('data-rendered-at') + ).toBe(revalidatedRenderedAt) + expect(siblingResult.static$('#two').length).toBe(0) + expect(siblingResult.dynamicPart).toContain('<div id="two">bar</div>') + expect(siblingResult.dynamicPart).not.toContain('<div id="two">foo</div>') + }) + }) }) diff --git a/test/e2e/app-dir/root-layout-render-once/index.test.ts b/test/e2e/app-dir/root-layout-render-once/index.test.ts index f4bfa2bed9ab..0617244d4953 100644 --- a/test/e2e/app-dir/root-layout-render-once/index.test.ts +++ b/test/e2e/app-dir/root-layout-render-once/index.test.ts @@ -1,15 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('app-dir root layout render once', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should only render root layout once', async () => { let $ = await next.render$('/render-once') expect($('#counter').text()).toBe('0') diff --git a/test/e2e/app-dir/root-layout/root-layout.test.ts b/test/e2e/app-dir/root-layout/root-layout.test.ts index fde385d9d85d..c2b5f699b72e 100644 --- a/test/e2e/app-dir/root-layout/root-layout.test.ts +++ b/test/e2e/app-dir/root-layout/root-layout.test.ts @@ -2,19 +2,10 @@ import { nextTestSetup } from 'e2e-utils' import { waitForRedbox, check, getRedboxSource } from 'next-test-utils' describe('app-dir root layout', () => { - const { - next, - isNextDev: isDev, - skipped, - } = nextTestSetup({ + const { next, isNextDev: isDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - if (isDev) { // TODO-APP: re-enable after reworking the error overlay. describe.skip('Missing required tags', () => { diff --git a/test/e2e/app-dir/root-suspense-dynamic/root-suspense-dynamic.test.ts b/test/e2e/app-dir/root-suspense-dynamic/root-suspense-dynamic.test.ts index 187cb40d6820..22cbde4144aa 100644 --- a/test/e2e/app-dir/root-suspense-dynamic/root-suspense-dynamic.test.ts +++ b/test/e2e/app-dir/root-suspense-dynamic/root-suspense-dynamic.test.ts @@ -3,7 +3,6 @@ import { nextTestSetup } from 'e2e-utils' describe('Root Suspense Dynamic Rendering', () => { const { next, isNextStart } = nextTestSetup({ files: __dirname + '/fixtures/default', - skipDeployment: true, }) // TODO: remove when there is a test for isNextDev === false diff --git a/test/e2e/app-dir/segment-cache/cached-navigations/cached-navigations.test.ts b/test/e2e/app-dir/segment-cache/cached-navigations/cached-navigations.test.ts index 75dceee0ff09..a5949ff419a9 100644 --- a/test/e2e/app-dir/segment-cache/cached-navigations/cached-navigations.test.ts +++ b/test/e2e/app-dir/segment-cache/cached-navigations/cached-navigations.test.ts @@ -405,33 +405,56 @@ describe('cached navigations', () => { ) }) - it.each([ - { source: 'dynamic RSC', top: 't2' }, - { source: 'initial HTML with partial resume', top: 't3' }, - ])( - 'caches a required fallback shell from $source for repeated navigations', - async ({ source, top }) => { - const route = `/required-fallback-params/${top}/b1` - const startDate = Date.now() - let bottomIsStatic = false - let page: Playwright.Page - let initialDocument: Promise<Playwright.Response> - const browser = await next.browser( - source === 'dynamic RSC' ? `/fallback-params-hub/${top}/start` : route, - { - async beforePageLoad(p: Playwright.Page) { - page = p - await page.clock.install() - await page.clock.setFixedTime(startDate) - initialDocument = page.waitForResponse((response) => - response.request().isNavigationRequest() - ) - }, - } + // The legacy Vercel builder incorrectly prerenders params omitted from + // generateStaticParams. + // @gate !deploy || adapter + it('caches only eligible params from a cold RSC navigation for repeated navigations', async () => { + const top = 't2' + const route = `/required-fallback-params/${top}/b1` + const startDate = Date.now() + let page: Playwright.Page + const browser = await next.browser(`/fallback-params-hub/${top}/start`, { + async beforePageLoad(p: Playwright.Page) { + page = p + await page.clock.install() + await page.clock.setFixedTime(startDate) + }, + }) + const act = createRouterAct(page) + + await act( + async () => { + await browser + .elementByCss(`input[data-link-accordion="${route}"]`) + .click() + await browser.elementByCss(`a[href="${route}"]`).click() + }, + { includes: 'Dynamic content' } + ) + + expect(await browser.elementById('top').text()).toBe(`Top: ${top}`) + expect(await browser.elementById('bottom').text()).toBe('Bottom: b1') + expect(await browser.elementById('dynamic-content').text()).toBe( + 'Dynamic content' + ) + + for (const [index, step] of ['a', 'b'].entries()) { + const hub = `/fallback-params-hub/${top}/${step}` + await act( + async () => { + await browser + .elementByCss(`input[data-link-accordion="${hub}"]`) + .click() + await browser.elementByCss(`a[href="${hub}"]`).click() + }, + { includes: `Fallback params hub ${step}` } + ) + expect(await browser.elementByCss('h1').text()).toBe( + `Fallback params hub ${step}` ) - const act = createRouterAct(page) + await page.clock.setFixedTime(startDate + (index + 1) * 60_000) - if (source === 'dynamic RSC') { + await act(async () => { await act( async () => { await browser @@ -439,79 +462,112 @@ describe('cached navigations', () => { .click() await browser.elementByCss(`a[href="${route}"]`).click() }, - { includes: 'Dynamic content' } + { includes: 'Dynamic content', block: true } ) - } else { - // Inspect the document that populated this browser's cache, not a - // separate prefetch or a later request after the shell was cached. - const html = await (await initialDocument).text() - const [shell, resume] = html.split('<!-- PPR_BOUNDARY_SENTINEL -->') - expect(resume).toBeDefined() - const $ = cheerio.load(shell) - expect($('#top').text()).toBe(`Top: ${top}`) - bottomIsStatic = $('#bottom').length > 0 - expect($('#bottom-boundary').text()).toBe( - bottomIsStatic ? 'Bottom: b1' : 'Loading bottom...' + + expect(await browser.elementById('top').text()).toBe(`Top: ${top}`) + expect(await browser.elementById('bottom-boundary').text()).toBe( + 'Loading bottom...' ) - expect($('#connection-boundary').text()).toBe('Loading connection...') - expect(resume).toContain('id="dynamic-content"') - } + expect(await browser.elementById('connection-boundary').text()).toBe( + 'Loading connection...' + ) + }) expect(await browser.elementById('top').text()).toBe(`Top: ${top}`) expect(await browser.elementById('bottom').text()).toBe('Bottom: b1') expect(await browser.elementById('dynamic-content').text()).toBe( 'Dynamic content' ) + } + }) - for (const [index, step] of ['a', 'b'].entries()) { - const hub = `/fallback-params-hub/${top}/${step}` + // The legacy Vercel builder incorrectly prerenders params omitted from + // generateStaticParams. + // @gate !deploy || adapter + it('caches only eligible params from an initial HTML on-demand prerender for repeated navigations', async () => { + const top = 't3' + const route = `/required-fallback-params/${top}/b1` + const startDate = Date.now() + let page: Playwright.Page + let initialDocument: Promise<Playwright.Response> + const browser = await next.browser(route, { + async beforePageLoad(p: Playwright.Page) { + page = p + await page.clock.install() + await page.clock.setFixedTime(startDate) + initialDocument = page.waitForResponse((response) => + response.request().isNavigationRequest() + ) + }, + }) + const act = createRouterAct(page) + + // Inspect the document that populated this browser's cache, not a separate + // prefetch or a later request after the shell was cached. + const response = await initialDocument + expect(response.status()).toBe(200) + const html = await response.text() + const [shell, resume] = html.split('<!-- PPR_BOUNDARY_SENTINEL -->') + expect(resume).toBeDefined() + const $ = cheerio.load(shell) + expect($('#top').text()).toBe(`Top: ${top}`) + expect($('#bottom').length).toBe(0) + expect($('#bottom-boundary').text()).toBe('Loading bottom...') + expect($('#dynamic-content').length).toBe(0) + expect($('#connection-boundary').text()).toBe('Loading connection...') + expect(resume).toContain('id="bottom"') + expect(resume).toContain('id="dynamic-content"') + + expect(await browser.elementById('top').text()).toBe(`Top: ${top}`) + expect(await browser.elementById('bottom').text()).toBe('Bottom: b1') + expect(await browser.elementById('dynamic-content').text()).toBe( + 'Dynamic content' + ) + + for (const [index, step] of ['a', 'b'].entries()) { + const hub = `/fallback-params-hub/${top}/${step}` + await act( + async () => { + await browser + .elementByCss(`input[data-link-accordion="${hub}"]`) + .click() + await browser.elementByCss(`a[href="${hub}"]`).click() + }, + { includes: `Fallback params hub ${step}` } + ) + expect(await browser.elementByCss('h1').text()).toBe( + `Fallback params hub ${step}` + ) + await page.clock.setFixedTime(startDate + (index + 1) * 60_000) + + await act(async () => { await act( async () => { await browser - .elementByCss(`input[data-link-accordion="${hub}"]`) + .elementByCss(`input[data-link-accordion="${route}"]`) .click() - await browser.elementByCss(`a[href="${hub}"]`).click() + await browser.elementByCss(`a[href="${route}"]`).click() }, - { includes: `Fallback params hub ${step}` } + { includes: 'Dynamic content', block: true } ) - expect(await browser.elementByCss('h1').text()).toBe( - `Fallback params hub ${step}` - ) - await page.clock.setFixedTime(startDate + (index + 1) * 60_000) - - await act(async () => { - await act( - async () => { - await browser - .elementByCss(`input[data-link-accordion="${route}"]`) - .click() - await browser.elementByCss(`a[href="${route}"]`).click() - }, - { includes: 'Dynamic content', block: true } - ) - - // Hydration retains the static content of its actual prerender. A - // cold dynamic RSC render retains the required shell's unresolved - // bottom param. - expect(await browser.elementByCss('main').text()).toContain( - `Top: ${top}` - ) - expect(await browser.elementById('bottom-boundary').text()).toBe( - bottomIsStatic ? 'Bottom: b1' : 'Loading bottom...' - ) - expect(await browser.elementById('connection-boundary').text()).toBe( - 'Loading connection...' - ) - }) expect(await browser.elementById('top').text()).toBe(`Top: ${top}`) - expect(await browser.elementById('bottom').text()).toBe('Bottom: b1') - expect(await browser.elementById('dynamic-content').text()).toBe( - 'Dynamic content' + expect(await browser.elementById('bottom-boundary').text()).toBe( + 'Loading bottom...' ) - } + expect(await browser.elementById('connection-boundary').text()).toBe( + 'Loading connection...' + ) + }) + + expect(await browser.elementById('top').text()).toBe(`Top: ${top}`) + expect(await browser.elementById('bottom').text()).toBe('Bottom: b1') + expect(await browser.elementById('dynamic-content').text()).toBe( + 'Dynamic content' + ) } - ) + }) it('caches a fully static on-demand param for repeated navigations', async () => { const route = '/fully-static-params/t4' diff --git a/test/e2e/app-dir/segment-config-ts/segment-config-ts.test.ts b/test/e2e/app-dir/segment-config-ts/segment-config-ts.test.ts index e36a01a4540f..540f414b0d57 100644 --- a/test/e2e/app-dir/segment-config-ts/segment-config-ts.test.ts +++ b/test/e2e/app-dir/segment-config-ts/segment-config-ts.test.ts @@ -3,7 +3,6 @@ import { nextTestSetup } from 'e2e-utils' describe('TypeScript type expressions in route segment config', () => { const { next, isNextStart } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) describe('app directory', () => { diff --git a/test/e2e/app-dir/similar-pages-paths/similar-pages-paths.test.ts b/test/e2e/app-dir/similar-pages-paths/similar-pages-paths.test.ts index d5fe1af996a6..ad30c8800c78 100644 --- a/test/e2e/app-dir/similar-pages-paths/similar-pages-paths.test.ts +++ b/test/e2e/app-dir/similar-pages-paths/similar-pages-paths.test.ts @@ -1,15 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('app-dir similar pages paths', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should not have conflicts for similar pattern page paths between app and pages', async () => { // pages/page and app/page const res1 = await next.fetch('/') diff --git a/test/e2e/app-dir/trace-build-file/trace-build-file.test.ts b/test/e2e/app-dir/trace-build-file/trace-build-file.test.ts index 806f2da3c004..7c275d788adf 100644 --- a/test/e2e/app-dir/trace-build-file/trace-build-file.test.ts +++ b/test/e2e/app-dir/trace-build-file/trace-build-file.test.ts @@ -7,7 +7,6 @@ describe('trace-build-file', () => { const { next } = nextTestSetup({ files: __dirname, skipStart: !isNextDev, - skipDeployment: true, env: { // Enable persistent caching even when the git working directory is // dirty (e.g. when developing Next.js itself). Without this, the diff --git a/test/e2e/app-dir/turbopack-loader-content-type/turbopack-loader-content-type.test.ts b/test/e2e/app-dir/turbopack-loader-content-type/turbopack-loader-content-type.test.ts index 8d037e1668da..ee2b875cb526 100644 --- a/test/e2e/app-dir/turbopack-loader-content-type/turbopack-loader-content-type.test.ts +++ b/test/e2e/app-dir/turbopack-loader-content-type/turbopack-loader-content-type.test.ts @@ -1,13 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('turbopack-loader-content-type', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return - it('should apply loader based on contentType glob pattern', async () => { const $ = await next.render$('/') const text = $('#text').text() diff --git a/test/e2e/app-dir/turbopack-postcss-multiple-configs/turbopack-postcss-multiple-configs.test.ts b/test/e2e/app-dir/turbopack-postcss-multiple-configs/turbopack-postcss-multiple-configs.test.ts index 758a496d714c..0b938649011f 100644 --- a/test/e2e/app-dir/turbopack-postcss-multiple-configs/turbopack-postcss-multiple-configs.test.ts +++ b/test/e2e/app-dir/turbopack-postcss-multiple-configs/turbopack-postcss-multiple-configs.test.ts @@ -1,17 +1,14 @@ import { nextTestSetup } from 'e2e-utils' describe('turbopack-postcss-multiple-configs', () => { - const { next, isTurbopack, skipped } = nextTestSetup({ + const { next, isTurbopack } = nextTestSetup({ files: __dirname, // Per-directory PostCSS config resolution is a Turbopack-only feature // (turbopackLocalPostcssConfig). Webpack does not support this feature and // does not accept function-valued PostCSS plugins, so skip non-Turbopack runs. skipStart: true, - skipDeployment: true, }) - if (skipped) return - if (!isTurbopack) { it('should only run with Turbopack', () => {}) return diff --git a/test/e2e/app-dir/unauthorized/default/unauthorized-default.test.ts b/test/e2e/app-dir/unauthorized/default/unauthorized-default.test.ts index f1280f6f76ce..d3bbe09f0048 100644 --- a/test/e2e/app-dir/unauthorized/default/unauthorized-default.test.ts +++ b/test/e2e/app-dir/unauthorized/default/unauthorized-default.test.ts @@ -6,15 +6,10 @@ import { } from 'next-test-utils' describe('app dir - unauthorized with default unauthorized boundary', () => { - const { next, isNextDev, skipped } = nextTestSetup({ + const { next, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - // TODO: error unauthorized usage in root layout it.skip('should error on client unauthorized from root layout in browser', async () => { const browser = await next.browser('/') diff --git a/test/e2e/app-dir/use-cache-infinity-profile/use-cache-infinity-profile.test.ts b/test/e2e/app-dir/use-cache-infinity-profile/use-cache-infinity-profile.test.ts index 4ae67724464f..bbf9b525ec61 100644 --- a/test/e2e/app-dir/use-cache-infinity-profile/use-cache-infinity-profile.test.ts +++ b/test/e2e/app-dir/use-cache-infinity-profile/use-cache-infinity-profile.test.ts @@ -3,9 +3,6 @@ import { nextTestSetup } from 'e2e-utils' const uuidRegExp = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/ -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// Deployment platforms provide their own cache handlers. -// @force-gate !deploy describe('use-cache-infinity-profile', () => { const { next, isNextStart } = nextTestSetup({ files: __dirname, diff --git a/test/e2e/app-dir/use-cache-og-image-top-level-await/use-cache-og-image-top-level-await.test.ts b/test/e2e/app-dir/use-cache-og-image-top-level-await/use-cache-og-image-top-level-await.test.ts index cd8cc02e9162..5cd34efa05e7 100644 --- a/test/e2e/app-dir/use-cache-og-image-top-level-await/use-cache-og-image-top-level-await.test.ts +++ b/test/e2e/app-dir/use-cache-og-image-top-level-await/use-cache-og-image-top-level-await.test.ts @@ -1,9 +1,5 @@ import { nextTestSetup } from 'e2e-utils' -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// The prerendered output can't be observed in a deployment, and without -// it nothing distinguishes broken from fixed behavior. -// @force-gate !deploy describe('use-cache-og-image-top-level-await', () => { const { next, isNextStart } = nextTestSetup({ files: __dirname, diff --git a/test/e2e/app-dir/use-cache-output-export/use-cache-output-export.test.ts b/test/e2e/app-dir/use-cache-output-export/use-cache-output-export.test.ts index 3bb551cd31f2..4f5c3eabba70 100644 --- a/test/e2e/app-dir/use-cache-output-export/use-cache-output-export.test.ts +++ b/test/e2e/app-dir/use-cache-output-export/use-cache-output-export.test.ts @@ -3,9 +3,6 @@ import { renderViaHTTP, startCleanStaticServer } from 'next-test-utils' import { join } from 'path' import { AddressInfo, Server } from 'net' -// TODO(deploy-test-completion): Re-enable this suite in deploy mode. -// It likely asserts local CLI or runtime output that deploy tests do not expose. -// @force-gate !deploy describe('use-cache-output-export', () => { const { next, isNextStart } = nextTestSetup({ files: __dirname, diff --git a/test/e2e/app-dir/webpack-loader-binary/webpack-loader-binary.test.ts b/test/e2e/app-dir/webpack-loader-binary/webpack-loader-binary.test.ts index e00169d31bf3..380d461e06f0 100644 --- a/test/e2e/app-dir/webpack-loader-binary/webpack-loader-binary.test.ts +++ b/test/e2e/app-dir/webpack-loader-binary/webpack-loader-binary.test.ts @@ -1,13 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('webpack-loader-ts-transform', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return - it('should allow passing binary assets to and from a Webpack loader', async () => { const $ = await next.render$('/') expect($('#text').text()).toBe('Got a buffer of 18 bytes') diff --git a/test/e2e/app-dir/webpack-loader-conditions/webpack-loader-conditions.test.ts b/test/e2e/app-dir/webpack-loader-conditions/webpack-loader-conditions.test.ts index 52f00ef05b41..ad87cb79239e 100644 --- a/test/e2e/app-dir/webpack-loader-conditions/webpack-loader-conditions.test.ts +++ b/test/e2e/app-dir/webpack-loader-conditions/webpack-loader-conditions.test.ts @@ -4,13 +4,10 @@ import { nextTestSetup } from 'e2e-utils' ;(process.env.IS_TURBOPACK_TEST ? describe : describe.skip)( 'webpack-loader-conditions', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return - it('should render correctly on server site', async () => { const res = await next.fetch('/') const html = (await res.text()).replaceAll(/<!-- -->/g, '') diff --git a/test/e2e/app-dir/webpack-loader-fs/webpack-loader-fs.test.ts b/test/e2e/app-dir/webpack-loader-fs/webpack-loader-fs.test.ts index f247c44ade27..60d395d40033 100644 --- a/test/e2e/app-dir/webpack-loader-fs/webpack-loader-fs.test.ts +++ b/test/e2e/app-dir/webpack-loader-fs/webpack-loader-fs.test.ts @@ -1,13 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('webpack-loader-fs', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return - it('should allow reading the input FS', async () => { const $ = await next.render$('/') expect($('#test').text()).toBe( diff --git a/test/e2e/app-dir/webpack-loader-import-module/webpack-loader-import-module.test.ts b/test/e2e/app-dir/webpack-loader-import-module/webpack-loader-import-module.test.ts index d2f9a7ddf9b3..c98c946cd1b6 100644 --- a/test/e2e/app-dir/webpack-loader-import-module/webpack-loader-import-module.test.ts +++ b/test/e2e/app-dir/webpack-loader-import-module/webpack-loader-import-module.test.ts @@ -1,15 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('webpack-loader-import-module', () => { - const { next, skipped, isTurbopack } = nextTestSetup({ + const { next, isTurbopack } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should support this.importModule() in a webpack loader', async () => { const $ = await next.render$('/') expect($('#title').text()).toBe('Import Module Works') diff --git a/test/e2e/app-dir/webpack-loader-module-type/webpack-loader-module-type.test.ts b/test/e2e/app-dir/webpack-loader-module-type/webpack-loader-module-type.test.ts index a63afb00be9e..31988b6b7e7c 100644 --- a/test/e2e/app-dir/webpack-loader-module-type/webpack-loader-module-type.test.ts +++ b/test/e2e/app-dir/webpack-loader-module-type/webpack-loader-module-type.test.ts @@ -1,13 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('webpack-loader-module-type', () => { - const { next, isTurbopack, skipped } = nextTestSetup({ + const { next, isTurbopack } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return - // bytes type is Turbopack-only, webpack doesn't have a direct equivalent const itTurbopackOnly = isTurbopack ? it : it.skip diff --git a/test/e2e/app-dir/webpack-loader-resolve/webpack-loader-resolve.test.ts b/test/e2e/app-dir/webpack-loader-resolve/webpack-loader-resolve.test.ts index d6e671eba40f..f4bb205927b2 100644 --- a/test/e2e/app-dir/webpack-loader-resolve/webpack-loader-resolve.test.ts +++ b/test/e2e/app-dir/webpack-loader-resolve/webpack-loader-resolve.test.ts @@ -1,16 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('webpack-loader-resolve', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - // This test is skipped because it's only expected to run in turbopack, which isn't enabled for builds - skipDeployment: true, }) - if (skipped) { - return - } - it('should support resolving absolute path via loader getResolve', async () => { const $ = await next.render$('/') expect($('#absolute').text()).toBe('abc') diff --git a/test/e2e/app-dir/webpack-loader-resource-query/webpack-loader-resource-query.test.js b/test/e2e/app-dir/webpack-loader-resource-query/webpack-loader-resource-query.test.js index 6a86fffdc412..247a6ad44d1d 100644 --- a/test/e2e/app-dir/webpack-loader-resource-query/webpack-loader-resource-query.test.js +++ b/test/e2e/app-dir/webpack-loader-resource-query/webpack-loader-resource-query.test.js @@ -1,13 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('webpack-loader-resource-query', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return - it('should pass query to loader', async () => { await next.render$('/') diff --git a/test/e2e/app-dir/webpack-loader-ts-transform/webpack-loader-ts-transform.test.ts b/test/e2e/app-dir/webpack-loader-ts-transform/webpack-loader-ts-transform.test.ts index fa1a67bd32a4..ca995abd2da4 100644 --- a/test/e2e/app-dir/webpack-loader-ts-transform/webpack-loader-ts-transform.test.ts +++ b/test/e2e/app-dir/webpack-loader-ts-transform/webpack-loader-ts-transform.test.ts @@ -1,14 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('webpack-loader-ts-transform', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - // This test is skipped because it's only expected to run in turbopack, which isn't enabled for builds - skipDeployment: true, }) - if (skipped) return - it('should accept Typescript returned from Webpack loaders', async () => { const $ = await next.render$('/') expect($('p').text()).toBe('something') diff --git a/test/e2e/app-dir/with-babel/with-babel.test.ts b/test/e2e/app-dir/with-babel/with-babel.test.ts index 11d143c4825a..7a6310aa2d0b 100644 --- a/test/e2e/app-dir/with-babel/with-babel.test.ts +++ b/test/e2e/app-dir/with-babel/with-babel.test.ts @@ -2,15 +2,10 @@ import { nextTestSetup } from 'e2e-utils' import { retry } from 'next-test-utils' describe('with babel', () => { - const { next, isNextStart, isTurbopack, skipped } = nextTestSetup({ + const { next, isNextStart, isTurbopack } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should support babel in app dir', async () => { const $ = await next.render$('/') expect($('h1').text()).toBe('hello') diff --git a/test/e2e/app-document/client.test.ts b/test/e2e/app-document/client.test.ts index 5b68d879f413..49d6096bca93 100644 --- a/test/e2e/app-document/client.test.ts +++ b/test/e2e/app-document/client.test.ts @@ -4,7 +4,6 @@ import { nextTestSetup } from 'e2e-utils' describe('Document and App - Client side', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) it('should share module state with pages', async () => { diff --git a/test/e2e/config-schema-check/index.test.ts b/test/e2e/config-schema-check/index.test.ts index 9e483ae0dcb3..3f58f078b36b 100644 --- a/test/e2e/config-schema-check/index.test.ts +++ b/test/e2e/config-schema-check/index.test.ts @@ -3,7 +3,7 @@ import { nextTestSetup } from 'e2e-utils' import { check } from 'next-test-utils' describe('next.config.js schema validating - defaultConfig', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: { 'pages/index.js': ` export default function Page() { @@ -16,13 +16,8 @@ describe('next.config.js schema validating - defaultConfig', () => { } `, }, - skipDeployment: true, }) - if (skipped) { - return - } - it('should validate against defaultConfig', async () => { const output = stripAnsi(next.cliOutput) @@ -31,7 +26,7 @@ describe('next.config.js schema validating - defaultConfig', () => { }) describe('next.config.js schema validating - invalid config', () => { - const { next, isNextStart, skipped } = nextTestSetup({ + const { next, isNextStart } = nextTestSetup({ files: { 'pages/index.js': ` export default function Page() { @@ -44,13 +39,8 @@ describe('next.config.js schema validating - invalid config', () => { } `, }, - skipDeployment: true, }) - if (skipped) { - return - } - it('should warn the invalid next config', async () => { await check(() => { const output = stripAnsi(next.cliOutput) diff --git a/test/e2e/disable-js/disable-js.test.ts b/test/e2e/disable-js/disable-js.test.ts index 21eb2905f493..925a715c309f 100644 --- a/test/e2e/disable-js/disable-js.test.ts +++ b/test/e2e/disable-js/disable-js.test.ts @@ -2,11 +2,9 @@ import { nextTestSetup } from 'e2e-utils' import cheerio from 'cheerio' describe('disabled runtime JS', () => { - const { next, isNextDev, isNextStart, skipped } = nextTestSetup({ + const { next, isNextDev, isNextStart } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return it('should render the page', async () => { const html = await next.render('/') diff --git a/test/e2e/edge-runtime-uses-edge-light-import-specifier-for-packages/edge-runtime-uses-edge-light-import-specifier-for-packages.test.ts b/test/e2e/edge-runtime-uses-edge-light-import-specifier-for-packages/edge-runtime-uses-edge-light-import-specifier-for-packages.test.ts index 98455dcb8119..a861852cc5d0 100644 --- a/test/e2e/edge-runtime-uses-edge-light-import-specifier-for-packages/edge-runtime-uses-edge-light-import-specifier-for-packages.test.ts +++ b/test/e2e/edge-runtime-uses-edge-light-import-specifier-for-packages/edge-runtime-uses-edge-light-import-specifier-for-packages.test.ts @@ -1,7 +1,7 @@ import { nextTestSetup } from 'e2e-utils' describe('edge-runtime uses edge-light import specifier for packages', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, packageJson: { scripts: { @@ -13,13 +13,8 @@ describe('edge-runtime uses edge-light import specifier for packages', () => { installCommand: 'pnpm i', startCommand: (global as any).isNextDev ? 'pnpm dev' : 'pnpm start', buildCommand: 'pnpm build', - skipDeployment: true, }) - if (skipped) { - return - } - // In case you need to test the response object it('pages/api endpoints import the correct module', async () => { const res = await next.fetch('/api/edge') diff --git a/test/e2e/gip-identifier/gip-identifier.test.ts b/test/e2e/gip-identifier/gip-identifier.test.ts index 2abc71ec9081..061c3cce6e6d 100644 --- a/test/e2e/gip-identifier/gip-identifier.test.ts +++ b/test/e2e/gip-identifier/gip-identifier.test.ts @@ -3,11 +3,9 @@ import { retry } from 'next-test-utils' import cheerio from 'cheerio' describe('gip identifiers', () => { - const { next, isNextDev, skipped } = nextTestSetup({ + const { next, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return const getNextData = async () => { const html = await next.render('/') diff --git a/test/e2e/i18n-data-fetching-redirect/redirect-from-context.test.ts b/test/e2e/i18n-data-fetching-redirect/redirect-from-context.test.ts index 012c662f82fa..8e6bb0b456c9 100644 --- a/test/e2e/i18n-data-fetching-redirect/redirect-from-context.test.ts +++ b/test/e2e/i18n-data-fetching-redirect/redirect-from-context.test.ts @@ -3,12 +3,6 @@ import { FileRef, nextTestSetup } from 'e2e-utils' import { check } from 'next-test-utils' describe('i18n-data-fetching-redirect', () => { - // TODO: investigate tests failures on deploy - if ((global as any).isNextDeploy) { - it('should skip temporarily', () => {}) - return - } - const { next } = nextTestSetup({ files: { pages: new FileRef(join(__dirname, 'app/pages')), diff --git a/test/e2e/i18n-ignore-rewrite-source-locale/rewrites-with-basepath.test.ts b/test/e2e/i18n-ignore-rewrite-source-locale/rewrites-with-basepath.test.ts index ce28cb548de0..cb9934ce6224 100644 --- a/test/e2e/i18n-ignore-rewrite-source-locale/rewrites-with-basepath.test.ts +++ b/test/e2e/i18n-ignore-rewrite-source-locale/rewrites-with-basepath.test.ts @@ -6,15 +6,10 @@ import fs from 'fs-extra' const locales = ['', '/en', '/sv', '/nl'] describe('i18n-ignore-rewrite-source-locale with basepath', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - test.each(locales)( 'get public file by skipping locale in rewrite, locale: %s', async (locale) => { diff --git a/test/e2e/image-optimizer/image-optimizer.test.ts b/test/e2e/image-optimizer/image-optimizer.test.ts index cda4b5eb996f..f7436ca971e4 100644 --- a/test/e2e/image-optimizer/image-optimizer.test.ts +++ b/test/e2e/image-optimizer/image-optimizer.test.ts @@ -263,7 +263,7 @@ describe('Image Optimizer', () => { } }) describe('Server support for trailingSlash in next.config.js', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: join(__dirname, 'app'), nextConfig: { trailingSlash: true, @@ -272,9 +272,7 @@ describe('Image Optimizer', () => { qualities: [70, 75], }, }, - skipDeployment: true, }) - if (skipped) return it('should return successful response for original loader', async () => { const query = { url: '/test.png', w: 8, q: 70 } diff --git a/test/e2e/import-meta-env/import-meta-env.test.ts b/test/e2e/import-meta-env/import-meta-env.test.ts index f76acb0d266e..63760c07eca9 100644 --- a/test/e2e/import-meta-env/import-meta-env.test.ts +++ b/test/e2e/import-meta-env/import-meta-env.test.ts @@ -6,13 +6,10 @@ const testFn = : describe testFn('import.meta.env', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return - it('exposes built-in environment values on the server and client', async () => { const browser = await next.browser('/docs') const expectedMode = isNextDev ? 'development' : 'production' diff --git a/test/e2e/import-meta-glob/import-meta-glob.test.ts b/test/e2e/import-meta-glob/import-meta-glob.test.ts index 412894f72192..738ffda4f54c 100644 --- a/test/e2e/import-meta-glob/import-meta-glob.test.ts +++ b/test/e2e/import-meta-glob/import-meta-glob.test.ts @@ -7,13 +7,10 @@ const testFn = : describe testFn('import-meta-glob', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return - it('should resolve lazy glob modules', async () => { const $ = await next.render$('/') const lazyKeys = JSON.parse($('#lazy-keys').text()) diff --git a/test/e2e/jsconfig-baseurl/jsconfig-baseurl.test.ts b/test/e2e/jsconfig-baseurl/jsconfig-baseurl.test.ts index 25a3b5138e03..667c1ddeca74 100644 --- a/test/e2e/jsconfig-baseurl/jsconfig-baseurl.test.ts +++ b/test/e2e/jsconfig-baseurl/jsconfig-baseurl.test.ts @@ -3,11 +3,9 @@ import stripAnsi from 'next/dist/compiled/strip-ansi' import { retry } from 'next-test-utils' describe('jsconfig.json baseurl', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return describe('default behavior', () => { it('should render the page', async () => { diff --git a/test/e2e/legacy-link-behavior/index.test.ts b/test/e2e/legacy-link-behavior/index.test.ts index c34c5a3543a3..751da05fe77d 100644 --- a/test/e2e/legacy-link-behavior/index.test.ts +++ b/test/e2e/legacy-link-behavior/index.test.ts @@ -6,15 +6,10 @@ import { } from '../../lib/add-redbox-matchers' describe('Link with legacyBehavior', () => { - const { next, isNextDev, skipped } = nextTestSetup({ + const { next, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return it('should skip', () => {}) - } - describe('if the child is an <a> tag', () => { it('forwards the href attribute', async () => { const $ = await next.render$('/') diff --git a/test/e2e/middleware-custom-matchers/test/index.test.ts b/test/e2e/middleware-custom-matchers/test/index.test.ts index 6ed8cf2a36bf..9733868cca57 100644 --- a/test/e2e/middleware-custom-matchers/test/index.test.ts +++ b/test/e2e/middleware-custom-matchers/test/index.test.ts @@ -159,22 +159,17 @@ describe('Middleware custom matchers', () => { } ) - itif(!isModeDeploy)( - 'should match has cookie on client routing', - async () => { - const browser = await next.browser('/routes') - await browser.addCookie({ name: 'loggedIn', value: 'true' }) - await browser.refresh() - await browser.eval('window.__TEST_NO_RELOAD = true') - await browser.elementById('has-match-3').click() - const fromMiddleware = await browser - .elementById('from-middleware') - .text() - expect(fromMiddleware).toBe('true') - const noReload = await browser.eval('window.__TEST_NO_RELOAD') - expect(noReload).toBe(true) - } - ) + it('should match has cookie on client routing', async () => { + const browser = await next.browser('/routes') + await browser.addCookie({ name: 'loggedIn', value: 'true' }) + await browser.refresh() + await browser.eval('window.__TEST_NO_RELOAD = true') + await browser.elementById('has-match-3').click() + const fromMiddleware = await browser.elementById('from-middleware').text() + expect(fromMiddleware).toBe('true') + const noReload = await browser.eval('window.__TEST_NO_RELOAD') + expect(noReload).toBe(true) + }) } runTests() }) diff --git a/test/e2e/next-image-legacy/default/default-static.test.ts b/test/e2e/next-image-legacy/default/default-static.test.ts index d2e0d6b520ec..a088d6190aa2 100644 --- a/test/e2e/next-image-legacy/default/default-static.test.ts +++ b/test/e2e/next-image-legacy/default/default-static.test.ts @@ -44,11 +44,9 @@ describe('Build Error Tests', () => { }) describe('Static Image Component Tests', () => { - const { next, isTurbopack, skipped } = nextTestSetup({ + const { next, isTurbopack } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return let browser: Playwright let html: string diff --git a/test/e2e/next-image-svgo-webpack/svgo-webpack.test.ts b/test/e2e/next-image-svgo-webpack/svgo-webpack.test.ts index 4b03f9e7651c..63182ce879f2 100644 --- a/test/e2e/next-image-svgo-webpack/svgo-webpack.test.ts +++ b/test/e2e/next-image-svgo-webpack/svgo-webpack.test.ts @@ -3,7 +3,6 @@ import { nextTestSetup } from 'e2e-utils' describe('svgo-webpack loader', () => { const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, dependencies: { '@svgr/webpack': '8.1.0', }, diff --git a/test/e2e/next-link-errors/next-link-errors.test.ts b/test/e2e/next-link-errors/next-link-errors.test.ts index 852842a6ba71..a13b0d7b07e8 100644 --- a/test/e2e/next-link-errors/next-link-errors.test.ts +++ b/test/e2e/next-link-errors/next-link-errors.test.ts @@ -1,13 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('next-link', () => { - const { skipped, next, isNextDev } = nextTestSetup({ + const { next, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return - it('errors on invalid href', async () => { const browser = await next.browser('/invalid-href') diff --git a/test/e2e/on-request-error/skip-next-internal-error/skip-next-internal-error.test.ts b/test/e2e/on-request-error/skip-next-internal-error/skip-next-internal-error.test.ts index b34f2e4a1c7c..ae27c6a2d414 100644 --- a/test/e2e/on-request-error/skip-next-internal-error/skip-next-internal-error.test.ts +++ b/test/e2e/on-request-error/skip-next-internal-error/skip-next-internal-error.test.ts @@ -1,15 +1,10 @@ import { nextTestSetup } from 'e2e-utils' describe('on-request-error - skip-next-internal-error', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - async function assertNoNextjsInternalErrors() { const output = next.cliOutput // No navigation errors diff --git a/test/e2e/pages-performance-mark/index.test.ts b/test/e2e/pages-performance-mark/index.test.ts index 72f7e31dc726..ca591c192cdb 100644 --- a/test/e2e/pages-performance-mark/index.test.ts +++ b/test/e2e/pages-performance-mark/index.test.ts @@ -3,15 +3,10 @@ import { nextTestSetup } from 'e2e-utils' // This test case doesn't indicate rendering duplicate head in _document is valid, // but it's a way to reproduce the performance mark crashing. describe('pages performance mark', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - it('should render the page correctly without crashing with performance mark', async () => { const browser = await next.browser('/') expect(await browser.elementByCss('h1').text()).toBe('home') diff --git a/test/e2e/styled-jsx-dynamic/index.test.ts b/test/e2e/styled-jsx-dynamic/index.test.ts index adbd235b296a..2b03d6b897de 100644 --- a/test/e2e/styled-jsx-dynamic/index.test.ts +++ b/test/e2e/styled-jsx-dynamic/index.test.ts @@ -3,7 +3,6 @@ import { nextTestSetup } from 'e2e-utils' describe('styled-jsx dynamic styles SSR', () => { const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) // Dynamic styled-jsx (with interpolated expressions) produces numeric class diff --git a/test/e2e/styled-jsx/index.test.ts b/test/e2e/styled-jsx/index.test.ts index 03a37f410767..76388ca1d0fa 100644 --- a/test/e2e/styled-jsx/index.test.ts +++ b/test/e2e/styled-jsx/index.test.ts @@ -1,18 +1,13 @@ import { nextTestSetup } from 'e2e-utils' describe('styled-jsx', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, dependencies: { 'styled-jsx': '5.0.0', // styled-jsx on user side }, }) - if (skipped) { - return - } - it('should contain styled-jsx styles during SSR', async () => { const html = await next.render('/') expect(html).toMatch(/color:.*?red/) diff --git a/test/e2e/swc-plugins-env/index.test.ts b/test/e2e/swc-plugins-env/index.test.ts index dce816d5914b..86e4c9c2e3f9 100644 --- a/test/e2e/swc-plugins-env/index.test.ts +++ b/test/e2e/swc-plugins-env/index.test.ts @@ -1,11 +1,9 @@ import { nextTestSetup } from 'e2e-utils' describe('swc-plugins-env', () => { - const { next, skipped, isNextDev } = nextTestSetup({ + const { next, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) return it('should pass correct environment to swc plugins', async () => { const $ = await next.render$('/') diff --git a/test/e2e/swc-plugins/index.test.ts b/test/e2e/swc-plugins/index.test.ts index ca78dc13c7c5..fcbafdeaacdb 100644 --- a/test/e2e/swc-plugins/index.test.ts +++ b/test/e2e/swc-plugins/index.test.ts @@ -2,14 +2,12 @@ import { nextTestSetup, isNextDev } from 'e2e-utils' describe('swcPlugins', () => { describe('supports swcPlugins', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, dependencies: { '@swc/plugin-react-remove-properties': '13.0.0', }, }) - if (skipped) return it('basic case', async () => { const html = await next.render('/') diff --git a/test/e2e/transpile-packages-typescript-foreign/index.test.ts b/test/e2e/transpile-packages-typescript-foreign/index.test.ts index f99e91a8b918..4572e28bf856 100644 --- a/test/e2e/transpile-packages-typescript-foreign/index.test.ts +++ b/test/e2e/transpile-packages-typescript-foreign/index.test.ts @@ -39,9 +39,8 @@ Module parse failed: Unexpected token`) }) describe('with transpilePackages', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, dependencies: { pkg: `file:./pkg`, }, @@ -50,10 +49,6 @@ Module parse failed: Unexpected token`) }, }) - if (skipped) { - return - } - it('should work', async () => { const $ = await next.render$('/') expect($('main').text()).toEqual('Hello 123') diff --git a/test/e2e/turbopack-import-with-type/index.test.ts b/test/e2e/turbopack-import-with-type/index.test.ts index de628934f21f..2aa4e5bd33be 100644 --- a/test/e2e/turbopack-import-with-type/index.test.ts +++ b/test/e2e/turbopack-import-with-type/index.test.ts @@ -8,15 +8,10 @@ throw new Error('please dont execute me') ;(process.env.IS_TURBOPACK_TEST ? describe : describe.skip)( 'turbopack-import-with-type', () => { - const { next, skipped } = nextTestSetup({ + const { next } = nextTestSetup({ files: __dirname, - skipDeployment: true, }) - if (skipped) { - return - } - // Testing this together on one route ensures we also avoid weird duplicate module ident things it('supports import with type: text, type: bytes, and type: json', async () => { const response = JSON.parse(await next.render('/api')) diff --git a/test/e2e/turbopack-loader-config/index.test.ts b/test/e2e/turbopack-loader-config/index.test.ts index 9eae254ef6f3..d149c30a80a7 100644 --- a/test/e2e/turbopack-loader-config/index.test.ts +++ b/test/e2e/turbopack-loader-config/index.test.ts @@ -1,17 +1,12 @@ import { nextTestSetup } from 'e2e-utils' describe('turbopack-loader-config', () => { - const { next, isTurbopack, isNextDev, skipped } = nextTestSetup({ + const { next, isTurbopack, isNextDev } = nextTestSetup({ files: __dirname, - skipDeployment: true, // we can't set `nextConfig` inline because it contains regexes that fail to serialize, it needs // to be set in a separate module (`next.config.ts`) }) - if (skipped) { - return - } - if (!isTurbopack) { it('should only run the test in turbopack', () => {}) return diff --git a/test/e2e/typescript/typescript.test.ts b/test/e2e/typescript/typescript.test.ts index 63a265b2d5ed..56bfccb616ba 100644 --- a/test/e2e/typescript/typescript.test.ts +++ b/test/e2e/typescript/typescript.test.ts @@ -1,14 +1,12 @@ import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' describe('TypeScript Features', () => { - const { next, isTurbopack, skipped } = nextTestSetup({ + const { next, isTurbopack } = nextTestSetup({ files: __dirname, dependencies: { sass: 'latest', }, - skipDeployment: true, }) - if (skipped) return it('should render the page', async () => { const $ = await next.render$('/hello') diff --git a/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-cache-components.test.ts b/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-cache-components.test.ts index 6999f9be4de9..68d9080aacb0 100644 --- a/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-cache-components.test.ts +++ b/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-cache-components.test.ts @@ -44,32 +44,32 @@ describe('adapter dynamic routes (cache components)', () => { "7 entries /[lang] - ^[/]?/(?<nxtPlang>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /[lang]$rscSuffix?nxtPlang=$nxtPlang + ^[/]?/(?<nxtPlang>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /[lang]$2?nxtPlang=$nxtPlang - /$shellPrefix/fallback-shell/[slug] - ^[/]?/(?<shellPrefix>de|en)/fallback\\-shell/(?<nxtPslug>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /$shellPrefix/fallback-shell/[slug]$rscSuffix?nxtPslug=$nxtPslug + /$1/fallback-shell/[slug] + ^[/]?/(de|en)/fallback\\-shell/(?<nxtPslug>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /$1/fallback-shell/[slug]$3?nxtPslug=$nxtPslug /[lang]/fallback-shell/[slug] - ^[/]?/(?<nxtPlang>[^/]+?)/fallback\\-shell/(?<nxtPslug>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /[lang]/fallback-shell/[slug]$rscSuffix?nxtPlang=$nxtPlang&nxtPslug=$nxtPslug + ^[/]?/(?<nxtPlang>[^/]+?)/fallback\\-shell/(?<nxtPslug>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /[lang]/fallback-shell/[slug]$3?nxtPlang=$nxtPlang&nxtPslug=$nxtPslug /[lang]/ppr - ^[/]?/(?<nxtPlang>[^/]+?)/ppr(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /[lang]/ppr$rscSuffix?nxtPlang=$nxtPlang + ^[/]?/(?<nxtPlang>[^/]+?)/ppr(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /[lang]/ppr$2?nxtPlang=$nxtPlang /[lang]/static - ^[/]?/(?<nxtPlang>[^/]+?)/static(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /[lang]/static$rscSuffix?nxtPlang=$nxtPlang + ^[/]?/(?<nxtPlang>[^/]+?)/static(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /[lang]/static$2?nxtPlang=$nxtPlang - /$shellPrefix/[slug] - ^[/]?/(?<shellPrefix>de|en)/(?<nxtPslug>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /$shellPrefix/[slug]$rscSuffix?nxtPslug=$nxtPslug + /$1/[slug] + ^[/]?/(de|en)/(?<nxtPslug>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /$1/[slug]$3?nxtPslug=$nxtPslug /[lang]/[slug] - ^[/]?/(?<nxtPlang>[^/]+?)/(?<nxtPslug>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /[lang]/[slug]$rscSuffix?nxtPlang=$nxtPlang&nxtPslug=$nxtPslug" + ^[/]?/(?<nxtPlang>[^/]+?)/(?<nxtPslug>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /[lang]/[slug]$3?nxtPlang=$nxtPlang&nxtPslug=$nxtPslug" `) }) }) diff --git a/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-legacy.test.ts b/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-legacy.test.ts index 445ec8127724..ed1cae823dae 100644 --- a/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-legacy.test.ts +++ b/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-legacy.test.ts @@ -56,24 +56,24 @@ describe('adapter dynamic routes (legacy)', () => { -> /static-two /blog/[slug] - ^[/]?/blog/(?<nxtPslug>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /blog/[slug]$rscSuffix?nxtPslug=$nxtPslug + ^[/]?/blog/(?<nxtPslug>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /blog/[slug]$2?nxtPslug=$nxtPslug /docs/[lang]/accounts - ^[/]?/docs/(?<nxtPlang>[^/]+?)/accounts(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /docs/[lang]/accounts$rscSuffix?nxtPlang=$nxtPlang + ^[/]?/docs/(?<nxtPlang>[^/]+?)/accounts(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /docs/[lang]/accounts$2?nxtPlang=$nxtPlang /docs/[lang]/functions - ^[/]?/docs/(?<nxtPlang>[^/]+?)/functions(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /docs/[lang]/functions$rscSuffix?nxtPlang=$nxtPlang + ^[/]?/docs/(?<nxtPlang>[^/]+?)/functions(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /docs/[lang]/functions$2?nxtPlang=$nxtPlang /docs/[lang]/guide - ^[/]?/docs/(?<nxtPlang>[^/]+?)/guide(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /docs/[lang]/guide$rscSuffix?nxtPlang=$nxtPlang + ^[/]?/docs/(?<nxtPlang>[^/]+?)/guide(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /docs/[lang]/guide$2?nxtPlang=$nxtPlang /legacy/[id].rsc - ^[/]?/legacy/(?<nxtPid>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc)(?:/)?$ - -> /legacy/[id]$rscSuffix?nxtPid=$nxtPid + ^[/]?/legacy/(?<nxtPid>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc)(?:/)?$ + -> /legacy/[id]$2?nxtPid=$nxtPid /legacy/[id] ^[/]?/legacy/(?<nxtPid>[^/]+?)(?:/)?$ diff --git a/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-no-root-params.test.ts b/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-no-root-params.test.ts index 2cc553a0d35c..971c25febbbd 100644 --- a/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-no-root-params.test.ts +++ b/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-no-root-params.test.ts @@ -44,25 +44,25 @@ describe('adapter dynamic routes (no root params)', () => { .toMatchInlineSnapshot(` "5 entries - /$shellPrefix/posts/[id] - ^[/]?/(?<shellPrefix>acme\\.one\\-two,three/de|acme\\.one\\-two,three/en)/posts/(?<nxtPid>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /$shellPrefix/posts/[id]$rscSuffix?nxtPid=$nxtPid + /$1/posts/[id] + ^[/]?/(acme\\.one\\-two,three/de|acme\\.one\\-two,three/en)/posts/(?<nxtPid>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /$1/posts/[id]$3?nxtPid=$nxtPid /acme.one-two,three/[locale]/posts/[id] - ^[/]?/acme\\.one\\-two,three/(?<nxtPlocale>[^/]+?)/posts/(?<nxtPid>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /acme.one-two,three/[locale]/posts/[id]$rscSuffix?nxtPlocale=$nxtPlocale&nxtPid=$nxtPid + ^[/]?/acme\\.one\\-two,three/(?<nxtPlocale>[^/]+?)/posts/(?<nxtPid>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /acme.one-two,three/[locale]/posts/[id]$3?nxtPlocale=$nxtPlocale&nxtPid=$nxtPid /sparse/en/posts/[id] - ^[/]?/sparse/en/posts/(?<nxtPid>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /sparse/en/posts/[id]$rscSuffix?nxtPid=$nxtPid + ^[/]?/sparse/en/posts/(?<nxtPid>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /sparse/en/posts/[id]$2?nxtPid=$nxtPid /sparse/[locale]/posts/[id] - ^[/]?/sparse/(?<nxtPlocale>[^/]+?)/posts/(?<nxtPid>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /sparse/[locale]/posts/[id]$rscSuffix?nxtPlocale=$nxtPlocale&nxtPid=$nxtPid + ^[/]?/sparse/(?<nxtPlocale>[^/]+?)/posts/(?<nxtPid>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /sparse/[locale]/posts/[id]$3?nxtPlocale=$nxtPlocale&nxtPid=$nxtPid /[team]/[locale]/posts/[id] - ^[/]?/(?<nxtPteam>[^/]+?)/(?<nxtPlocale>[^/]+?)/posts/(?<nxtPid>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /[team]/[locale]/posts/[id]$rscSuffix?nxtPteam=$nxtPteam&nxtPlocale=$nxtPlocale&nxtPid=$nxtPid" + ^[/]?/(?<nxtPteam>[^/]+?)/(?<nxtPlocale>[^/]+?)/posts/(?<nxtPid>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /[team]/[locale]/posts/[id]$4?nxtPteam=$nxtPteam&nxtPlocale=$nxtPlocale&nxtPid=$nxtPid" `) }) }) diff --git a/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-shell-prefixes-single.test.ts b/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-shell-prefixes-single.test.ts index 2d7c58651208..73897325813c 100644 --- a/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-shell-prefixes-single.test.ts +++ b/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-shell-prefixes-single.test.ts @@ -29,12 +29,12 @@ describe('adapter dynamic routes (shell prefixes, one combination)', () => { "2 entries /acme.one-two,three/en/posts/[id] - ^[/]?/acme\\.one\\-two,three/en/posts/(?<nxtPid>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /acme.one-two,three/en/posts/[id]$rscSuffix?nxtPid=$nxtPid + ^[/]?/acme\\.one\\-two,three/en/posts/(?<nxtPid>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /acme.one-two,three/en/posts/[id]$2?nxtPid=$nxtPid /[team]/[locale]/posts/[id] - ^[/]?/(?<nxtPteam>[^/]+?)/(?<nxtPlocale>[^/]+?)/posts/(?<nxtPid>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /[team]/[locale]/posts/[id]$rscSuffix?nxtPteam=$nxtPteam&nxtPlocale=$nxtPlocale&nxtPid=$nxtPid" + ^[/]?/(?<nxtPteam>[^/]+?)/(?<nxtPlocale>[^/]+?)/posts/(?<nxtPid>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /[team]/[locale]/posts/[id]$4?nxtPteam=$nxtPteam&nxtPlocale=$nxtPlocale&nxtPid=$nxtPid" `) }) }) diff --git a/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-shell-prefixes.test.ts b/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-shell-prefixes.test.ts index 5135f84e59a3..664f358c4993 100644 --- a/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-shell-prefixes.test.ts +++ b/test/production/app-dir/adapter-dynamic-routes/dynamic-routes-shell-prefixes.test.ts @@ -31,13 +31,13 @@ describe('adapter dynamic routes (shell prefixes)', () => { .toMatchInlineSnapshot(` "2 entries - /$shellPrefix/posts/[id] - ^[/]?/(?<shellPrefix>acme\\.one\\-two,three/de|acme\\.one\\-two,three/en|sparse/en)/posts/(?<nxtPid>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /$shellPrefix/posts/[id]$rscSuffix?nxtPid=$nxtPid + /$1/posts/[id] + ^[/]?/(acme\\.one\\-two,three/de|acme\\.one\\-two,three/en|sparse/en)/posts/(?<nxtPid>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /$1/posts/[id]$3?nxtPid=$nxtPid /[team]/[locale]/posts/[id] - ^[/]?/(?<nxtPteam>[^/]+?)/(?<nxtPlocale>[^/]+?)/posts/(?<nxtPid>[^/]+?)(?<rscSuffix>\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ - -> /[team]/[locale]/posts/[id]$rscSuffix?nxtPteam=$nxtPteam&nxtPlocale=$nxtPlocale&nxtPid=$nxtPid" + ^[/]?/(?<nxtPteam>[^/]+?)/(?<nxtPlocale>[^/]+?)/posts/(?<nxtPid>[^/]+?)(\\.rsc|\\.segments/.+\\.segment\\.rsc|)(?:/)?$ + -> /[team]/[locale]/posts/[id]$4?nxtPteam=$nxtPteam&nxtPlocale=$nxtPlocale&nxtPid=$nxtPid" `) }) }) diff --git a/test/production/deterministic-build/deployment-id.test.ts b/test/production/deterministic-build/deployment-id.test.ts index 5d1f80ef9ed1..4577ae43d6a8 100644 --- a/test/production/deterministic-build/deployment-id.test.ts +++ b/test/production/deterministic-build/deployment-id.test.ts @@ -222,11 +222,9 @@ async function runTest( }) describe.each([ - { test: 'standard', mode: 'builder' } as const, - { test: 'standard', mode: 'adapter' } as const, - { test: 'cacheComponents', mode: 'builder' } as const, - { test: 'cacheComponents', mode: 'adapter' } as const, - ])('build output API - $test $mode', ({ test, mode }) => { + { test: 'standard' } as const, + { test: 'cacheComponents' } as const, + ])('build output API - $test adapter', ({ test }) => { const { next } = nextTestSetup({ files: { // A mock file to be able to run `vercel build` without logging in @@ -243,43 +241,33 @@ async function runTest( // We use NEXT_TEST_PREFER_OFFLINE, so just declaring `vercel: latest` as a dependency still // doesn't force the latest version. buildCommand: 'pnpm dlx vercel@latest build', - env: - mode === 'adapter' - ? { - NEXT_ENABLE_ADAPTER: '1', - } - : undefined, + env: { + NEXT_ENABLE_ADAPTER: '1', + }, skipStart: true, disableAutoSkewProtection: true, }) - it( - 'should produce identical build outputs even when changing deployment id', - async () => { - let { run1, run2 } = await runTest(next, readFilesBuilder) + it('should produce identical build outputs even when changing deployment id', async () => { + let { run1, run2 } = await runTest(next, readFilesBuilder) - expect(run1.size).toBeGreaterThan(0) - expect([...run1.keys()]).toEqual([...run2.keys()]) + expect(run1.size).toBeGreaterThan(0) + expect([...run1.keys()]).toEqual([...run2.keys()]) - if (test === 'standard') { - expect([...run1.keys()]).toIncludeAllMembers([ - '.vercel/output/functions/app-page.func/.vc-config.json', - '.vercel/output/functions/app-page.rsc.func/.vc-config.json', - '.vercel/output/functions/app-route.func/.vc-config.json', - '.vercel/output/functions/app-route.rsc.func/.vc-config.json', - '.vercel/output/functions/pages-dynamic.func/.vc-config.json', - '.vercel/output/functions/pages-static-gsp.func/.vc-config.json', - ]) - expect([...run1.keys()]).toSatisfyAny((k) => - k.includes('middleware.func') - ) - } - }, - // The builder mode can take a bit longer, so we increase the timeout - // for these tests. The adapter mode should be faster, so we leave it as - // the default. - mode === 'builder' ? 120_000 : undefined - ) + if (test === 'standard') { + expect([...run1.keys()]).toIncludeAllMembers([ + '.vercel/output/functions/app-page.func/.vc-config.json', + '.vercel/output/functions/app-page.rsc.func/.vc-config.json', + '.vercel/output/functions/app-route.func/.vc-config.json', + '.vercel/output/functions/app-route.rsc.func/.vc-config.json', + '.vercel/output/functions/pages-dynamic.func/.vc-config.json', + '.vercel/output/functions/pages-static-gsp.func/.vc-config.json', + ]) + expect([...run1.keys()]).toSatisfyAny((k) => + k.includes('middleware.func') + ) + } + }) }) } ) diff --git a/turbopack/crates/turbopack-node/js/src/loader-runner.d.ts b/turbopack/crates/turbopack-node/js/src/loader-runner.d.ts new file mode 100644 index 000000000000..580764faad28 --- /dev/null +++ b/turbopack/crates/turbopack-node/js/src/loader-runner.d.ts @@ -0,0 +1,8 @@ +import 'loader-runner' + +declare module 'loader-runner' { + // @types/loader-runner is out of date and omits missingDependencies. + interface RunLoaderResult { + missingDependencies: string[] + } +} diff --git a/turbopack/crates/turbopack-node/js/src/transforms/webpack-loaders.ts b/turbopack/crates/turbopack-node/js/src/transforms/webpack-loaders.ts index dd507558b1be..e3d74cc95c30 100644 --- a/turbopack/crates/turbopack-node/js/src/transforms/webpack-loaders.ts +++ b/turbopack/crates/turbopack-node/js/src/transforms/webpack-loaders.ts @@ -554,7 +554,10 @@ const transform = ( ipc.sendInfo({ type: 'dependencies', envVariables: getReadEnvVariables(), - filePaths: result.fileDependencies.map(toPath), + filePaths: [ + ...result.fileDependencies, + ...result.missingDependencies, + ].map(toPath), directories: result.contextDependencies.map((dep) => [ toPath(dep), '**',