From 554b7fe3b028ec40f4cd0df877049c018f78e2ce Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Mon, 11 May 2026 11:13:27 +0200 Subject: [PATCH 1/3] Turbopack: don't generate next-server.js.nft.json with adapters (#93684) Adapters don't read these files. Also added a snapshot test similar to the existing ones to not regress on function size --- crates/next-api/src/next_server_nft.rs | 6 + crates/next-core/src/next_config.rs | 7 + .../production/next-server-nft/my-adapter.mjs | 9 + .../next-server-nft/next-server-nft.test.ts | 209 +++++++++++++++++- 4 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 test/production/next-server-nft/my-adapter.mjs diff --git a/crates/next-api/src/next_server_nft.rs b/crates/next-api/src/next_server_nft.rs index 5335de042fe5..dea9d670c68e 100644 --- a/crates/next-api/src/next_server_nft.rs +++ b/crates/next-api/src/next_server_nft.rs @@ -40,6 +40,12 @@ enum ServerNftType { #[turbo_tasks::function] pub async fn next_server_nft_assets(project: Vc) -> Result> { + if *project.next_config().is_using_adapter().await? { + // When using an adapter, we don't need to generate any server NFTs as build-complete + // doesn't use them at all. + return Ok(Vc::cell(vec![])); + } + let has_next_support = *project.ci_has_next_support().await?; let is_standalone = *project.next_config().is_standalone().await?; diff --git a/crates/next-core/src/next_config.rs b/crates/next-core/src/next_config.rs index 61adce84dae2..01fd70feaf62 100644 --- a/crates/next-core/src/next_config.rs +++ b/crates/next-core/src/next_config.rs @@ -161,6 +161,8 @@ pub struct NextConfig { typescript: TypeScriptConfig, use_file_system_public_routes: bool, cache_components: Option, + + adapter_path: Option, // // These are never used by Turbopack, and potentially non-serializable anyway: // cache_life: (), @@ -2035,6 +2037,11 @@ impl NextConfig { ) } + #[turbo_tasks::function] + pub fn is_using_adapter(&self) -> Vc { + Vc::cell(self.adapter_path.is_some()) + } + #[turbo_tasks::function] pub fn should_append_server_deployment_id_at_runtime(&self) -> Vc { let needs_dpl_id = self diff --git a/test/production/next-server-nft/my-adapter.mjs b/test/production/next-server-nft/my-adapter.mjs new file mode 100644 index 000000000000..c4fab7895f52 --- /dev/null +++ b/test/production/next-server-nft/my-adapter.mjs @@ -0,0 +1,9 @@ +import fs from 'fs/promises' + +/** @type {import('next').NextAdapter } */ +export default { + name: 'next-server-nft', + async onBuildComplete(ctx) { + await fs.writeFile('build-complete.json', JSON.stringify(ctx, null, 2)) + }, +} diff --git a/test/production/next-server-nft/next-server-nft.test.ts b/test/production/next-server-nft/next-server-nft.test.ts index 8b01ec4303fb..b15db84c8cfa 100644 --- a/test/production/next-server-nft/next-server-nft.test.ts +++ b/test/production/next-server-nft/next-server-nft.test.ts @@ -1,14 +1,14 @@ import { nextTestSetup } from 'e2e-utils' import path from 'path' import fs from 'fs' +import { NextAdapter } from 'next' const isReact18 = parseInt(process.env.NEXT_TEST_REACT_VERSION) === 18 -async function readNormalizedNFT(next, name) { - const data = await next.readJSON(name) +function normalizeNFT(base: string, files: string[]): string[] { const result = [ ...new Set( - data.files + files .filter((file: string) => { // They are important, but they are never actually included by themselves but rather as // part of some JS files in the same directory tree, which are higher-signal for the @@ -17,8 +17,12 @@ async function readNormalizedNFT(next, name) { return false } + if (file.includes('.next/server/chunks/')) { + return false + } + // Filter out the many symlinks that power node_modules - const fileAbsolute = path.join(next.testDir, name, '..', file) + const fileAbsolute = path.join(base, file) try { if (fs.lstatSync(fileAbsolute).isSymbolicLink()) { return false @@ -57,6 +61,11 @@ async function readNormalizedNFT(next, name) { return result } +async function readNormalizedNFT(next, name) { + const data = await next.readJSON(name) + return normalizeNFT(path.join(next.testDir, name, '..'), data.files) +} + // Only run this test for Turbopack as it is more conservative (i.e. aggressive) in including // referenced files and might include too many. (The Webpack snapshots would different slightly from // the Turbopack ones below.) @@ -519,5 +528,197 @@ async function readNormalizedNFT(next, name) { `) }) }) + + describe('with adapters', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + dependencies: { + typescript: '5.9.2', + }, + nextConfig: { + adapterPath: path.join(__dirname, './my-adapter.mjs'), + }, + }) + + if (skipped) { + return + } + + it('should not include .next directory in traces despite dynamic fs operations', async () => { + // This test verifies that the denied_path feature prevents the .next directory + // from being included in traces. The app/dynamic-read page uses dynamic fs.readFileSync + // with path.join(process.cwd(), ...) which could theoretically read any file. + + // Check the page-specific trace that has the dynamic fs operations + const pageTrace = await readNormalizedNFT( + next, + '.next/server/app/dynamic-read/page.js.nft.json' + ) + + // Snapshot the non-node_modules and non-chunks files to see what's being traced + // We also filter out chunks because their names change with every build + const nonNodeModulesFiles = pageTrace.filter( + (file: string) => + !file.includes('/node_modules/') && !file.includes('/chunks/') + ) + + expect(nonNodeModulesFiles).toMatchInlineSnapshot(` + [ + "./page/react-loadable-manifest.json", + "./page_client-reference-manifest.js", + ] + `) + }) + + it('should not trace too many files in next-minimal-server.js.nft.json', async () => { + const { + outputs, + repoRoot, + }: Parameters[0] = await next.readJSON( + 'build-complete.json' + ) + + const files = new Set() + + function appendOutput(output: { + filePath: string + assets: Record + }) { + if (output == null) return + + for (const file of Object.values(output.assets)) { + files.add('./' + path.relative(repoRoot, file)) + } + } + + outputs.pages.forEach(appendOutput) + outputs.appPages.forEach(appendOutput) + appendOutput(outputs.middleware) + outputs.pagesApi.forEach(appendOutput) + outputs.appRoutes.forEach(appendOutput) + + const trace = normalizeNFT(repoRoot, Array.from(files)) + + expect(trace).toMatchInlineSnapshot(` + [ + "./.next/BUILD_ID", + "./.next/app-path-routes-manifest.json", + "./.next/build-manifest.json", + "./.next/prerender-manifest.json", + "./.next/required-server-files.json", + "./.next/routes-manifest.json", + "./.next/server/app-paths-manifest.json", + "./.next/server/app/_global-error/page/react-loadable-manifest.json", + "./.next/server/app/_global-error/page_client-reference-manifest.js", + "./.next/server/app/_not-found/page/react-loadable-manifest.json", + "./.next/server/app/_not-found/page_client-reference-manifest.js", + "./.next/server/app/dynamic-read/page/react-loadable-manifest.json", + "./.next/server/app/dynamic-read/page_client-reference-manifest.js", + "./.next/server/app/page/react-loadable-manifest.json", + "./.next/server/app/page_client-reference-manifest.js", + "./.next/server/functions-config-manifest.json", + "./.next/server/middleware-build-manifest.js", + "./.next/server/middleware-manifest.json", + "./.next/server/next-font-manifest.js", + "./.next/server/next-font-manifest.json", + "./.next/server/pages-manifest.json", + "./.next/server/prefetch-hints.json", + "./.next/server/server-reference-manifest.js", + "./.next/server/server-reference-manifest.json", + "/node_modules/@swc/helpers/cjs/_interop_require_default.cjs", + "/node_modules/next/dist/build/adapter/setup-node-env.external.js", + "/node_modules/next/dist/client/components/app-router-headers.js", + "/node_modules/next/dist/client/components/hooks-server-context.js", + "/node_modules/next/dist/client/components/static-generation-bailout.js", + "/node_modules/next/dist/client/lib/console.js", + "/node_modules/next/dist/compiled/@opentelemetry/api/index.js", + "/node_modules/next/dist/compiled/jsonwebtoken/index.js", + "/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js", + "/node_modules/next/dist/compiled/source-map/source-map.js", + "/node_modules/next/dist/compiled/stacktrace-parser/stack-trace-parser.cjs.js", + "/node_modules/next/dist/compiled/ws/index.js", + "/node_modules/next/dist/lib/client-and-server-references.js", + "/node_modules/next/dist/lib/constants.js", + "/node_modules/next/dist/lib/framework/boundary-constants.js", + "/node_modules/next/dist/lib/interop-default.js", + "/node_modules/next/dist/lib/is-error.js", + "/node_modules/next/dist/lib/picocolors.js", + "/node_modules/next/dist/lib/scheduler.js", + "/node_modules/next/dist/lib/semver-noop.js", + "/node_modules/next/dist/server/app-render/action-async-storage-instance.js", + "/node_modules/next/dist/server/app-render/action-async-storage.external.js", + "/node_modules/next/dist/server/app-render/after-task-async-storage-instance.js", + "/node_modules/next/dist/server/app-render/after-task-async-storage.external.js", + "/node_modules/next/dist/server/app-render/async-local-storage.js", + "/node_modules/next/dist/server/app-render/blocking-route-messages.js", + "/node_modules/next/dist/server/app-render/cache-signal.js", + "/node_modules/next/dist/server/app-render/console-async-storage-instance.js", + "/node_modules/next/dist/server/app-render/console-async-storage.external.js", + "/node_modules/next/dist/server/app-render/dynamic-access-async-storage-instance.js", + "/node_modules/next/dist/server/app-render/dynamic-access-async-storage.external.js", + "/node_modules/next/dist/server/app-render/dynamic-rendering.js", + "/node_modules/next/dist/server/app-render/instant-validation/boundary-constants.js", + "/node_modules/next/dist/server/app-render/instant-validation/boundary-tracking.js", + "/node_modules/next/dist/server/app-render/module-loading/track-module-loading.external.js", + "/node_modules/next/dist/server/app-render/module-loading/track-module-loading.instance.js", + "/node_modules/next/dist/server/app-render/staged-rendering.js", + "/node_modules/next/dist/server/app-render/work-async-storage-instance.js", + "/node_modules/next/dist/server/app-render/work-async-storage.external.js", + "/node_modules/next/dist/server/app-render/work-unit-async-storage-instance.js", + "/node_modules/next/dist/server/app-render/work-unit-async-storage.external.js", + "/node_modules/next/dist/server/dev/browser-logs/file-logger.js", + "/node_modules/next/dist/server/dynamic-rendering-utils.js", + "/node_modules/next/dist/server/lib/incremental-cache/memory-cache.external.js", + "/node_modules/next/dist/server/lib/incremental-cache/shared-cache-controls.external.js", + "/node_modules/next/dist/server/lib/incremental-cache/tags-manifest.external.js", + "/node_modules/next/dist/server/lib/lru-cache.js", + "/node_modules/next/dist/server/lib/parse-stack.js", + "/node_modules/next/dist/server/lib/router-utils/instrumentation-globals.external.js", + "/node_modules/next/dist/server/lib/router-utils/instrumentation-node-extensions.js", + "/node_modules/next/dist/server/lib/source-maps.js", + "/node_modules/next/dist/server/lib/trace/constants.js", + "/node_modules/next/dist/server/lib/trace/tracer.js", + "/node_modules/next/dist/server/load-manifest.external.js", + "/node_modules/next/dist/server/node-environment-baseline.js", + "/node_modules/next/dist/server/node-environment-extensions/console-dim.external.js", + "/node_modules/next/dist/server/node-environment-extensions/console-exit.js", + "/node_modules/next/dist/server/node-environment-extensions/console-file.js", + "/node_modules/next/dist/server/node-environment-extensions/date.js", + "/node_modules/next/dist/server/node-environment-extensions/error-inspect.js", + "/node_modules/next/dist/server/node-environment-extensions/fast-set-immediate.external.js", + "/node_modules/next/dist/server/node-environment-extensions/io-utils.js", + "/node_modules/next/dist/server/node-environment-extensions/node-crypto.js", + "/node_modules/next/dist/server/node-environment-extensions/random.js", + "/node_modules/next/dist/server/node-environment-extensions/unhandled-rejection.external.js", + "/node_modules/next/dist/server/node-environment-extensions/web-crypto.js", + "/node_modules/next/dist/server/node-environment.js", + "/node_modules/next/dist/server/node-polyfill-crypto.js", + "/node_modules/next/dist/server/patch-error-inspect.js", + "/node_modules/next/dist/server/require-hook.js", + "/node_modules/next/dist/server/response-cache/types.js", + "/node_modules/next/dist/server/route-modules/app-page/module.compiled.js", + "/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/app-router-context.js", + "/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/entrypoints.js", + "/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/head-manager-context.js", + "/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/hooks-client-context.js", + "/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/image-config-context.js", + "/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/router-context.js", + "/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/server-inserted-html.js", + "/node_modules/next/dist/server/runtime-reacts.external.js", + "/node_modules/next/dist/shared/lib/deep-freeze.js", + "/node_modules/next/dist/shared/lib/invariant-error.js", + "/node_modules/next/dist/shared/lib/is-plain-object.js", + "/node_modules/next/dist/shared/lib/is-thenable.js", + "/node_modules/next/dist/shared/lib/lazy-dynamic/bailout-to-csr.js", + "/node_modules/next/dist/shared/lib/no-fallback-error.external.js", + "/node_modules/next/dist/shared/lib/promise-with-resolvers.js", + "/node_modules/next/dist/shared/lib/server-reference-info.js", + "/node_modules/react/cjs/react.development.js", + "/node_modules/react/cjs/react.production.js", + "/node_modules/react/index.js", + ] + `) + }) + }) } ) From cd6f40c62ff6dda40e35b3fe003b0624c2dbc34d Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Mon, 11 May 2026 13:18:40 +0200 Subject: [PATCH 2/3] Add an info panel for the existing "Cache disabled" indicator (#93756) The dev tools already showed an orange `Cache disabled` badge when caches were bypassed in development, but offered no explanation of what that meant or why it appeared. This adds a `Cache: Disabled` entry to the dev tools menu (also shown only when caches are bypassed) that opens an info panel. The panel explains the three triggers (the browser's "Disable cache" toggle, a hard reload, or draft mode), that the loading experience may differ from production, that React DevTools annotations will not accurately reflect what would normally suspend, and that Next.js cannot validate whether a navigation would be instant or blocking while caches are bypassed. Screenshot 2026-05-11 at 12 14 26 Screenshot 2026-05-11 at 12 32 46 closes NAR-467 --- .../dev-tools-info/cache-disabled.tsx | 21 +++++++++++ .../dev-overlay/menu/context.tsx | 2 + .../dev-overlay/menu/panel-router.tsx | 30 +++++++++++++++ .../cache-indicator/cache-indicator.test.ts | 37 +++++++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 packages/next/src/next-devtools/dev-overlay/components/errors/dev-tools-indicator/dev-tools-info/cache-disabled.tsx diff --git a/packages/next/src/next-devtools/dev-overlay/components/errors/dev-tools-indicator/dev-tools-info/cache-disabled.tsx b/packages/next/src/next-devtools/dev-overlay/components/errors/dev-tools-indicator/dev-tools-info/cache-disabled.tsx new file mode 100644 index 000000000000..dbc00ab444ad --- /dev/null +++ b/packages/next/src/next-devtools/dev-overlay/components/errors/dev-tools-indicator/dev-tools-info/cache-disabled.tsx @@ -0,0 +1,21 @@ +import type { ComponentProps } from 'react' + +export function CacheDisabledBody(props: ComponentProps<'div'>) { + return ( +
+

+ While loading this page, all caches were bypassed. +

+

+ This is the case when the cache was disabled in the browser's devtools, + the page was hard-reloaded, or draft mode is enabled. +

+

+ As a result, the loading experience might not be the same as in + production. React's DevTools will also not accurately show information + about what would normally suspend in the page, and Next.js cannot + validate whether a navigation to this page would be instant or blocking. +

+
+ ) +} diff --git a/packages/next/src/next-devtools/dev-overlay/menu/context.tsx b/packages/next/src/next-devtools/dev-overlay/menu/context.tsx index f8dc9f207c2a..39c4180f12fc 100644 --- a/packages/next/src/next-devtools/dev-overlay/menu/context.tsx +++ b/packages/next/src/next-devtools/dev-overlay/menu/context.tsx @@ -11,6 +11,8 @@ export type PanelStateKind = | 'segment-explorer' | 'panel-selector' | 'instant-navs' + | 'turbo-info' + | 'cache-disabled' export const PanelRouterContext = createContext<{ panel: PanelStateKind | null diff --git a/packages/next/src/next-devtools/dev-overlay/menu/panel-router.tsx b/packages/next/src/next-devtools/dev-overlay/menu/panel-router.tsx index dd06ec1803e4..a92897a84cb9 100644 --- a/packages/next/src/next-devtools/dev-overlay/menu/panel-router.tsx +++ b/packages/next/src/next-devtools/dev-overlay/menu/panel-router.tsx @@ -30,6 +30,7 @@ import { useUpdateAllPanelPositions } from '../components/devtools-indicator/dev import { saveDevToolsConfig } from '../utils/save-devtools-config' import { InstantNavsPanel } from '../components/instant-navs/instant-navs-panel' import './panel-router.css' +import { CacheDisabledBody } from '../components/errors/dev-tools-indicator/dev-tools-info/cache-disabled' const MenuPanel = () => { const { setPanel, setSelectedIndex } = usePanelRouterContext() @@ -117,6 +118,16 @@ const MenuPanel = () => { 'data-instant-nav': true, }, }, + state.cacheIndicator === 'bypass' && { + title: + 'Caching is currently disabled (bypassed). Click to learn more.', + label: 'Cache', + value: 'Disabled', + onClick: () => setPanel('cache-disabled'), + attributes: { + 'data-cache-disabled': true, + }, + }, isAppRouter && { label: 'Route Info', value: , @@ -274,6 +285,25 @@ export const PanelRouter = () => { )} + + {state.cacheIndicator === 'bypass' && ( + + } + > +
+ +
+
+
+ )} ) } diff --git a/test/development/app-dir/cache-indicator/cache-indicator.test.ts b/test/development/app-dir/cache-indicator/cache-indicator.test.ts index ae775250be50..adfdaa15c742 100644 --- a/test/development/app-dir/cache-indicator/cache-indicator.test.ts +++ b/test/development/app-dir/cache-indicator/cache-indicator.test.ts @@ -163,6 +163,43 @@ describe('cache-indicator', () => { }) }) + it('opens cache-disabled info panel from the devtools menu', async () => { + const browser = await next.browser('/', { + extraHTTPHeaders: { 'cache-control': 'no-cache' }, + }) + + await retry(async () => { + const hasCacheBypassBadge = await browser.hasElementByCss( + '[data-cache-bypass-badge]' + ) + expect(hasCacheBypassBadge).toBe(true) + }) + + // Open the devtools menu via the cache-bypass badge (the regular + // indicator is hidden when caches are bypassed). + await browser + .elementByCss('[data-cache-bypass-badge] [data-issues-open]') + .click() + + await retry(async () => { + const hasMenu = await browser.hasElementByCss('#nextjs-dev-tools-menu') + expect(hasMenu).toBe(true) + }) + + await browser.elementByCss('[data-cache-disabled]').click() + + await retry(async () => { + const article = await browser.elementByCss('.dev-tools-info-article') + expect(await article.text()).toMatchInlineSnapshot(` + "While loading this page, all caches were bypassed. + + This is the case when the cache was disabled in the browser's devtools, the page was hard-reloaded, or draft mode is enabled. + + As a result, the loading experience might not be the same as in production. React's DevTools will also not accurately show information about what would normally suspend in the page, and Next.js cannot validate whether a navigation to this page would be instant or blocking." + `) + }) + }) + it('can dismiss cache-bypassing badge', async () => { const browser = await next.browser('/', { extraHTTPHeaders: { 'cache-control': 'no-cache' }, From 8141dcf12e61c02b298537ed79a6aebbe775f919 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Mon, 11 May 2026 13:55:47 +0200 Subject: [PATCH 3/3] Convert test/integration to isolated tests (#93247) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What? Converts every test under `test/integration/` to an isolated test running through `nextTestSetup` (under `test/e2e/`, `test/production/`, `test/development/`, or `test/unit/`), then deletes `test/integration/` along with the legacy CI orchestration that was specific to it. - `test/integration/` removed entirely (~327 test suites) - New isolated suites added across the existing folders: - `test/e2e/` — 175 - `test/production/` — 130 - `test/development/` — 43 - `test/unit/` — 1 - `.github/workflows/build_and_test.yml` and `run-tests.js` no longer have any `integration` branches - `nextTestSetup` gained a `baseUrl` option on `next.browser()` so a small number of tests that drive their own proxy/static-export server can keep using `next.browser(...)` instead of importing `next-webdriver` directly ### Why? `test/integration/` predated `nextTestSetup` and ran tests directly against the source checkout via custom helpers (`launchApp`, `nextBuild`, `nextStart`, `runNextCommand`, `webdriver`, `fetchViaHTTP`, …). Each suite hand-rolled its own dev/start/build orchestration, fixture mutation, and process management. The isolated test model used by the rest of the repo gives each suite an isolated working directory containing a packed `next.tgz` install, a uniform `next.start()` / `next.build()` / `next.fetch()` / `next.browser()` API, and the same lifecycle for dev, start, and deploy modes — so a single set of assertions covers all three. Deploy-mode skips and per-feature gates are expressed declaratively (`skipDeployment`, `disableAutoSkewProtection`, `if (skipped) return`) instead of branching on `process.env`. Removing `test/integration/` lets us: - Delete the bespoke orchestration code in the CI workflow and `run-tests.js` - Run every converted suite consistently in dev, start, and deploy modes (where applicable) - Reproduce every test locally with the same `pnpm test-{dev,start}-{turbo,webpack}` commands; no separate `integration` path - Open the door to running `test/production` against deployments in the future (the converted suites already declare `skipDeployment` so they can be flipped on) ### How? Mechanical conversion per suite, with targeted clean-ups: 1. **Per-suite conversion.** Each `test/integration//test/index.test.{js,ts}` was rewritten into a single `.test.ts` under the right folder based on what the original exercised: - `launchApp` / dev-only assertions → `test/development/` - `nextBuild` + `nextStart` / start-only assertions → `test/production/` - Both → `test/e2e/` - The one pure jsdom render check (`link-without-router`) → `test/unit/` 2. **API mapping.** Custom helpers were replaced by `nextTestSetup` equivalents: `launchApp` → `next.start()`, `nextBuild` → `next.build()`, `runNextCommand` → `next.runCommand`, `fetchViaHTTP` → `next.fetch`, `webdriver(...)` → `next.browser(...)`. Fixture mutations switched from raw `fs.writeFile`/`fs.rename` to `next.patchFile` (with the 3-arg `runWithTempContent` callback when the change has a defined scope) and `next.deleteFile`. 3. **Deploy-mode handling.** Suites that can't run in deploy mode (use `patchFile` / `next.build()` / depend on local CLI output) declare `skipDeployment: true` and early-return on the `skipped` boolean. Suites where Vercel's edge mutates URLs (`&dpl=`, immutable assets) declare `disableAutoSkewProtection: true`. 4. **`next.browser({ baseUrl })`.** A handful of tests (`prerender-export`, `cdn-cache-busting`, `preload-viewport`, both `react-virtualized` suites) need to drive a separate server (a static-export server or an `http-proxy` instance) rather than the Next.js process. Instead of importing `next-webdriver` directly, those tests now pass `{ baseUrl: }` to `next.browser()`. For the proxy cases, the proxy was moved into `server.js` inside the fixture and `http-proxy` declared via the `dependencies` option of `nextTestSetup`, so the test runs with a fully isolated dependency graph. 5. **CI clean-up.** With `test/integration` gone, the `test integration*` jobs and `integration-tests-manifest`-related logic in `.github/workflows/build_and_test.yml` were removed, and `run-tests.js` no longer has the `integration` test-folder branch. 6. **Validation.** The PR was iterated against multiple full CI runs; the remaining failures on the latest run are pre-existing flakes (segment-cache 60s `act` timeouts in turbopack-prod) or transient infrastructure issues unrelated to the conversion. --- .agents/skills/pr-status-triage/SKILL.md | 6 +- .agents/skills/pr-status-triage/workflow.md | 2 +- .config/eslintignore.mjs | 2 - .github/workflows/build_and_test.yml | 241 +- .prettierignore | 4 - crates/next-core/src/url_node.rs | 2 +- package.json | 1 - .../helpers/get-pkg-manager.ts | 38 +- packages/create-next-app/templates/index.ts | 53 +- packages/eslint-config-next/package.json | 1 + .../src/server/dev/hot-reloader-rspack.ts | 2 +- .../shared/lib/router/utils/sorted-routes.ts | 2 +- packages/next/taskfile-swc.js | 2 +- run-tests.js | 23 +- scripts/get-changed-tests.mjs | 5 +- scripts/pr-logs.js | 426 ++++ scripts/pr-status.js | 270 ++- scripts/run-for-change.mjs | 4 +- test/cache-components-tests-manifest.json | 4 + .../development/app-aspath/app-aspath.test.ts | 32 + .../app-aspath/pages/_app.js | 0 .../app-aspath/pages/index.js | 0 .../app-config-asset-prefix.test.ts | 15 + .../app-config-asset-prefix/app/layout.js | 0 .../app-config-asset-prefix/app/page.js | 0 .../app-config-asset-prefix/next.config.js | 0 .../app-document-add-hmr.test.ts} | 74 +- .../app-document-add-hmr/pages/index.js | 0 .../app-document-remove-hmr.test.ts | 96 + .../app-document-remove-hmr/pages/_app.js | 0 .../pages/_document.js | 0 .../app-document-remove-hmr/pages/index.js | 0 .../app-functional/app-functional.test.ts | 12 + .../app-functional/next.config.js | 0 .../app-functional/pages/_app.js | 0 .../app-functional/pages/index.js | 0 .../app-functional/shared-module.js | 0 .../babel-next-image/.babelrc | 0 .../babel-next-image/app/layout.js | 0 .../babel-next-image/app/page.js | 0 .../babel-next-image/babel-next-image.test.ts | 12 + .../broken-webpack-plugin.test.ts | 22 + .../broken-webpack-plugin/next.config.js | 0 .../broken-webpack-plugin/pages/index.js | 0 .../client-navigation-a11y.test.ts} | 71 +- .../client-navigation-a11y/pages/index.js | 0 .../pages/page-with-h1-and-title.js | 0 .../pages/page-with-h1.js | 0 .../pages/page-with-title.js | 0 .../pages/page-without-h1-or-title.js | 0 .../compression/compression.test.ts | 13 + .../compression/pages/index.js | 0 .../config-devtool-dev.test.ts} | 41 +- .../config-devtool-dev/next.config.js | 0 .../config-devtool-dev/pages/index.js | 0 .../config-mjs/.gitignore | 0 .../components/hello-webpack-css.css | 0 .../components/hello-webpack-css.js | 0 .../components/hello-webpack-sass.scss | 0 .../development/config-mjs/config-mjs.test.ts | 23 + .../config-mjs/next.config.mjs | 0 .../node_modules/css-framework/framework.css | 0 .../module-only-package/modern.js | 0 .../module-only-package/package.json | 0 .../config-mjs/pages/module-only-content.js | 0 .../config-mjs/pages/next-config.js | 0 .../config-output-export.test.ts | 424 ++++ .../config-output-export/next.config.js | 0 .../config-output-export}/pages/index.js | 0 .../config/.gitignore | 0 .../config/components/hello-webpack-css.css | 0 .../config/components/hello-webpack-css.js | 0 .../config/components/hello-webpack-sass.scss | 0 test/development/config/config.test.ts | 29 + .../config/next.config.js | 0 .../node_modules/css-framework/framework.css | 3 + .../module-only-package/modern.js | 0 .../module-only-package/package.json | 0 .../config/pages/build-id.js | 0 .../config/pages/module-only-content.js | 0 .../config/pages/next-config.js | 0 .../css-features/css-modules-support.test.ts | 57 + .../css-features/dev-css-handling.test.ts | 111 + .../fixtures/dev-module}/next.config.js | 0 .../fixtures/dev-module}/pages/index.js | 0 .../dev-module}/pages/index.module.css | 0 .../fixtures}/hmr-module/pages/index.js | 0 .../hmr-module}/pages/index.module.css | 0 .../fixtures/multi-page}/.gitignore | 0 .../fixtures/multi-page}/pages/_app.js | 0 .../fixtures}/multi-page/pages/page1.js | 0 .../fixtures}/multi-page/pages/page2.js | 0 .../fixtures/multi-page}/styles/global1.css | 0 .../fixtures/multi-page}/styles/global2.css | 0 .../fixtures/transition-react}/.gitignore | 0 .../fixtures}/transition-react/pages/index.js | 0 .../fixtures}/transition-react/pages/other.js | 0 .../transition-react/pages/other.module.css | 0 .../fixtures/unused}/pages/index.js | 0 .../css-modules/css-modules.test.ts | 59 + .../fixtures/dev-module}/next.config.js | 0 .../fixtures}/dev-module/pages/index.js | 0 .../dev-module}/pages/index.module.css | 0 .../fixtures/hmr-module/pages/index.js | 15 + .../hmr-module/pages}/index.module.css | 0 .../development-hmr-refresh.test.ts | 18 + .../pages/with+Special&Chars=.js | 0 .../document-head-warnings.test.ts | 28 + .../document-head-warnings/pages/_document.js | 0 .../document-head-warnings/pages/index.js | 0 .../development/dynamic-require/index.test.ts | 16 + .../dynamic-require/locales/en.js | 0 .../dynamic-require/locales/ru.js | 0 .../dynamic-require/pages/index.js | 0 .../dynamic-route-rename.test.ts | 29 + .../dynamic-route-rename/pages/[pid].js | 0 .../empty-object-getInitialProps.test.ts | 47 + .../pages/another.js | 0 .../pages/index.js | 0 .../pages/static.js | 0 .../empty-project/empty-project.test.ts | 18 + .../empty-project}/next.config.js | 0 .../empty-project/pages/.gitkeep | 0 .../gssp-redirect-with-rewrites.test.ts | 30 + .../next.config.js | 0 .../pages/main-content.js | 0 .../pages/redirector.js | 0 .../invalid-revalidate-values.test.ts | 94 + .../invalid-revalidate-values/pages/ssg.js | 0 .../jsconfig-paths-wildcard/.gitignore | 0 .../jsconfig-paths-wildcard.test.ts | 39 + .../jsconfig-paths-wildcard/jsconfig.json | 0 .../jsconfig-paths-wildcard}/next.config.js | 0 .../node_modules/mypackage/data.js | 0 .../node_modules/mypackage/myfile.js | 0 .../pages/wildcard-alias.js | 0 .../link-with-encoding.test.ts | 219 ++ .../link-with-encoding/pages/index.js | 0 .../link-with-encoding/pages/query.js | 0 .../link-with-encoding/pages/single/[slug].js | 0 .../middleware-dev-update.test.ts | 127 + .../middleware-dev-update/middleware.js | 0 .../middleware-dev-update/pages/index.js | 0 .../middleware-overrides-node.js-api.test.ts | 21 + .../middleware.js | 0 .../pages/index.js | 0 .../missing-document-component-error.test.ts} | 51 +- .../pages/index.js | 0 .../export-config/export-config.test.ts | 17 + .../export-config}/next.config.js | 0 .../export-config/pages/index.js | 0 .../export-config}/public/test.webp | Bin .../invalid-image-import.test.ts | 49 + .../invalid-image-import/pages/index.js | 0 .../invalid-image-import/public/invalid.svg | 0 .../middleware/middleware-intercept.test.ts | 20 + .../next-image-new/middleware/middleware.js | 0 .../middleware/middleware.test.ts | 18 + .../next-image-new/middleware/pages/index.js | 0 .../middleware}/public/small.jpg | Bin .../no-override-next-props.test.ts | 12 + .../no-override-next-props/pages/_app.js | 0 .../no-override-next-props/pages/index.js | 0 .../ondemand/components/hello.js | 0 .../ondemand/next.config.js | 0 test/development/ondemand/ondemand.test.ts | 63 + .../ondemand/pages/about.js | 0 .../ondemand/pages/index.js | 0 .../ondemand/pages/nav/dynamic.js | 0 .../ondemand/pages/nav/index.js | 0 .../ondemand/pages/third.js | 0 .../ondemand/server.js | 21 +- .../plugin-mdx-rs/components/button.js | 0 .../plugin-mdx-rs/components/marker.js | 0 .../plugin-mdx-rs/mdx-components.js | 0 .../plugin-mdx-rs/next.config.js | 0 .../plugin-mdx-rs/pages/button.mdx | 0 .../plugin-mdx-rs/pages/gfm.mdx | 0 .../plugin-mdx-rs/pages/index.mdx | 0 .../plugin-mdx-rs/pages/provider.mdx | 0 .../plugin-mdx-rs/plugin-mdx-rs.test.ts | 60 + .../prerender/pages/blog/[post]/index.js | 0 test/development/prerender/prerender.test.ts | 54 + .../pages/api/blog/[slug].js | 0 .../server-side-dev-errors/pages/api/hello.js | 0 .../pages/blog/[slug].js | 0 .../server-side-dev-errors/pages/gsp.js | 0 .../server-side-dev-errors/pages/gssp.js | 0 .../pages/uncaught-empty-exception.js | 0 .../pages/uncaught-empty-rejection.js | 0 .../pages/uncaught-exception.js | 0 .../pages/uncaught-rejection.js | 0 .../server-side-dev-errors.test.ts | 454 ++++ .../trailing-slash-dist/next.config.js | 0 .../trailing-slash-dist}/pages/index.js | 0 .../trailing-slash-dist.test.ts | 17 + .../fixtures/empty-config}/next.config.js | 0 .../fixtures/empty-config}/pages/index.js | 0 .../fixtures/no-config/pages/index.js | 3 + .../unsupported-config}/next.config.js | 2 +- .../unsupported-config/pages/index.js | 3 + .../turbopack-unsupported-log.test.ts | 65 + .../next-env.strictRouteTypes.d.ts} | 2 + .../pages/index.tsx | 0 .../typescript-app-type-declarations.test.ts | 106 + .../typescript-external-dir/.gitignore | 7 + .../project/components/world.tsx | 0 .../project/next.config.js | 0 .../project/pages/index.tsx | 0 .../project/tsconfig.json | 0 .../shared/components/counter.tsx | 0 .../shared/libs/inc.ts | 0 .../shared/tsconfig.json | 0 .../typescript-external-dir.test.ts | 30 + .../typescript-hmr}/next.config.js | 0 .../typescript-hmr/pages/hello.tsx | 0 .../pages/type-error-recover.tsx | 0 .../typescript-hmr/typescript-hmr.test.ts | 79 + test/e2e/404-page-app/404-page-app.test.ts | 45 + .../404-page-app}/next.config.js | 0 .../404-page-app/pages/404.js | 0 .../404-page-app/pages/_app.js | 0 .../404-page-app/pages/err.js | 0 .../404-page-app}/pages/index.js | 0 .../404-page-custom-error.test.ts | 48 + .../404-page-custom-error/pages/_error.js | 0 .../404-page-custom-error/pages/err.js | 0 .../404-page-custom-error}/pages/index.js | 0 test/e2e/404-page-ssg/404-page-ssg.test.ts | 72 + .../404-page-ssg}/next.config.js | 0 .../404-page-ssg/pages/404.js | 0 .../404-page-ssg/pages/_app.js | 0 .../404-page-ssg/pages/err.js | 0 .../404-page-ssg}/pages/index.js | 0 test/e2e/404-page/404-page.test.ts | 264 ++ .../404-page}/next.config.js | 0 .../404-page/pages/404.js | 0 .../404-page/pages/err.js | 0 .../500-page => e2e/404-page}/pages/index.js | 0 .../404-page/pages/invalidExtension.d.ts | 0 test/e2e/500-page/500-page-build.test.ts | 333 +++ test/e2e/500-page/500-page.test.ts | 108 + .../500-page}/next.config.js | 0 .../500-page/pages/500.js | 0 .../500-page/pages/err.js | 0 .../500-page}/pages/index.js | 0 .../api-body-parser/api-body-parser.test.ts | 62 + .../api-body-parser/pages/api/index.js | 0 .../api-body-parser/server.js | 9 +- test/e2e/api-catch-all/api-catch-all.test.ts | 45 + .../pages/api/users/[...slug].js | 0 .../api-catch-all/pages/api/users/index.js | 0 .../api-support/api-support.test.ts} | 512 ++-- .../{integration => e2e}/api-support/big.json | 0 .../api-support/pages/api-conflict.js | 0 .../api-support/pages/api/[post]/[comment].js | 0 .../api-support/pages/api/[post]/comments.js | 0 .../api-support/pages/api/[post]/index.js | 0 .../pages/api/auth/[...nextauth].js | 0 .../api-support/pages/api/big-parse.js | 0 .../api-support/pages/api/big-parse.ts | 0 .../pages/api/blog/[post]/comment/[id].js | 0 .../api-support/pages/api/blog/index.js | 0 .../api-support/pages/api/bool.js | 0 .../api-support/pages/api/child-process.js | 0 .../api-support/pages/api/cookies.js | 0 .../api-support/pages/api/cors.js | 0 .../api-support/pages/api/error.js | 0 .../api/external-resolver-false-positive.js | 0 .../pages/api/external-resolver.js | 0 .../api-support/pages/api/index.js | 0 .../api-support/pages/api/json-null.js | 0 .../api-support/pages/api/json-string.js | 0 .../api-support/pages/api/json-undefined.js | 0 .../pages/api/large-chunked-response.js | 0 .../api/large-response-with-config-size.js | 0 .../pages/api/large-response-with-config.js | 0 .../api-support/pages/api/large-response.js | 0 .../api-support/pages/api/no-parsing.js | 0 .../api-support/pages/api/nullable-payload.js | 0 .../api-support/pages/api/parse.js | 0 .../api-support/pages/api/parsing.js | 0 .../api-support/pages/api/proxy-self.js | 0 .../api-support/pages/api/query.js | 0 .../api-support/pages/api/redirect-301.js | 0 .../api-support/pages/api/redirect-307.js | 0 .../api-support/pages/api/redirect-error.js | 0 .../api-support/pages/api/redirect-null.js | 0 .../api-support/pages/api/status-204.js | 0 .../api-support/pages/api/test-no-end.js | 0 .../api-support/pages/api/test-res-pipe.js | 0 .../api-support/pages/api/user-error-async.js | 0 .../api-support/pages/api/user-error.js | 0 .../api-support/pages/api/users.js | 0 .../api-support/pages/index.js | 0 .../api-support/pages/user.js | 0 test/e2e/app-dir-export/test/config.test.ts | 26 +- .../client-cache.original.test.ts | 5 +- .../generate-static-params-error.test.ts | 3 +- .../error-on-next-codemod-comment.test.ts | 3 +- .../interception-routes-output-export.test.ts | 45 +- .../e2e/app-dir/logging/fetch-logging.test.ts | 3 +- .../middleware-rsc-external-rewrite.test.ts | 54 +- .../e2e/app-dir/ppr-errors/ppr-errors.test.ts | 79 +- .../ppr-missing-root-params.test.ts | 9 +- .../rewrite-headers/rewrite-headers.test.ts | 3 +- .../invalid-global-module.test.ts | 52 +- .../invalid-module/invalid-module.test.ts | 38 - .../cdn-cache-busting.test.ts | 69 +- .../cdn-cache-busting/server.mjs | 2 +- .../segment-cache-output-export.test.ts | 42 +- .../app-dir/segment-cache/export/server.mjs | 54 +- .../app-dir/typed-routes/typed-routes.test.ts | 8 +- .../webpack-loader-errors.test.ts | 3 +- .../app-document-import-order.test.ts | 42 + .../app-document-import-order/next.config.js | 0 .../app-document-import-order/pages/_app.js | 0 .../pages/_document.js | 0 .../app-document-import-order/pages/index.js | 0 .../requiredByApp.js | 0 .../requiredByPage.js | 0 .../sideEffectModule.js | 0 test/e2e/app-tree/app-tree.test.ts | 39 + .../app-tree/pages/_app.tsx | 0 .../app-tree/pages/another.js | 0 .../app-tree/pages/hello.tsx | 0 .../app-tree/pages/index.js | 0 test/e2e/auto-export/auto-export.test.ts | 56 + .../auto-export/pages/[post]/[cmnt].js | 0 .../auto-export/pages/[post]/index.js | 0 .../auto-export/pages/commonjs1.js | 0 .../auto-export/pages/commonjs2.js | 0 .../basepath-root-catch-all.test.ts | 17 + .../basepath-root-catch-all/next.config.js | 0 .../pages/[...parts].js | 0 .../basepath-root-catch-all/pages/hello.js | 0 test/e2e/bigint/bigint.test.ts | 19 + .../bigint/pages/api/bigint.js | 0 .../catches-missing-getStaticProps.test.ts | 27 + .../pages/[slug].js | 0 .../basic/file with spaces to --require.js | 0 ...ces to-require-with-node-require-option.js | 0 .../cli/basic/pages/index.js | 0 .../cli/certificates/localhost-key.pem | 0 .../cli/certificates/localhost.pem | 0 test/e2e/cli/cli.test.ts | 1249 ++++++++++ .../cli/duplicate-sass/package.json | 0 .../cli/duplicate-sass/pages/index.js | 0 test/e2e/client-404/client-404.test.ts | 87 + .../client-404}/next.config.js | 0 .../client-404/pages/_error.js | 0 .../client-404/pages/index.js | 0 .../client-404/pages/invalid-link.js | 0 .../client-404/pages/missing.js | 0 .../client-404/pages/to-missing-link.js | 0 .../client-shallow-routing.test.ts | 87 + .../client-shallow-routing/pages/[slug].js | 0 .../config-experimental-warning.test.ts | 269 +++ .../pages/index.js | 0 .../conflicting-public-file-page.test.ts | 39 + .../pages/another/conflict.js | 0 .../pages/another/index.js | 0 .../pages/hello.js | 0 .../public/another/conflict | 0 .../public/another/index | 0 .../conflicting-public-file-page/public/hello | 0 .../public/normal.txt | 0 .../cpu-profiling/cpu-profiling-build.test.ts | 1 + .../e2e/css-client-nav/css-client-nav.test.ts | 223 ++ .../css-client-nav}/next.config.js | 1 - .../css-client-nav}/pages/_app.js | 0 .../css-client-nav}/pages/blue.js | 0 .../css-client-nav}/pages/blue.module.css | 0 .../css-client-nav}/pages/global.css | 0 .../css-client-nav}/pages/none.js | 0 .../css-client-nav}/pages/red.js | 0 .../css-client-nav}/pages/red.module.css | 0 .../css-features/css-and-styled-jsx.test.ts | 17 + .../css-features/css-modules-ordering.test.ts | 656 +++++ .../fixtures/composes-ordering}/.gitignore | 0 .../composes-ordering/pages/common.module.css | 0 .../composes-ordering/pages/index.js | 0 .../composes-ordering/pages/index.module.css | 0 .../composes-ordering/pages/other.js | 0 .../composes-ordering/pages/other.module.css | 0 .../global-and-module-ordering}/.gitignore | 0 .../global-and-module-ordering}/pages/_app.js | 0 .../global-and-module-ordering/pages/index.js | 0 .../pages/index.module.css | 0 .../pages/index2.module.css | 0 .../styles/global.css | 0 .../fixtures/multi-page}/.gitignore | 0 .../fixtures}/multi-page/pages/_app.js | 0 .../fixtures/multi-page/pages/page1.js | 12 + .../fixtures/multi-page/pages/page2.js | 12 + .../fixtures/multi-page}/styles/global1.css | 0 .../fixtures/multi-page}/styles/global2.css | 0 .../fixtures/next-issue-12343}/.gitignore | 0 .../next-issue-12343/components/button.jsx | 0 .../components/button.module.css | 0 .../next-issue-12343/pages/another-page.js | 0 .../pages/homepage.module.css | 0 .../fixtures}/next-issue-12343/pages/index.js | 0 .../fixtures/transition-react}/.gitignore | 0 .../fixtures/transition-react/pages/index.js | 11 + .../fixtures/transition-react/pages/other.js | 34 + .../transition-react/pages/other.module.css | 3 + .../fixtures/with-styled-jsx}/pages/_app.js | 0 .../fixtures}/with-styled-jsx/pages/index.js | 0 .../with-styled-jsx/styles/global.css | 0 .../custom-error-page-exception.test.ts | 22 + .../pages/_error.js | 0 .../pages/index.js | 0 test/e2e/custom-error/custom-error.test.ts | 73 + .../custom-error/pages/_error.js | 0 .../custom-error/pages/index.js | 0 .../custom-page-extension.test.ts | 19 + .../custom-page-extension/next.config.js | 0 .../pages/blog/[pid].page.js | 0 .../pages/blog/index.page.js | 0 .../custom-routes-catchall.test.ts | 30 + .../custom-routes-catchall/next.config.js | 0 .../custom-routes-catchall/pages/hello.js | 0 .../custom-routes-catchall/public/another.txt | 0 .../public/static/data.json | 0 .../custom-routes-i18n-index-redirect.test.ts | 36 + .../next.config.js | 0 .../pages/index.js | 0 .../custom-routes-i18n.test.ts} | 153 +- .../custom-routes-i18n/next.config.js | 0 .../custom-routes-i18n/pages/links.js | 0 .../custom-routes/custom-routes.test.ts} | 2127 ++++++++++++----- .../custom-routes/next.config.js | 0 test/e2e/custom-routes/package.json | 8 + .../pages/_sport/[slug]/index.js | 0 .../custom-routes/pages/_sport/[slug]/test.js | 0 .../custom-routes/pages/another/[id].js | 0 .../custom-routes/pages/api/dynamic/[slug].js | 0 .../custom-routes/pages/api/hello.js | 0 .../custom-routes/pages/auto-export/[slug].js | 0 .../pages/auto-export/another.js | 0 .../pages/blog-catchall/[...slug].js | 0 .../custom-routes/pages/blog/[post]/index.js | 0 .../pages/docs/v2/more/now-for-github.js | 0 .../custom-routes/pages/hello-again.js | 0 .../custom-routes/pages/hello.js | 0 .../custom-routes/pages/multi-rewrites.js | 0 .../custom-routes/pages/nav.js | 0 .../custom-routes/pages/overridden.js | 0 .../custom-routes/pages/overridden/[slug].js | 0 .../custom-routes/pages/redirect-override.js | 0 .../custom-routes/pages/with-params.js | 0 .../custom-routes/public/blog/data.json | 0 .../custom-routes/public/static/hello.txt | 0 test/e2e/custom-server/custom-server.test.ts | 347 +++ .../custom-server/middleware.js | 0 .../custom-server/next.config.js | 0 .../custom-server/pages/404.js | 0 .../custom-server/pages/500.js | 0 test/e2e/custom-server/pages/asset.js | 5 + .../custom-server/pages/dashboard/index.js | 0 .../pages/dynamic-dashboard/index.js | 0 .../custom-server/pages/index.js | 0 .../pages/middleware-augmented.js | 0 .../custom-server/pages/no-query.js | 0 .../custom-server/server.js | 39 +- test/e2e/custom-server/ssh/ca-key.pem | 28 + test/e2e/custom-server/ssh/ca.pem | 20 + test/e2e/custom-server/ssh/localhost-key.pem | 28 + test/e2e/custom-server/ssh/localhost.pem | 21 + .../custom-server/static/hello.txt | 0 .../data-fetching-errors.test.ts | 214 ++ .../data-fetching-errors/pages/index.js | 0 test/e2e/disable-js/disable-js.test.ts | 53 + .../disable-js}/next.config.js | 0 .../disable-js/pages/index.js | 0 test/e2e/dist-dir/dist-dir.test.ts | 61 + .../dist-dir/next.config.js | 0 .../dist-dir/pages/index.js | 0 test/e2e/draft-mode/draft-mode.test.ts | 215 ++ .../draft-mode/pages/another.tsx | 0 .../draft-mode/pages/api/disable.ts | 0 .../draft-mode/pages/api/enable.ts | 0 .../draft-mode/pages/api/read.ts | 0 .../draft-mode/pages/index.tsx | 0 .../draft-mode/pages/ssp.tsx | 0 .../draft-mode/pages/to-index.tsx | 0 ...mic-optional-routing-root-fallback.test.ts | 30 + .../next.config.js | 0 .../pages/[[...optionalName]].js | 0 ...optional-routing-root-static-paths.test.ts | 22 + .../pages/[[...optionalName]].js | 0 .../dynamic-optional-routing.test.ts | 298 +++ .../dynamic-optional-routing/next.config.js | 0 .../pages/[[...optionalName]].js | 0 .../dynamic-optional-routing/pages/about.js | 0 .../pages/api/post/[[...slug]].js | 0 .../get-static-paths-fallback/[[...slug]].js | 0 .../get-static-paths-false/[[...slug]].js | 0 .../get-static-paths-null/[[...slug]].js | 0 .../get-static-paths-undefined/[[...slug]].js | 0 .../pages/get-static-paths/[[...slug]].js | 0 .../pages/nested/[[...optionalName]].js | 0 .../dynamic-routing-middleware.test.ts | 28 + .../dynamic-routing/dynamic-routing.test.ts | 16 + .../dynamic-routing/pages/[name]/[comment].js | 0 .../pages/[name]/[comment]/[...rest].js | 0 .../dynamic-routing/pages/[name]/comments.js | 0 .../dynamic-routing/pages/[name]/index.js | 0 .../pages/[name]/on-mount-redir.js | 0 .../dynamic-routing/pages/_app.js | 0 .../dynamic-routing/pages/another.js | 0 .../dynamic-routing/pages/b/[123].js | 0 .../pages/blog/[name]/comment/[id].js | 0 ...aramnameshouldbeallowedeventhoughweird].js | 0 .../pages/catchall-dash/[...hello-world].js | 0 .../dynamic-routing/pages/d/[id].js | 0 .../pages/dash/[hello-world].js | 0 .../dynamic-routing/pages/index.js | 0 .../dynamic-routing/pages/index/[...slug].js | 0 .../dynamic-routing/pages/on-mount/[post].js | 0 .../pages/p1/p2/all-ssg/[...rest].js | 0 .../pages/p1/p2/all-ssr/[...rest].js | 0 .../p1/p2/nested-all-ssg/[...rest]/index.js | 0 .../[...rest]/styles.module.css | 0 .../pages/p1/p2/predefined-ssg/[...rest].js | 0 .../dynamic-routing/public/hello copy.txt | 0 .../dynamic-routing/public/hello%20copy.txt | 0 .../dynamic-routing/public/hello+copy.txt | 0 .../dynamic-routing/public/hello.txt | 0 .../dynamic-routing/shared.ts} | 1292 +++++----- .../dynamic-routing/static/hello copy.txt | 0 .../dynamic-routing/static/hello%20copy.txt | 0 .../dynamic-routing/static/hello+copy.txt | 0 .../dynamic-routing/static/hello.txt | 0 .../edge-configurable-runtime/index.test.ts | 95 +- .../.pnpm/test/node_modules/lib/index.js | 0 .../.pnpm/test/node_modules/lib/package.json | 0 .../edge-runtime-configurable-guards.test.ts | 710 ++++++ .../middleware.js | 0 .../node_modules/lib/index.js | 1 + .../node_modules/lib/package.json | 8 + .../pages/api/route.js | 0 .../pages/index.js | 0 .../edge-runtime-dynamic-code.test.ts | 159 ++ .../edge-runtime-dynamic-code/lib/square.wasm | Bin .../edge-runtime-dynamic-code/lib/utils.js | 0 .../edge-runtime-dynamic-code/lib/wasm.js | 0 .../edge-runtime-dynamic-code/middleware.js | 0 .../edge-runtime-dynamic-code/next.config.js | 0 .../pages/api/route.js | 0 .../edge-runtime-dynamic-code/pages/index.js | 0 .../edge-runtime-module-errors.test.ts | 984 ++++++++ .../edge-runtime-module-errors/lib.js | 0 .../edge-runtime-module-errors/middleware.js | 0 .../pages/api/route.js | 0 .../edge-runtime-module-errors/pages/index.js | 0 .../edge-runtime-response-error.test.ts | 24 + .../edge-runtime-response-error/lib.js | 0 .../edge-runtime-response-error/middleware.js | 0 .../pages/api/route.js | 0 .../pages/index.js | 0 .../edge-runtime-streaming-error.test.ts | 26 + .../pages/api/test.js | 0 .../edge-runtime-with-node.js-apis.test.ts | 118 + .../lib/utils.js | 0 .../middleware.js | 0 .../pages/api/route.js | 0 .../pages/index.js | 0 .../env-config/app => e2e/env-config}/.env | 0 .../app => e2e/env-config}/.env.development | 0 .../env-config}/.env.development.local | 0 .../app => e2e/env-config}/.env.local | 0 .../app => e2e/env-config}/.env.production | 0 .../env-config}/.env.production.local | 0 .../app => e2e/env-config}/.env.test | 0 .../app => e2e/env-config}/.env.test.local | 0 test/e2e/env-config/.gitignore | 2 + test/e2e/env-config/env-config.test.ts | 311 +++ .../app => e2e/env-config}/next.config.js | 0 .../env-config}/pages/another-global.js | 0 .../app => e2e/env-config}/pages/api/all.js | 0 .../app => e2e/env-config}/pages/global.js | 0 .../app => e2e/env-config}/pages/index.js | 0 .../app => e2e/env-config}/pages/some-ssg.js | 0 .../app => e2e/env-config}/pages/some-ssp.js | 0 .../externals-pages-bundle}/.gitignore | 0 .../externals-pages-bundle.test.ts | 99 + .../externals-pages-bundle/next.config.js | 0 .../node_modules/external-package/index.js | 0 .../external-package/package.json | 0 .../opted-out-external-package/index.js | 0 .../opted-out-external-package/package.json | 0 .../externals-pages-bundle/pages/index.js | 0 .../fallback-false-rewrite.test.ts} | 81 +- .../fallback-false-rewrite/next.config.js | 0 .../fallback-false-rewrite/pages/[slug].js | 0 .../fallback-false-rewrite/pages/another.js | 0 .../fallback-route-params.test.ts | 28 + .../fallback-route-params/pages/[slug].js | 0 .../fetch-polyfill-ky-universal/api-server.js | 0 .../api/api-route.js | 0 .../fetch-polyfill-ky-universal.test.ts | 93 + .../pages/getinitialprops.js | 0 .../fetch-polyfill-ky-universal/pages/ssr.js | 0 .../pages/static.js | 0 .../e2e/fetch-polyfill/fetch-polyfill.test.ts | 85 + .../fetch-polyfill/pages/api/api-route.js | 0 .../fetch-polyfill/pages/getinitialprops.js | 0 .../fetch-polyfill/pages/ssr.js | 0 .../fetch-polyfill/pages/static.js | 0 .../fetch-polyfill/pages/user/[username].js | 0 .../file-serving/file-serving.test.ts} | 207 +- .../file-serving}/pages/index.js | 0 .../file-serving/public/hello world.txt | 0 .../file-serving/public/vercel-icon-dark.avif | Bin .../file-serving/static/hello world.txt | 0 .../file-serving/test-file.txt | 0 .../filesystempublicroutes.test.ts | 42 + .../filesystempublicroutes/next.config.js | 0 .../pages/exportpathmap-route.js | 0 .../filesystempublicroutes/pages/index.js | 0 .../filesystempublicroutes/public/hello.txt | 0 .../filesystempublicroutes/server.js | 22 +- .../getinitialprops/getinitialprops.test.ts | 29 + .../getinitialprops/next.config.js | 0 .../getinitialprops/pages/blog/[post].js | 0 .../getinitialprops/pages/index.js | 0 .../getinitialprops/pages/normal.js | 0 .../getserversideprops-preview.test.ts | 251 ++ .../pages/api/preview.js | 0 .../pages/api/reset.js | 0 .../getserversideprops-preview/pages/index.js | 0 .../pages/to-index.js | 0 .../e2e/gip-identifier/gip-identifier.test.ts | 71 + .../gip-identifier}/pages/index.js | 0 .../gssp-pageProps-merge.test.ts | 19 + .../gssp-pageProps-merge/pages/_app.js | 0 .../gssp-pageProps-merge/pages/gsp.js | 0 .../gssp-pageProps-merge/pages/gssp.js | 0 .../gssp-redirect-base-path.test.ts} | 347 +-- .../gssp-redirect-base-path/next.config.js | 0 .../gssp-redirect-base-path/pages/404.js | 0 .../gssp-redirect-base-path/pages/another.js | 0 .../pages/gsp-blog/[post].js | 0 .../pages/gssp-blog/[post].js | 0 .../gssp-redirect-base-path/pages/index.js | 0 .../gssp-redirect/gssp-redirect.test.ts} | 410 ++-- .../gssp-redirect/pages/404.js | 0 .../gssp-redirect/pages/another.js | 0 .../pages/gsp-blog-blocking/[post].js | 0 .../gssp-redirect/pages/gsp-blog/[post].js | 0 .../gssp-redirect/pages/gssp-blog/[post].js | 0 .../gssp-redirect/pages/index.js | 0 test/e2e/hashbang/hashbang.test.ts | 24 + .../hashbang/src/cases/cjs.cjs | 0 .../hashbang/src/cases/js.js | 0 .../hashbang/src/cases/mjs.mjs | 0 .../hashbang/src/pages/index.js | 0 test/e2e/hydration/hydration.test.ts | 29 + .../hydration/pages/404.js | 0 .../hydration/pages/_app.js | 0 .../hydration/pages/_document.js | 0 .../hydration/pages/details.js | 0 .../hydration/pages/index.js | 0 .../i18n-support-base-path.test.ts | 161 ++ .../i18n-support-base-path/next.config.js | 0 .../i18n-support-base-path/pages/404.js | 0 .../pages/[post]/[comment].js | 0 .../pages/[post]/index.js | 0 .../i18n-support-base-path/pages/_app.js | 0 .../i18n-support-base-path/pages/another.js | 0 .../i18n-support-base-path/pages/api/hello.js | 0 .../pages/api/post/[slug].js | 0 .../pages/auto-export/index.js | 0 .../pages/developments/index.js | 0 .../pages/dynamic/[slug].js | 0 .../i18n-support-base-path/pages/frank.js | 0 .../pages/gsp/fallback/[slug].js | 0 .../i18n-support-base-path/pages/gsp/index.js | 0 .../pages/gsp/no-fallback/[slug].js | 0 .../pages/gssp/[slug].js | 0 .../pages/gssp/index.js | 0 .../i18n-support-base-path/pages/index.js | 0 .../i18n-support-base-path/pages/links.js | 0 .../pages/locale-false.js | 0 .../i18n-support-base-path/pages/mixed.js | 0 .../not-found/blocking-fallback/[slug].js | 0 .../pages/not-found/fallback/[slug].js | 0 .../pages/not-found/index.js | 0 .../public/files/texts/file.txt | 0 .../i18n-support-catchall.test.ts} | 133 +- .../i18n-support-catchall/next.config.js | 0 .../pages/[[...slug]].js | 0 .../i18n-support-custom-error.test.ts} | 95 +- .../i18n-support-custom-error/next.config.js | 0 .../i18n-support-custom-error/pages/[slug].js | 0 .../i18n-support-custom-error/pages/_error.js | 0 .../i18n-support-custom-error/pages/index.js | 0 ...8n-support-fallback-rewrite-legacy.test.ts | 79 + .../next.config.js | 0 .../pages/another.js | 0 .../pages/dynamic/[slug].js | 0 .../pages/index.js | 0 .../i18n-support-fallback-rewrite.test.ts | 79 + .../next.config.js | 0 .../pages/another.js | 0 .../pages/dynamic/[slug].js | 0 .../pages/index.js | 0 .../i18n-support-index-rewrite.test.ts} | 65 +- .../i18n-support-index-rewrite/next.config.js | 0 .../pages/[...slug].js | 0 ...i18n-support-same-page-hash-change.test.ts | 92 + .../next.config.js | 0 .../pages/about.js | 0 .../pages/posts/[...slug].js | 0 .../i18n-support/i18n-support.test.ts} | 518 ++-- .../i18n-support/next.config.js | 0 .../i18n-support/pages/404.js | 0 .../i18n-support/pages/[post]/[comment].js | 0 .../i18n-support/pages/[post]/index.js | 0 .../i18n-support/pages/_app.js | 15 +- .../i18n-support/pages/another.js | 0 .../i18n-support/pages/api/hello.js | 0 .../i18n-support/pages/api/post/[slug].js | 0 .../i18n-support/pages/auto-export/index.js | 0 .../i18n-support/pages/developments/index.js | 0 .../i18n-support/pages/dynamic/[slug].js | 0 .../i18n-support/pages/frank.js | 0 .../i18n-support/pages/gsp/fallback/[slug].js | 0 .../i18n-support/pages/gsp/index.js | 0 .../pages/gsp/no-fallback/[slug].js | 0 .../i18n-support/pages/gssp/[slug].js | 0 .../i18n-support/pages/gssp/index.js | 0 .../i18n-support/pages/index.js | 0 .../i18n-support/pages/links.js | 0 .../i18n-support/pages/locale-false.js | 0 .../i18n-support/pages/mixed.js | 0 .../not-found/blocking-fallback/[slug].js | 0 .../pages/not-found/fallback/[slug].js | 0 .../i18n-support/pages/not-found/index.js | 0 .../i18n-support/public/files/texts/file.txt | 0 .../test => e2e/i18n-support}/shared.ts | 43 +- .../image-optimizer/app/.gitignore | 0 .../app/pages/api/application.svg.js | 0 .../app/pages/api/comma.svg.js | 0 .../app/pages/api/conditional-cookie.js | 0 .../app/pages/api/no-header.js | 0 .../app/pages/api/stateful/test.png.js | 0 .../app/pages/api/uppercase.svg.js | 0 .../app/pages/api/wrong-header.svg.js | 0 .../image-optimizer/app/pages/index.js | 0 .../image-optimizer/app/public/animated.gif | Bin .../image-optimizer/app/public/animated.png | Bin .../image-optimizer/app/public/animated.webp | Bin .../image-optimizer/app/public/animated2.png | Bin .../image-optimizer/app/public/grayscale.png | Bin .../image-optimizer/app/public/mountains.jpg | Bin .../app/public/png-as-octet-stream | Bin .../image-optimizer/app/public/test.avif | Bin .../image-optimizer/app/public/test.bmp | Bin .../image-optimizer/app/public/test.gif | Bin .../image-optimizer/app/public/test.heic | Bin .../image-optimizer/app/public/test.icns | Bin .../image-optimizer/app/public/test.ico | Bin .../image-optimizer/app/public/test.jp2 | Bin .../image-optimizer}/app/public/test.jpg | Bin .../image-optimizer/app/public/test.jxl | Bin .../image-optimizer/app/public/test.pdf | Bin .../image-optimizer/app/public/test.pic | Bin .../image-optimizer/app/public/test.png | Bin .../image-optimizer/app/public/test.svg | 0 .../image-optimizer/app/public/test.tiff | Bin .../image-optimizer/app}/public/test.webp | Bin .../image-optimizer/app/public/text.txt | 0 ...3\274\305\241\304\215\305\231\303\255.png" | Bin .../content-disposition-type.test.ts | 4 - .../dangerously-allow-svg.test.ts | 4 - .../disable-write-to-cache-dir.test.ts | 4 - .../image-optimizer/image-optimizer.test.ts | 534 +++++ .../max-disk-size-cache-85kb.test.ts | 4 - .../max-disk-size-cache-zero.test.ts | 4 - .../maximum-redirects-0.test.ts | 6 - .../maximum-redirects-1.test.ts | 6 - .../minimum-cache-ttl.test.ts | 8 - test/e2e/image-optimizer/sharp.test.ts | 5 + .../test => e2e/image-optimizer}/util.ts | 662 ++--- .../import-assertion/data | 0 .../import-assertion/data.d.ts | 0 .../import-assertion/import-assertion.test.ts | 16 + .../import-assertion}/pages/es.js | 0 .../import-assertion}/pages/ts.ts | 0 .../import-attributes/data | 0 .../import-attributes/data.d.ts | 0 .../import-attributes.test.ts | 16 + .../import-attributes}/pages/es.js | 2 +- .../import-attributes}/pages/ts.ts | 2 +- test/e2e/index-index/index-index.test.ts | 144 ++ .../future => e2e/index-index}/next.config.js | 0 .../index-index/pages/index.js | 0 .../index-index/pages/index/index.js | 0 .../index-index/pages/index/index/index.js | 0 .../index-index/pages/index/project/index.js | 0 .../index-index/pages/index/user.js | 0 .../index-index/pages/links.js | 0 test/e2e/initial-ref/initial-ref.test.ts | 12 + .../initial-ref/pages/index.js | 0 .../invalid-custom-routes.test.ts | 567 +++++ .../invalid-custom-routes}/next.config.js | 0 .../invalid-custom-routes}/pages/index.js | 0 test/e2e/invalid-href/invalid-href.test.ts | 183 ++ .../invalid-href/pages/[post].js | 0 .../pages/dynamic-route-mismatch-manual.js | 0 .../pages/dynamic-route-mismatch.js | 0 .../invalid-href/pages/exotic-href.js | 0 .../invalid-href/pages/first.js | 0 .../invalid-href/pages/index.js | 0 .../invalid-href/pages/invalid-relative.js | 0 .../invalid-href/pages/second.js | 0 .../invalid-href/pages/third.js | 0 .../invalid-middleware-matchers.test.ts | 209 ++ .../pages/index.js | 0 .../invalid-multi-match.test.ts | 26 + .../invalid-multi-match/next.config.js | 0 .../invalid-multi-match/pages/hello.js | 0 .../invalid-server-options.test.ts} | 4 +- .../invalid-server-options/pages/index.js | 0 .../jsconfig-baseurl/components/world.js | 0 .../jsconfig-baseurl/jsconfig-baseurl.test.ts | 59 + .../jsconfig-baseurl/jsconfig.json | 0 .../jsconfig-baseurl}/next.config.js | 0 .../jsconfig-baseurl/pages/hello.js | 0 .../jsconfig-paths/.gitignore | 0 .../jsconfig-paths/components/hello.js | 0 .../jsconfig-paths/components/world.js | 0 .../e2e/jsconfig-paths/jsconfig-paths.test.ts | 215 ++ .../jsconfig-paths/jsconfig.json | 0 .../jsconfig-paths/lib/a/api.js | 0 .../jsconfig-paths/lib/b/api.js | 0 .../jsconfig-paths/lib/b/b-only.js | 0 .../jsconfig-paths/next.config.js | 0 .../node_modules/mypackage/data.js | 0 .../node_modules/mypackage/myfile.js | 0 .../jsconfig-paths/pages/basic-alias.js | 0 .../jsconfig-paths/pages/resolve-fallback.js | 0 .../jsconfig-paths/pages/resolve-order.js | 0 .../jsconfig-paths/pages/single-alias.js | 0 .../validations.console.test.ts | 6 +- .../app/child-ref-func-cleanup/page.js | 0 .../link-ref-app/app/child-ref-func/page.js | 0 .../link-ref-app/app/child-ref/page.js | 0 .../link-ref-app/app/class/page.js | 0 .../app/click-away-race-condition/page.js | 0 .../link-ref-app/app/function/page.js | 0 .../link-ref-app/app/layout.js | 0 .../link-ref-app/app/page.js | 0 test/e2e/link-ref-app/link-ref-app.test.ts | 97 + .../e2e/link-ref-pages/link-ref-pages.test.ts | 98 + .../pages/child-ref-func-cleanup.js | 0 .../link-ref-pages/pages/child-ref-func.js | 0 .../link-ref-pages/pages/child-ref.js | 0 .../link-ref-pages/pages/class.js | 0 .../pages/click-away-race-condition.js | 0 .../link-ref-pages/pages/function.js | 0 .../link-ref-pages}/pages/index.js | 0 .../middleware-basic/middleware-basic.test.ts | 14 + .../middleware-basic/middleware.ts | 0 .../middleware-basic/next.config.js | 0 .../middleware-basic/pages/index.js | 0 .../middleware-src-node.test.ts | 113 + .../middleware-src-node}/next.config.js | 0 .../middleware-src-node/src/middleware.js | 0 .../middleware-src-node/src/middleware.ts | 0 .../middleware-src-node/src/pages/index.js | 0 .../e2e/middleware-src/middleware-src.test.ts | 114 + .../middleware-src/src/middleware.js | 0 .../middleware-src/src/middleware.ts | 0 .../middleware-src/src/pages/index.js | 0 .../module-ids/components/CustomComponent.tsx | 0 test/e2e/module-ids/module-ids.test.ts | 121 + .../module-ids/module-with-long-name.js | 0 .../module-ids/next.config.js | 0 .../external-module-with-long-name.js | 0 .../module-ids/pages/index.js | 0 test/e2e/next-analyze/next-analyze.test.ts | 109 +- .../next-dynamic-css-asset-prefix.test.ts | 105 + .../next.config.js | 0 .../src/Component2.jsx | 0 .../src/Component2.module.scss | 0 .../src/Content.jsx | 0 .../src/Content.module.css | 0 .../src/Content4.module.css | 0 .../src/app/layout.tsx | 0 .../src/app/test-app/page.tsx | 0 .../src/inner/k.jsx | 0 .../src/pages/index.jsx | 0 .../next-dynamic-css/next-dynamic-css.test.ts | 38 + .../next-dynamic-css}/next.config.js | 0 .../next-dynamic-css/src/Component2.jsx | 0 .../src/Component2.module.scss | 0 .../next-dynamic-css/src/Content.jsx | 0 .../next-dynamic-css/src/Content.module.css | 0 .../next-dynamic-css/src/Content4.module.css | 0 .../next-dynamic-css/src/app/layout.tsx | 0 .../src/app/test-app/page.tsx | 0 .../next-dynamic-css/src/inner/k.jsx | 0 .../next-dynamic-css/src/pages/index.jsx | 0 .../next-dynamic-lazy-compilation/.babelrc | 0 .../apples/index.js | 0 .../components/four.js | 0 .../components/one.js | 0 .../components/three.js | 0 .../components/two.js | 0 .../next-dynamic-lazy-compilation.test.ts | 40 + .../next.config.js | 0 .../pages/index.js | 0 .../next-dynamic/apples/index.js | 0 .../next-dynamic/components/four.js | 0 .../next-dynamic/components/one.js | 0 .../next-dynamic/components/three.js | 0 .../next-dynamic/components/two.js | 0 test/e2e/next-dynamic/next-dynamic.test.ts | 27 + .../next-dynamic/pages/index.js | 0 .../asset-prefix/asset-prefix.test.ts | 33 + .../asset-prefix/next.config.js | 0 .../asset-prefix/pages/index.js | 0 .../asset-prefix}/public/test.jpg | Bin .../base-path/base-path-static.test.ts} | 139 +- .../base-path/base-path.test.ts | 399 ++++ .../base-path/components/TallImage.js | 0 .../base-path/components/tall.png | Bin .../base-path/next.config.js | 0 .../next-image-legacy/base-path/pages/flex.js | 0 .../base-path/pages/hidden-parent.js | 0 .../base-path/pages/index.js | 0 .../pages/invalid-src-proto-relative.js | 0 .../base-path/pages/invalid-src.js | 0 .../base-path/pages/layout-fill.js | 0 .../base-path/pages/layout-fixed.js | 0 .../base-path/pages/layout-intrinsic.js | 0 .../base-path/pages/layout-responsive.js | 0 .../base-path/pages/missing-src.js | 0 .../base-path/pages/prose.js | 0 .../base-path/pages/prose.module.css | 0 .../base-path/pages/rotated.js | 0 .../base-path/pages/sizes.js | 0 .../base-path/pages/static-img.js | 0 .../base-path/pages/update.js | 0 .../base-path/public/exif-rotation.jpg | Bin .../base-path/public/foo/test-rect.jpg | Bin .../base-path/public/test.avif | Bin .../base-path/public/test.bmp | Bin .../base-path/public/test.gif | Bin .../base-path/public/test.ico | Bin .../base-path}/public/test.jpg | Bin .../base-path/public/test.png | Bin .../base-path/public/test.svg | 0 .../base-path/public/test.tiff | Bin .../base-path}/public/test.webp | Bin .../base-path/public/wide.png | Bin .../default/components/TallImage.js | 0 .../default/components/static-img.js | 0 .../default/components/tall.png | Bin .../default/default-static.test.ts | 190 ++ .../next-image-legacy/default/default.test.ts | 1302 ++++++++++ .../default/pages/_document.js | 0 .../next-image-legacy/default/pages/blob.js | 0 .../default/pages/blurry-placeholder.js | 0 .../default/pages/drop-srcset.js | 0 .../default/pages/dynamic-static-img.js | 0 .../next-image-legacy/default/pages/flex.js | 0 .../default/pages/hidden-parent.js | 0 .../next-image-legacy/default/pages/index.js | 0 .../default/pages/inside-paragraph.js | 0 .../default/pages/invalid-loader.js | 0 .../pages/invalid-placeholder-blur-static.js | 0 .../default/pages/invalid-placeholder-blur.js | 0 .../default/pages/invalid-sizes.js | 0 .../pages/invalid-src-proto-relative.js | 0 .../default/pages/invalid-src.js | 0 .../default/pages/invalid-unsized.js | 0 .../default/pages/invalid-width-or-height.js | 0 .../pages/layout-fill-inside-nonrelative.js | 0 .../default/pages/layout-fill.js | 0 .../default/pages/layout-fixed.js | 0 .../default/pages/layout-intrinsic.js | 0 .../pages/layout-responsive-inside-flex.js | 0 .../default/pages/layout-responsive.js | 0 .../default/pages/lazy-src-change.js | 0 .../default/pages/lazy-withref.js | 0 .../default/pages/loader-svg.js | 0 .../default/pages/missing-src.js | 0 .../default/pages/on-error.js | 0 .../default/pages/on-load.js | 0 .../default/pages/on-loading-complete.js | 0 .../default/pages/priority-missing-warning.js | 0 .../default/pages/priority.js | 0 .../next-image-legacy/default/pages/prose.js | 0 .../default/pages/prose.module.css | 0 .../default/pages/quality-50.js | 0 .../default/pages/rotated.js | 0 .../next-image-legacy/default/pages/sizes.js | 0 .../default/pages/small-img-import.js | 0 .../default/pages/static-img.js | 0 .../default/pages/style-filter.js | 0 .../default/pages/style-inheritance.js | 0 .../default/pages/style-prop.js | 0 .../next-image-legacy/default/pages/update.js | 0 .../default/pages/valid-html-w3c.js | 0 .../default/pages/warning-once.js | 0 .../default/public/exif-rotation.jpg | Bin .../default/public/foo/test-rect.jpg | Bin .../default}/public/small.jpg | Bin .../default/public/test.avif | Bin .../next-image-legacy/default/public/test.bmp | Bin .../next-image-legacy/default/public/test.gif | Bin .../next-image-legacy/default/public/test.ico | Bin .../default}/public/test.jpg | Bin .../next-image-legacy/default/public/test.png | Bin .../next-image-legacy/default/public/test.svg | 0 .../default/public/test.tiff | Bin .../default}/public/test.webp | Bin .../next-image-legacy/default/public/wide.png | Bin .../default/style.module.css | 0 .../image-from-node-modules.test.ts | 14 + .../image-from-node-modules/next.config.js | 0 .../node_modules/my-cool-image/index.js | 0 .../node_modules/my-cool-image/package.json | 0 .../pages/image-from-node-modules.js | 0 .../trailing-slash/next.config.js | 0 .../trailing-slash/pages/index.js | 0 .../trailing-slash}/public/test.jpg | Bin .../trailing-slash/trailing-slash.test.ts | 24 + .../typescript/components/image-card.tsx | 0 .../components/image-dynamic-src.tsx | 0 .../typescript/next.config.js | 0 .../typescript/pages/invalid.tsx | 0 .../typescript/pages/valid.tsx | 0 .../typescript/public/tall.png | Bin .../typescript/public/test.avif | Bin .../typescript/public/test.svg | 0 .../typescript/typescript.test.ts | 99 + .../next-image-legacy/unicode/next.config.js | 0 .../next-image-legacy/unicode/pages/index.js | 0 .../unicode/public/hello world.jpg | Bin ...3\274\305\241\304\215\305\231\303\255.png" | Bin .../unicode/unicode.test.ts} | 76 +- .../unoptimized/next.config.js | 0 .../unoptimized/pages/index.js | 0 .../unoptimized}/public/test.jpg | Bin .../unoptimized/public/test.png | Bin .../unoptimized}/public/test.webp | Bin .../unoptimized/unoptimized.test.ts} | 76 +- .../app-dir-image-from-node-modules.test.ts | 20 + .../app/layout.js | 0 .../app/page.js | 0 .../next.config.js | 0 .../node_modules/my-cool-image/index.js | 0 .../node_modules/my-cool-image/package.json | 0 .../app-dir-localpatterns.test.ts} | 96 +- .../app/does-not-exist/page.js | 0 .../app/images/static-img.png | Bin .../app-dir-localpatterns/app/layout.js | 0 .../app/nested-assets-query/page.js | 0 .../app/nested-blocked/page.js | 0 .../app-dir-localpatterns/app/page.js | 0 .../app/top-level/page.js | 0 .../app-dir-localpatterns/next.config.js | 0 .../public/assets/test.png | Bin .../public/blocked/test.png | Bin .../app-dir-localpatterns/public/test.png | Bin .../app-dir-localpatterns/style.module.css | 0 .../app-dir-qualities.test.ts} | 116 +- .../app-dir-qualities/app/images/test.png | Bin .../app/invalid-quality/page.js | 0 .../app-dir-qualities/app/layout.js | 0 .../app-dir-qualities/app/page.js | 0 .../app-dir-qualities/next.config.js | 0 .../app-dir/app-dir-static.test.ts | 270 +++ .../next-image-new/app-dir/app-dir.test.ts} | 1719 +++++++------ .../next-image-new/app-dir/app/blob/page.js | 0 .../app-dir/app/blurry-placeholder/page.js | 0 .../app-dir/app/data-url-placeholder/page.js | 0 .../app/data-url-with-fill-and-sizes/page.js | 0 .../app-dir/app/drop-srcset/page.js | 0 .../app/dynamic-static-img/async-image.js | 0 .../app-dir/app/dynamic-static-img/page.js | 0 .../app-dir/app/empty-string-src/page.js | 0 .../app-dir/app/fill-blur/page.js | 0 .../app/fill-data-url-placeholder/page.js | 0 .../app-dir/app/fill-warnings/page.js | 0 .../next-image-new/app-dir/app/fill/page.js | 0 .../next-image-new/app-dir/app/flex/page.js | 0 .../app-dir/app/hidden-parent/page.js | 0 .../app-dir/app/inside-paragraph/page.js | 0 .../app/invalid-Infinity-width/page.js | 0 .../app-dir/app/invalid-fill-position/page.js | 0 .../app-dir/app/invalid-fill-width/page.js | 0 .../app-dir/app/invalid-height/page.js | 0 .../app-dir/app/invalid-loader/page.js | 0 .../invalid-placeholder-blur-static/page.js | 0 .../app/invalid-placeholder-blur/page.js | 0 .../app/invalid-src-leading-space/page.js | 0 .../app-dir/app/invalid-src-null/page.js | 0 .../app/invalid-src-proto-relative/page.js | 0 .../app/invalid-src-trailing-space/page.js | 0 .../app-dir/app/invalid-src/page.js | 0 .../app-dir/app/invalid-width/page.js | 0 .../next-image-new/app-dir/app/layout.js | 0 .../app-dir/app/legacy-layout-fill/page.js | 0 .../app/legacy-layout-responsive/page.js | 0 .../app-dir/app/loader-svg/page.js | 0 .../app-dir/app/missing-alt/page.js | 0 .../app-dir/app/missing-height/page.js | 0 .../app-dir/app/missing-src/page.js | 0 .../app-dir/app/missing-width/page.js | 0 .../app/on-error-before-hydration/page.js | 0 .../app-dir/app/on-error/page.js | 0 .../app-dir/app/on-load/page.js | 0 .../app-dir/app/on-loading-complete/page.js | 0 .../app-dir/app/override-src/page.js | 0 .../next-image-new/app-dir/app/page.js | 0 .../app-dir/app/picture/page.js | 0 .../app-dir/app/placeholder-blur/page.js | 0 .../app/preload-missing-warning/page.js | 0 .../app-dir/app/preload/page.js | 0 .../app-dir/app/priority/page.js | 0 .../next-image-new/app-dir/app/prose/page.js | 0 .../app-dir/app/prose/prose.module.css | 0 .../app-dir/app/quality-50/page.js | 0 .../app-dir/app/ref-cleanup/page.js | 0 .../app-dir/app/rotated/page.js | 0 .../app/should-not-warn-unmount/page.js | 0 .../next-image-new/app-dir/app/sizes/page.js | 0 .../app-dir/app/small-img-import/page.js | 0 .../app-dir/app/static-img/page.js | 0 .../app-dir/app/style-filter/page.js | 0 .../app-dir/app/style-inheritance/page.js | 0 .../app-dir/app/style-prop/page.js | 0 .../next-image-new/app-dir/app/update/page.js | 0 .../app-dir/app/valid-html-w3c/page.js | 0 .../app-dir/app/warning-once/page.js | 0 .../app-dir/app/wrapper-div/page.js | 0 .../app-dir/components/TallImage.js | 0 .../app-dir/components/static-img.js | 0 .../app-dir/components/tall.png | Bin .../next-image-new/app-dir}/next.config.js | 0 .../app-dir/public/exif-rotation.jpg | Bin .../app-dir/public/foo/test-rect.jpg | Bin .../next-image-new/app-dir}/public/small.jpg | Bin .../app-dir/public/super-wide.png | Bin .../next-image-new/app-dir/public/test.avif | Bin .../next-image-new/app-dir/public/test.bmp | Bin .../next-image-new/app-dir/public/test.gif | Bin .../next-image-new/app-dir/public/test.ico | Bin .../next-image-new/app-dir}/public/test.jpg | Bin .../next-image-new/app-dir/public/test.png | Bin .../next-image-new/app-dir/public/test.svg | 0 .../next-image-new/app-dir/public/test.tiff | Bin .../next-image-new/app-dir}/public/test.webp | Bin .../app-dir/public/test_light.png | Bin .../next-image-new/app-dir/public/wide.png | Bin .../next-image-new/app-dir/style.module.css | 0 .../asset-prefix/asset-prefix.test.ts | 40 + .../asset-prefix/next.config.js | 0 .../asset-prefix/pages/index.js | 0 .../asset-prefix}/public/test.jpg | Bin .../base-path/base-path-static.test.ts} | 165 +- .../base-path/base-path.test.ts | 151 ++ .../base-path/components/TallImage.js | 0 .../base-path/components/tall.png | Bin .../next-image-new/base-path/next.config.js | 0 .../next-image-new/base-path/pages/flex.js | 0 .../base-path/pages/hidden-parent.js | 0 .../next-image-new/base-path/pages/index.js | 0 .../pages/invalid-src-proto-relative.js | 0 .../base-path/pages/invalid-src.js | 0 .../base-path/pages/missing-src.js | 0 .../next-image-new/base-path/pages/prose.js | 0 .../base-path/pages/prose.module.css | 0 .../next-image-new/base-path/pages/sizes.js | 0 .../base-path/pages/static-img.js | 0 .../next-image-new/base-path/pages/update.js | 0 .../base-path/public/exif-rotation.jpg | Bin .../base-path/public/foo/test-rect.jpg | Bin .../next-image-new/base-path/public/test.avif | Bin .../next-image-new/base-path/public/test.bmp | Bin .../next-image-new/base-path/public/test.gif | Bin .../next-image-new/base-path/public/test.ico | Bin .../next-image-new/base-path}/public/test.jpg | Bin .../next-image-new/base-path/public/test.png | Bin .../next-image-new/base-path/public/test.svg | 0 .../next-image-new/base-path/public/test.tiff | Bin .../base-path}/public/test.webp | Bin .../next-image-new/base-path/public/wide.png | Bin .../both-basepath-trailingslash.test.ts | 55 + .../next.config.js | 0 .../pages/index.js | 0 .../public/test.jpg | Bin .../default/components/TallImage.js | 0 .../default/components/static-img.js | 0 .../default/components/tall.png | Bin .../default/default-static.test.ts} | 164 +- .../next-image-new/default/default.test.ts} | 1732 +++++++------- .../next-image-new/default/pages/_document.js | 0 .../next-image-new/default/pages/blob.js | 0 .../default/pages/blurry-placeholder.js | 0 .../default/pages/data-url-placeholder.js | 0 .../pages/data-url-with-fill-and-sizes.js | 0 .../default/pages/drop-srcset.js | 0 .../default/pages/dynamic-static-img.js | 0 .../next-image-new/default/pages/edge.js | 0 .../default/pages/empty-string-src.js | 0 .../next-image-new/default/pages/fill-blur.js | 0 .../pages/fill-data-url-placeholder.js | 0 .../default/pages/fill-warnings.js | 0 .../next-image-new/default/pages/fill.js | 0 .../next-image-new/default/pages/flex.js | 0 .../default/pages/hidden-parent.js | 0 .../next-image-new/default/pages/index.js | 0 .../default/pages/inside-paragraph.js | 0 .../default/pages/invalid-Infinity-width.js | 0 .../default/pages/invalid-fill-position.js | 0 .../default/pages/invalid-fill-width.js | 0 .../default/pages/invalid-height.js | 0 .../default/pages/invalid-loader.js | 0 .../pages/invalid-placeholder-blur-static.js | 0 .../default/pages/invalid-placeholder-blur.js | 0 .../pages/invalid-src-leading-space.js | 0 .../default/pages/invalid-src-null.js | 0 .../pages/invalid-src-proto-relative.js | 0 .../pages/invalid-src-trailing-space.js | 0 .../default/pages/invalid-src.js | 0 .../default/pages/invalid-width.js | 0 .../default/pages/legacy-layout-fill.js | 0 .../default/pages/legacy-layout-responsive.js | 0 .../default/pages/loader-svg.js | 0 .../default/pages/missing-alt.js | 0 .../default/pages/missing-height.js | 0 .../default/pages/missing-src.js | 0 .../default/pages/missing-width.js | 0 .../pages/on-error-before-hydration.js | 0 .../next-image-new/default/pages/on-error.js | 0 .../next-image-new/default/pages/on-load.js | 0 .../default/pages/on-loading-complete.js | 0 .../default/pages/override-src.js | 0 .../next-image-new/default/pages/picture.js | 0 .../default/pages/placeholder-blur.js | 0 .../default/pages/preload-missing-warning.js | 0 .../next-image-new/default/pages/preload.js | 0 .../next-image-new/default/pages/priority.js | 0 .../next-image-new/default/pages/prose.js | 0 .../default/pages/prose.module.css | 0 .../default/pages/quality-50.js | 0 .../next-image-new/default/pages/rotated.js | 0 .../default/pages/should-not-warn-unmount.js | 0 .../next-image-new/default/pages/sizes.js | 0 .../default/pages/small-img-import.js | 0 .../default/pages/static-img.js | 0 .../default/pages/style-filter.js | 0 .../default/pages/style-inheritance.js | 0 .../default/pages/style-prop.js | 0 .../next-image-new/default/pages/update.js | 0 .../default/pages/valid-html-w3c.js | 0 .../default/pages/warning-once.js | 0 .../default/pages/wrapper-div.js | 0 .../default/public/exif-rotation.jpg | Bin .../default/public/foo/test-rect.jpg | Bin .../next-image-new/default}/public/small.jpg | Bin .../default/public/super-wide.png | Bin .../next-image-new/default/public/test.avif | Bin .../next-image-new/default/public/test.bmp | Bin .../next-image-new/default/public/test.gif | Bin .../next-image-new/default/public/test.ico | Bin .../next-image-new/default}/public/test.jpg | Bin .../next-image-new/default/public/test.png | Bin .../next-image-new/default/public/test.svg | 0 .../next-image-new/default/public/test.tiff | Bin .../next-image-new/default}/public/test.webp | Bin .../default/public/test_light.png | Bin .../next-image-new/default/public/wide.png | Bin .../next-image-new/default/style.module.css | 0 .../image-from-node-modules.test.ts | 26 + .../image-from-node-modules/next.config.js | 0 .../node_modules/my-cool-image/index.js | 0 .../node_modules/my-cool-image/package.json | 0 .../image-from-node-modules/pages/index.js | 0 .../dummy-loader.js | 0 ...er-config-default-loader-with-file.test.ts | 63 + .../next.config.js | 0 .../pages/get-img-props.js | 0 .../pages/index.js | 0 .../public/logo.png | Bin .../dummy-loader.js | 0 .../loader-config-edge-runtime.test.ts | 35 + .../loader-config-edge-runtime/next.config.js | 0 .../loader-config-edge-runtime/pages/index.js | 0 .../public/logo.png | Bin .../loader-config}/dummy-loader.js | 0 .../loader-config/loader-config.test.ts | 51 + .../loader-config/next.config.js | 0 .../loader-config/pages/get-img-props.js | 0 .../loader-config/pages/index.js | 0 .../loader-config/public/logo.png | Bin .../trailing-slash/next.config.js | 0 .../trailing-slash/pages/index.js | 0 .../trailing-slash}/public/test.jpg | Bin .../trailing-slash/trailing-slash.test.ts | 21 + .../typescript/components/image-card.tsx | 0 .../components/image-dynamic-src.tsx | 0 .../components/image-with-loader.tsx | 0 .../next-image-new/typescript/next.config.js | 0 .../typescript/pages/invalid.tsx | 0 .../next-image-new/typescript/pages/valid.tsx | 0 .../next-image-new/typescript/public/tall.png | Bin .../typescript/public/test.avif | Bin .../next-image-new/typescript/public/test.svg | 0 .../typescript/typescript.test.ts | 83 + .../next-image-new/unicode/next.config.js | 0 .../next-image-new/unicode/pages/index.js | 0 .../unicode/public/hello world.jpg | Bin ...3\274\305\241\304\215\305\231\303\255.png" | Bin .../next-image-new/unicode/unicode.test.ts} | 95 +- .../next-image-new/unoptimized/next.config.js | 0 .../unoptimized/pages/get-img-props.js | 0 .../next-image-new/unoptimized/pages/index.js | 0 .../unoptimized}/public/test.jpg | Bin .../unoptimized/public/test.png | Bin .../unoptimized/public/test.webp | Bin .../unoptimized/unoptimized.test.ts | 140 ++ .../no-page-props/no-page-props.test.ts} | 58 +- .../no-page-props/pages/_app.js | 0 .../no-page-props/pages/gsp.js | 0 .../no-page-props/pages/gssp.js | 0 .../no-page-props/pages/index.js | 0 .../node-fetch-keep-alive.test.ts | 54 + .../node-fetch-keep-alive/pages/api/json.js | 0 .../pages/blog/[slug].js | 0 .../node-fetch-keep-alive/pages/ssg.js | 0 .../node-fetch-keep-alive/pages/ssr.js | 0 .../non-standard-node-env-warning.test.ts | 150 ++ .../pages/index.js | 0 .../non-standard-node-env-warning/server.js | 17 +- .../nullish-config}/next.config.js | 0 .../e2e/nullish-config/nullish-config.test.ts | 64 + .../nullish-config}/pages/index.js | 0 ...tional-chaining-nullish-coalescing.test.ts | 19 + .../pages/nullish-coalescing.js | 0 .../pages/optional-chaining.js | 0 .../pages-ssg-data-deployment-skew.test.ts | 153 +- .../port-env-var}/pages/index.js | 0 test/e2e/port-env-var/port-env-var.test.ts | 10 + .../next.config.js | 0 .../pages/fallback-blocking/[slug].js | 0 .../pages/fallback-false/[slug].js | 0 .../pages/fallback-true/[slug].js | 0 .../prerender-fallback-encoding/paths.js | 0 .../prerender-fallback-encoding.test.ts} | 243 +- .../prerender-preview/pages/api/preview.js | 0 .../prerender-preview/pages/api/read.js | 0 .../prerender-preview/pages/api/reset.js | 0 .../prerender-preview/pages/index.js | 0 .../prerender-preview/pages/to-index.js | 0 .../prerender-preview.test.ts | 371 +++ .../preview-fallback/pages/api/disable.js | 0 .../preview-fallback/pages/api/enable.js | 0 .../preview-fallback/pages/fallback/[post].js | 0 .../preview-fallback/pages/index.js | 0 .../pages/no-fallback/[post].js | 0 .../preview-fallback.test.ts} | 186 +- .../app/components/foo.js | 0 .../app/components/red.tsx | 0 .../app/components/streaming-data.js | 0 .../react-current-version/app/next.config.js | 0 .../react-current-version/app/package.json | 7 + .../app/pages/dynamic.js | 0 .../react-current-version/app/pages/index.js | 0 .../app/pages/use-flush-effect/styled-jsx.tsx | 0 .../react-current-version/app/pages/use-id.js | 0 .../react-current-version.test.ts | 149 ++ .../__generated__/pagesAQuery.graphql.ts | 0 .../project-a/next.config.js | 0 .../project-a/pages/api/query.ts | 0 .../project-a/pages/index.tsx | 0 .../__generated__/pagesBQuery.graphql.ts | 0 .../project-b/next.config.js | 0 .../project-b/pages/api/query.ts | 0 .../project-b/pages/index.tsx | 0 .../relay-graphql-swc-multi-project.test.ts | 85 + .../relay.config.js | 0 .../schema.graphql | 0 .../next.config.js | 0 .../pages/api/query.ts | 0 .../pages/index.tsx | 0 .../__generated__/pagesQuery.graphql.ts | 0 .../queries/pagesQuery.js | 0 .../relay-graphql-swc-single-project.test.ts | 22 + .../relay.config.js | 0 .../schema.graphql | 0 .../repeated-slashes/app/next.config.js | 0 .../repeated-slashes/app/pages/_error.js | 0 .../repeated-slashes/app/pages/another.js | 0 .../repeated-slashes/app/pages/index.js | 0 .../repeated-slashes/app/pages/invalid.js | 0 .../repeated-slashes.test.ts} | 326 +-- .../next.config.js | 0 .../pages/dynamic-page/[[...param]].js | 0 .../pages/index.js | 0 .../rewrite-with-browser-history.test.ts | 34 + .../rewrites-client-resolving/next.config.js | 0 .../rewrites-client-resolving/pages/404.js | 0 .../pages/category/[...slug].js | 0 .../pages/category/index.js | 0 .../rewrites-client-resolving/pages/index.js | 0 .../pages/product/[productId].js | 0 .../pages/product/index.js | 0 .../rewrites-client-resolving.test.ts} | 58 +- .../next.config.js | 0 .../pages/index.js | 0 .../rewrites-destination-query-array.test.ts | 12 + .../rewrites-has-condition/next.config.js | 0 .../rewrites-has-condition/pages/another.js | 0 .../rewrites-has-condition/pages/index.js | 0 .../rewrites-has-condition.test.ts} | 53 +- .../rewrites-manual-href-as/next.config.js | 0 .../rewrites-manual-href-as/pages/another.js | 0 .../rewrites-manual-href-as/pages/index.js | 0 .../pages/news/[[...slugs]].js | 0 .../pages/preview/[slug].js | 0 .../rewrites-manual-href-as.test.ts} | 81 +- .../route-index/pages/index/index.js | 0 test/e2e/route-index/route-index.test.ts | 38 + .../route-indexes/pages/api/sub/[id].js | 0 .../route-indexes/pages/api/sub/index.js | 0 .../route-indexes/pages/index.js | 0 .../pages/nested-index/index/index.js | 0 .../route-indexes/pages/sub/[id].js | 0 .../route-indexes/pages/sub/index.js | 0 .../route-indexes/route-indexes.test.ts} | 87 +- .../route-load-cancel}/pages/index.js | 0 .../route-load-cancel/pages/page1.js | 0 .../route-load-cancel}/pages/page2.js | 0 .../route-load-cancel.test.ts | 21 + .../router-hash-navigation/pages/index.js | 0 .../router-hash-navigation.test.ts | 21 + .../router-is-ready-app-gip/pages/_app.js | 0 .../router-is-ready-app-gip/pages/appGip.js | 0 .../router-is-ready-app-gip/pages/gsp.js | 0 .../router-is-ready-app-gip/pages/invalid.js | 0 .../router-is-ready-app-gip.test.ts | 37 + .../pages/auto-export/[slug].js | 0 .../pages/auto-export/index.js | 0 .../router-is-ready/pages/gip.js | 0 .../router-is-ready/pages/gsp.js | 0 .../router-is-ready/pages/gssp.js | 0 .../router-is-ready/pages/invalid.js | 0 .../router-is-ready/router-is-ready.test.ts | 67 + .../router-prefetch/pages/another-page.js | 0 .../router-prefetch/pages/index.js | 0 .../router-prefetch/router-prefetch.test.ts | 31 + .../router-rerender/middleware.js | 0 .../router-rerender/next.config.js | 0 .../router-rerender/pages/index.js | 0 .../router-rerender/router-rerender.test.ts | 25 + .../base => e2e/script-loader}/next.config.js | 0 .../base => e2e/script-loader}/pages/_app.js | 0 .../script-loader}/pages/_document.js | 0 .../base => e2e/script-loader}/pages/index.js | 0 .../base => e2e/script-loader}/pages/page1.js | 0 .../script-loader}/pages/page10.js | 0 .../base => e2e/script-loader}/pages/page3.js | 0 .../base => e2e/script-loader}/pages/page4.js | 2 - .../base => e2e/script-loader}/pages/page5.js | 0 .../base => e2e/script-loader}/pages/page6.js | 0 .../base => e2e/script-loader}/pages/page7.js | 0 .../base => e2e/script-loader}/pages/page8.js | 0 .../base => e2e/script-loader}/pages/page9.js | 0 .../script-loader/partytown-missing.test.ts | 21 + .../partytown-missing/next.config.js | 0 .../partytown-missing/pages/index.js | 0 test/e2e/script-loader/script-loader.test.ts | 255 ++ .../script-loader}/styles/styles.css | 0 .../scroll-back-restoration/next.config.js | 0 .../scroll-back-restoration/pages/another.js | 0 .../scroll-back-restoration/pages/index.js | 0 .../scroll-back-restoration.test.ts} | 65 +- .../scroll-forward-restoration/next.config.js | 0 .../pages/another.js | 0 .../scroll-forward-restoration/pages/index.js | 0 .../scroll-forward-restoration.test.ts | 53 + .../server-asset-modules/my-data.json | 0 .../server-asset-modules/pages/api/test.js | 0 .../server-asset-modules.test.ts | 14 + .../src-dir-support-double-dir/pages/index.js | 0 .../src-dir-support-double-dir.test.ts | 17 + .../src/pages/hello.js | 0 .../src/pages/index.js | 0 .../src-dir-support/src-dir-support.test.ts | 55 + .../src/pages/[name]/[comment].js | 0 .../src/pages/[name]/comments.js | 0 .../src-dir-support/src/pages/[name]/index.js | 0 .../src-dir-support/src/pages/another.js | 0 .../src/pages/blog/[name]/comment/[id].js | 0 .../src-dir-support/src/pages/index.js | 0 .../src/pages/on-mount/[post].js | 0 .../ssg-dynamic-routes-404-page/pages/404.js | 0 .../pages/post/[id].js | 0 .../ssg-dynamic-routes-404-page.test.ts | 19 + .../static-page-name/pages/index.js | 0 .../static-page-name/pages/static.js | 0 .../static-page-name/static-page-name.test.ts | 20 + .../telemetry/.babelrc.default | 0 .../telemetry/.babelrc.plugin | 0 .../telemetry/.babelrc.preset | 0 .../telemetry/_app/layout.jsx | 0 .../funtion-level-use-cache/page.jsx | 0 .../telemetry/_app/use-cache/page.jsx | 0 .../telemetry/_app/use-cache/page2/page.jsx | 0 .../{integration => e2e}/telemetry/adapter.js | 0 .../telemetry/app/app-dir/page.js | 0 .../telemetry/app/hello/page.js | 0 .../telemetry/app/layout.js | 0 test/e2e/telemetry/config.test.ts | 808 +++++++ .../telemetry/jsconfig.swc | 0 .../telemetry/next.config.adapter-path | 0 .../telemetry/next.config.cache-components | 0 .../telemetry/next.config.custom-routes | 0 .../telemetry/next.config.filesystem-cache | 0 .../telemetry/next.config.i18n-images | 0 .../telemetry/next.config.middleware-options | 0 .../telemetry/next.config.next-script-workers | 0 .../telemetry/next.config.optimize-css | 0 .../telemetry/next.config.reactCompiler-base | 0 .../next.config.reactCompiler-options | 0 .../telemetry/next.config.swc | 0 .../telemetry/next.config.swc-plugins | 0 .../telemetry/next.config.transpile-packages | 0 .../telemetry/next.config.use-cache | 0 .../telemetry/next.config.webpack | 0 .../telemetry/package.babel | 0 .../app => e2e/telemetry}/package.json | 4 + .../telemetry/package.swc-plugins | 0 test/e2e/telemetry/page-features.test.ts | 323 +++ .../telemetry/pages/__ytho__/lel.js | 0 .../pages/_app_withoutreportwebvitals.empty | 0 .../pages/_app_withreportwebvitals.empty | 0 .../telemetry/pages/about.js | 0 .../telemetry/pages/api/og.jsx | 0 .../telemetry/pages/data.json | 0 .../pages/dynamic-file-imports/index.js | 0 .../telemetry/pages/edge.js | 0 .../telemetry/pages/gip.js | 0 .../telemetry/pages/gssp-again.js | 0 .../telemetry/pages/gssp.js | 0 .../telemetry/pages/hello.test.skip | 0 .../telemetry}/pages/index.js | 0 .../telemetry/pages/script.js | 0 .../telemetry/pages/ssg.js | 0 .../telemetry/pages/ssg/[dynamic].js | 0 .../telemetry/pages/warning.skip | 0 .../telemetry/public/small.jpg | Bin test/e2e/telemetry/telemetry.test.ts | 350 +++ .../next.config.js | 0 .../pages/404.js | 0 .../pages/[slug].js | 0 .../pages/another.js | 0 .../pages/blog/[slug].js | 0 .../pages/blog/another.js | 0 .../pages/catch-all/[...slug].js | 0 .../pages/catch-all/first.js | 0 .../pages/index.js | 0 .../trailing-slashes-href-resolving.test.ts} | 86 +- .../trailing-slashes-rewrite/next.config.js | 2 +- .../pages/catch-all/[...slug].js | 0 .../trailing-slashes-rewrite/pages/index.js | 0 .../pages/products/[product].js | 0 .../pages/products/index.js | 0 .../trailing-slashes-rewrite/server.js | 0 .../trailing-slashes-rewrite.test.ts | 61 + .../.babelrc | 0 .../User.ts | 0 .../UserStatistics.ts | 0 .../pages/index.tsx | 0 .../pages/normal.tsx | 0 ...ypescript-only-remove-type-imports.test.ts | 22 + .../packages/www/test/index.test.ts | 3 +- .../angle-bracket-type-assertions.ts | 0 .../typescript/components/generics.ts | 0 .../typescript/components/hello.module.css | 0 .../typescript/components/hello.module.sass | 0 .../typescript/components/hello.module.scss | 0 .../typescript/components/hello.ts | 0 .../typescript/components/image-legacy.tsx | 0 .../typescript/components/image.tsx | 0 .../typescript/components/link.tsx | 0 .../typescript/components/router.tsx | 0 .../typescript/components/world.tsx | 0 .../typescript/extension-order/js-first.js | 0 .../typescript/extension-order/js-first.ts | 0 .../typescript}/next.config.js | 0 .../typescript/pages/_app.tsx | 0 .../typescript/pages/_document.tsx | 0 .../typescript/pages/_error.tsx | 0 .../pages/angle-bracket-type-assertions.tsx | 0 .../typescript/pages/api/async.tsx | 0 .../typescript/pages/api/sync.tsx | 0 .../typescript/pages/generics.tsx | 0 .../typescript/pages/hello.tsx | 0 .../typescript/pages/ssg/[slug].tsx | 0 .../typescript/pages/ssg/blog/[post].tsx | 0 .../typescript/pages/ssg/blog/index.tsx | 0 .../typescript/pages/ssr/[slug].tsx | 0 .../typescript/pages/ssr/blog/[post].tsx | 0 .../typescript/pages/ssr/cookies.tsx | 0 .../typescript/pages/ssr/promise.tsx | 0 test/e2e/typescript/typescript.test.ts | 174 ++ .../undefined-webpack-config/next.config.js | 0 .../undefined-webpack-config/pages/index.js | 0 .../undefined-webpack-config.test.ts | 33 + .../webpack-loader-parse-error.test.ts | 3 +- .../webpack-require-hook/next.config.js | 0 .../webpack-require-hook/pages/hello.js | 0 .../webpack-require-hook.test.ts | 29 + .../worker-webpack5/lib/sharedCode.js | 0 .../worker-webpack5/lib/worker.js | 0 .../worker-webpack5}/next.config.js | 0 .../worker-webpack5/pages/index.js | 0 .../worker-webpack5/worker-webpack5.test.ts | 31 + .../404-page-app/test/index.test.ts | 93 - .../404-page-custom-error/test/index.test.ts | 87 - .../404-page-ssg/test/index.test.ts | 145 -- test/integration/404-page/test/index.test.ts | 331 --- .../500-page/test/gsp-gssp.test.ts | 248 -- test/integration/500-page/test/index.test.ts | 409 ---- .../absolute-assetprefix/test/index.test.ts | 142 -- .../api-body-parser/test/index.test.ts | 87 - .../api-catch-all/test/index.test.ts | 75 - .../integration/app-aspath/test/index.test.ts | 57 - .../test/index.test.ts | 35 - .../test/index.test.ts | 107 - .../test/index.test.ts | 126 - .../test/index.test.ts | 45 - .../app-dynamic-error/test/index.test.ts | 20 - .../app-functional/test/index.test.ts | 33 - test/integration/app-tree/test/index.test.ts | 78 - test/integration/app-tree/tsconfig.json | 26 - test/integration/app-types/app-types.test.ts | 137 -- .../auto-export-error-bail/test/index.test.ts | 39 - .../test/index.test.ts | 41 - .../auto-export/test/index.test.ts | 93 - test/integration/babel-custom/test/.babelrc | 12 - .../babel-custom/test/index.test.ts | 22 - .../babel-next-image/babel-next-image.test.ts | 22 - .../test/index.test.ts | 56 - test/integration/bigint/test/index.test.ts | 62 - .../broken-webpack-plugin/test/index.test.ts | 43 - .../build-output/test/index.test.ts | 272 --- .../test/index.test.ts | 33 - .../test/index.test.ts | 103 - .../test/index.test.ts | 155 -- .../build-warnings/test/index.test.ts | 104 - .../bundle-size-profiling/next.config.js | 19 - .../test/index.test.ts | 40 - test/integration/chunking/test/index.test.ts | 140 -- .../node_modules/node-sass/package.json | 4 - .../node_modules/sass/package.json | 4 - test/integration/cli/test/index.test.ts | 1122 --------- .../integration/client-404/test/index.test.ts | 108 - .../client-shallow-routing/test/index.test.ts | 116 - .../compression/test/index.test.ts | 29 - .../test/index.test.ts | 269 --- .../integration/config-mjs/test/index.test.ts | 60 - .../config-output-export/test/index.test.ts | 461 ---- .../config-promise-error/test/index.test.ts | 42 - .../config-resolve-alias/test/index.test.ts | 23 - .../config-syntax-error/test/index.test.ts | 56 - .../config-validation/test/index.test.ts | 97 - .../node_modules/css-framework/framework.css | 3 - test/integration/config/test/index.test.ts | 60 - .../test/index.test.ts | 63 - .../conflicting-ssg-paths/test/index.test.ts | 192 -- .../fixtures/basic-app/tsconfig.json | 31 - .../cpu-profiling/test/index.test.ts | 61 - .../create-next-app/templates/matrix.test.ts | 87 - test/integration/create-next-app/utils.ts | 100 - .../critical-css/test/index.test.ts | 87 - .../css-client-nav/test/index.test.ts | 242 -- .../css-customization/test/index.test.ts | 421 ---- .../css-features/test/browserslist.test.ts | 123 - .../css-features/test/css-modules.test.ts | 195 -- .../css-features/test/index.test.ts | 170 -- .../cssmodules-pure-no-check/tsconfig.json | 24 - .../css-fixtures/multi-module/next.config.js | 3 - .../url-global-asset-prefix-1/next.config.js | 5 - .../url-global-asset-prefix-2/next.config.js | 5 - .../pages/_app.js | 5 - .../pages/index.js | 38 - .../postcss.config.js | 13 - .../styles/global.css | 3 - .../integration/css-minify/test/index.test.ts | 50 - .../css-modules/test/index.test.ts | 800 ------- .../css/test/basic-global-support.test.ts | 731 ------ .../css/test/css-and-styled-jsx.test.ts | 76 - .../css/test/css-compilation.test.ts | 618 ----- test/integration/css/test/css-modules.test.ts | 555 ----- .../css/test/css-rendering.test.ts | 515 ---- .../css/test/dev-css-handling.test.ts | 209 -- .../css/test/valid-invalid-css.test.ts | 182 -- .../test/index.test.ts | 39 - .../custom-error/test/index.test.ts | 121 - .../custom-page-extension/test/index.test.ts | 53 - .../custom-routes-catchall/test/index.test.ts | 72 - .../test/index.test.ts | 85 - .../custom-server-types/test/index.test.ts | 12 - .../custom-server-types/tsconfig.json | 20 - test/integration/custom-server/pages/asset.js | 1 - .../custom-server/ssh/localhost-key.pem | 28 - .../custom-server/ssh/localhost.pem | 25 - .../custom-server/test/index.test.ts | 391 --- .../data-fetching-errors/test/index.test.ts | 182 -- .../dedupes-scripts/test/index.test.ts | 47 - .../test/index.test.ts | 28 - .../integration/disable-js/test/index.test.ts | 97 - test/integration/dist-dir/test/index.test.ts | 113 - .../test/index.test.ts | 79 - .../document-head-warnings/test/index.test.ts | 44 - .../integration/draft-mode/test/index.test.ts | 255 -- test/integration/draft-mode/tsconfig.json | 24 - .../test/index.test.ts | 89 - .../test/index.test.ts | 73 - .../test/index.test.ts | 325 --- .../dynamic-require/test/index.test.ts | 20 - .../dynamic-route-rename/test/index.test.ts | 51 - .../dynamic-routing/test/middleware.test.ts | 7 - .../node_modules/lib | 1 - .../test/index.test.ts | 524 ---- .../test/index.test.ts | 309 --- .../test/index.test.ts | 417 ---- .../test/module-imports.test.ts | 324 --- .../edge-runtime-module-errors/test/utils.js | 101 - .../test/index.test.ts | 94 - .../test/index.test.ts | 88 - .../test/index.test.ts | 146 -- .../test/index.test.ts | 71 - .../empty-project/test/index.test.ts | 28 - test/integration/env-config/app/package.json | 4 - .../integration/env-config/test/index.test.ts | 396 --- .../error-in-error/test/index.test.ts | 43 - .../error-load-fail/test/index.test.ts | 50 - .../test/index.test.ts | 25 - .../test/index.test.ts | 43 - .../test/index.test.ts | 23 - .../integration/export-404/test/index.test.ts | 60 - .../export-dynamic-pages/test/index.test.ts | 52 - .../test/index.test.ts | 29 - .../test/index.test.ts | 20 - .../export-image-default/test/index.test.ts | 24 - .../test/index.test.ts | 155 -- .../export-image-loader/test/index.test.ts | 187 -- .../test/index.test.ts | 21 - .../export-intent/test/index.test.ts | 253 -- .../export-subfolders/test/index.test.ts | 45 - .../node_modules/esm-package1/correct.mjs | 1 - .../externals-esm-loose/test/index.test.ts | 50 - .../test/externals.test.ts | 55 - .../externals-pages-bundle/test/index.test.ts | 68 - .../fallback-modules/test/index.test.ts | 68 - .../fallback-route-params/test/index.test.ts | 70 - .../test/index.test.ts | 95 - test/integration/fetch-polyfill/api-server.js | 38 - .../fetch-polyfill/test/index.test.ts | 114 - .../filesystempublicroutes/test/index.test.ts | 55 - .../firebase-grpc/test/index.test.ts | 45 - test/integration/future/test/index.test.ts | 39 - .../getinitialprops/test/index.test.ts | 68 - .../test/index.test.ts | 32 - .../test/index.test.ts | 292 --- .../gip-identifier/test/index.test.ts | 102 - .../gsp-build-errors/test/index.test.ts | 178 -- .../gsp-extension/test/index.test.ts | 70 - .../gssp-pageProps-merge/test/index.test.ts | 61 - .../test/index.test.ts | 59 - .../handles-export-errors/test/index.test.ts | 32 - test/integration/hashbang/test/index.test.ts | 66 - .../hydrate-then-render/test/index.test.ts | 41 - test/integration/hydration/test/index.test.ts | 70 - .../i18n-support-base-path/test/index.test.ts | 187 -- .../test/index.test.ts | 121 - .../test/index.test.ts | 121 - .../test/index.test.ts | 97 - .../image-generation/test/index.test.ts | 45 - .../image-optimizer/app/next.config.js | 2 - .../image-optimizer/test/index.test.ts | 964 -------- .../image-optimizer/test/sharp.test.ts | 8 - .../import-assertion/next.config.js | 9 - .../import-assertion/test/index.test.ts | 18 - .../import-attributes/test/index.test.ts | 18 - .../import-attributes/tsconfig.json | 20 - .../index-index/test/index.test.ts | 223 -- .../initial-ref/test/index.test.ts | 50 - .../invalid-config-values/test/index.test.ts | 43 - .../invalid-custom-routes/test/index.test.ts | 651 ----- .../test/index.test.ts | 65 - .../invalid-href/test/index.test.ts | 219 -- .../test/index.test.ts | 177 -- .../invalid-multi-match/test/index.test.ts | 62 - .../test/index.test.ts | 66 - .../test/index.test.ts | 101 - .../jsconfig-baseurl/test/index.test.ts | 93 - .../jsconfig-empty/test/index.test.ts | 20 - .../test/index.test.ts | 60 - .../jsconfig-paths/test/index.test.ts | 159 -- test/integration/jsconfig/test/index.test.ts | 46 - .../test/index.test.ts | 20 - .../link-ref-app/test/index.test.ts | 132 - .../link-ref-pages/test/index.test.ts | 128 - .../link-with-encoding/test/index.test.ts | 301 --- .../link-without-router/test/index.test.tsx | 32 - .../middleware-basic/test/index.test.ts | 56 - .../test/index.test.ts | 96 - .../middleware-dev-update/test/index.test.ts | 131 - .../test/index.test.ts | 48 - .../middleware-prefetch/tests/index.test.ts | 93 - .../middleware-src-node/test/index.test.ts | 139 -- .../middleware-src/test/index.test.ts | 139 -- .../test/index.test.ts | 127 - .../integration/module-ids/test/index.test.ts | 137 -- .../test/index.test.ts | 169 -- .../tsconfig.json | 25 - .../next-dynamic-css/test/index.test.ts | 73 - .../next-dynamic-css/tsconfig.json | 31 - .../test/index.test.ts | 86 - .../next-dynamic/test/index.test.ts | 78 - .../asset-prefix/test/index.test.ts | 78 - .../base-path/test/index.test.ts | 495 ---- .../basic/test/index.test.ts | 406 ---- .../custom-resolver/test/index.test.ts | 62 - .../default/test/index.test.ts | 1514 ------------ .../default/test/static.test.ts | 171 -- .../test/index.test.ts | 55 - .../test/index.test.ts | 76 - .../noscript/test/index.test.ts | 47 - .../react-virtualized/test/index.test.ts | 83 - .../trailing-slash/test/index.test.ts | 82 - .../typescript/test/index.test.ts | 109 - .../typescript/tsconfig.json | 20 - .../test/index.test.ts | 59 - .../app-dir/test/static.test.ts | 291 --- .../asset-prefix/test/index.test.ts | 101 - .../base-path/test/index.test.ts | 222 -- .../test/index.test.ts | 84 - .../export-config/test/index.test.ts | 44 - .../test/index.test.ts | 74 - .../invalid-image-import/test/index.test.ts | 124 - .../test/index.test.ts | 96 - .../test/index.test.ts | 81 - .../loader-config/test/index.test.ts | 89 - .../middleware/test/index.test.ts | 46 - .../middleware-intercept-next-image.test.ts | 44 - .../react-virtualized/test/index.test.ts | 83 - .../trailing-slash/test/index.test.ts | 82 - .../typescript/test/index.test.ts | 107 - .../next-image-new/typescript/tsconfig.json | 20 - .../unoptimized/test/index.test.ts | 190 -- .../no-op-export/test/index.test.ts | 102 - .../no-override-next-props/test/index.test.ts | 20 - .../node-fetch-keep-alive/test/index.test.ts | 101 - .../non-next-dist-exclude/test/index.test.ts | 21 - .../test/index.test.ts | 192 -- .../not-found-revalidate/test/index.test.ts | 217 -- .../nullish-config/test/index.test.ts | 91 - .../numeric-sep/test/index.test.ts | 25 - test/integration/ondemand/test/index.test.ts | 102 - .../test/index.test.ts | 57 - .../page-config/test/index.test.ts | 104 - .../page-extensions/test/index.test.ts | 79 - .../plugin-mdx-rs/test/index.test.ts | 73 - .../polyfilling-minimal/test/index.test.ts | 26 - test/integration/polyfills/test/index.test.ts | 59 - .../port-env-var/test/index.test.ts | 59 - .../preload-viewport/test/index.test.ts | 610 ----- .../test/index.test.ts | 21 - .../test/index.test.ts | 25 - .../test/index.test.ts | 102 - .../prerender-preview/test/index.test.ts | 403 ---- .../prerender-revalidate/test/index.test.ts | 125 - .../prerender/pages/another/index.js | 30 - .../prerender/pages/api-docs/[...slug].js | 27 - test/integration/prerender/pages/api/bad.js | 3 - test/integration/prerender/pages/bad-gssp.js | 7 - test/integration/prerender/pages/bad-ssr.js | 7 - .../pages/blocking-fallback-once/[slug].js | 43 - .../pages/blocking-fallback-some/[slug].js | 43 - .../pages/blocking-fallback/[slug].js | 43 - .../prerender/pages/blog/[post]/[comment].js | 41 - .../integration/prerender/pages/blog/index.js | 24 - .../pages/catchall-explicit/[...slug].js | 41 - .../pages/catchall-optional/[[...slug]].js | 29 - .../prerender/pages/catchall/[...slug].js | 34 - .../prerender/pages/default-revalidate.js | 24 - .../prerender/pages/dynamic/[slug].js | 27 - .../prerender/pages/fallback-only/[slug].js | 43 - test/integration/prerender/pages/index.js | 108 - .../prerender/pages/index/index.js | 18 - .../prerender/pages/lang/[lang]/about.js | 12 - .../prerender/pages/non-json-blocking/[p].js | 21 - .../prerender/pages/non-json/[p].js | 21 - test/integration/prerender/pages/normal.js | 1 - test/integration/prerender/pages/something.js | 34 - .../prerender/pages/user/[user]/profile.js | 28 - test/integration/prerender/test/index.test.ts | 70 - .../production-build-dir/test/index.test.ts | 36 - .../production-config/test/index.test.ts | 86 - .../production-nav/test/index.test.ts | 39 - .../test/index.test.ts | 22 - .../query-with-encoding/test/index.test.ts | 247 -- .../test/index.test.ts | 43 - .../world.txt | 1 - .../react-current-version/app/tsconfig.json | 20 - .../react-current-version/test/index.test.ts | 169 -- .../react-current-version/tsconfig.json | 30 - .../project-a/tsconfig.json | 20 - .../project-b/tsconfig.json | 20 - .../test/index.test.ts | 100 - .../test/index.test.ts | 62 - .../tsconfig.json | 20 - .../test/index.test.ts | 48 - .../with-get-initial-props/test/index.test.ts | 42 - .../test/index.test.ts | 42 - .../revalidate-as-path/test/index.test.ts | 94 - .../test/index.test.ts | 67 - .../test/index.test.ts | 51 - .../root-catchall-cache/test/index.test.ts | 63 - .../test/index.test.ts | 94 - .../route-index/test/index.test.ts | 76 - .../route-load-cancel-css/test/index.test.ts | 57 - .../route-load-cancel/test/index.test.ts | 70 - .../router-hash-navigation/test/index.test.ts | 56 - .../test/index.test.ts | 81 - .../router-is-ready/test/index.test.ts | 111 - .../router-prefetch/test/index.test.ts | 69 - .../router-rerender/test/index.test.ts | 61 - .../script-loader/partytown/package.json | 9 - .../script-loader/partytown/pages/index.js | 15 - .../script-loader/test/index.test.ts | 376 --- .../test/index.test.ts | 96 - .../server-asset-modules/test/index.test.ts | 55 - .../server-side-dev-errors/test/index.test.ts | 656 ----- test/integration/sharp-api/app/.gitignore | 2 - .../sharp-api/app/package-lock.json | 559 ----- test/integration/sharp-api/app/package.json | 7 - .../sharp-api/test/sharp-api.test.ts | 49 - .../test/index.test.ts | 56 - .../src-dir-support/test/index.test.ts | 104 - .../test/index.test.ts | 60 - .../integration/static-404/test/index.test.ts | 79 - .../static-page-name/test/index.test.ts | 57 - .../styled-jsx-plugin/app/package.json | 24 - .../styled-jsx-plugin/test/index.test.ts | 48 - test/integration/telemetry/pages/index.js | 1 - .../integration/telemetry/test/config.test.ts | 743 ------ test/integration/telemetry/test/index.test.ts | 371 --- .../telemetry/test/page-features.test.ts | 281 --- test/integration/test-file.txt | 1 - .../trailing-slash-dist/test/index.test.ts | 47 - .../test/index.test.ts | 106 - .../turbopack-unsupported-log/index.test.ts | 101 - .../turborepo-access-trace/test/index.test.ts | 38 - .../test/index.test.ts | 87 - .../typeof-window-replace/test/index.test.ts | 85 - .../next-env.d.ts | 6 - .../next-env.strictRouteTypes.d.ts | 8 - .../test/index.test.ts | 87 - .../tsconfig.json | 20 - .../test/index.test.ts | 26 - .../project/test/index.test.ts | 29 - .../test/index.test.ts | 23 - .../typescript-filtered-files/tsconfig.json | 20 - .../typescript-hmr/test/index.test.ts | 114 - .../test/index.test.ts | 78 - .../typescript-ignore-errors/tsconfig.json | 20 - .../test/index.test.ts | 61 - .../tsconfig.json | 20 - .../integration/typescript/test/index.test.ts | 187 -- test/integration/typescript/tsconfig.json | 20 - .../test/index.test.ts | 41 - .../webpack-bun-externals/test/index.test.ts | 61 - .../webpack-require-hook/test/index.test.ts | 52 - test/integration/with-electron/app/.gitignore | 1 - .../with-electron/app/next.config.js | 6 - .../with-electron/app/package.json | 10 - .../with-electron/app/public/main.js | 31 - test/integration/with-electron/next.config.js | 3 - .../with-electron/test/index.test.ts | 82 - .../worker-webpack5/test/index.test.ts | 64 - test/lib/create-next-install.js | 18 + test/lib/next-modes/base.ts | 212 +- test/lib/next-test-utils.ts | 13 +- test/lib/next-webdriver.ts | 10 + .../absolute-assetprefix.test.ts | 124 + .../absolute-assetprefix}/next.config.js | 0 .../absolute-assetprefix/pages/about.js | 0 .../pages/gsp-fallback/[slug].js | 0 .../absolute-assetprefix/pages/gssp.js | 0 .../absolute-assetprefix/pages/index.js | 0 ...pted-into-client-rendering-warning.test.ts | 22 +- .../app-dir/post-build/post-build.test.ts | 23 +- .../typed-routes-with-webpack-worker.test.ts | 58 +- .../app-dir/upload-trace/upload-trace.test.ts | 25 +- .../worker-restart/worker-restart.test.ts | 96 +- .../app-document-style-fragment.test.ts | 15 + .../pages/_document.js | 0 .../pages/index.js | 0 .../app-dynamic-error.test.ts | 20 + .../app/dynamic-error/loading.js | 0 .../app/dynamic-error/page.js | 0 .../app-dynamic-error/app/layout.js | 0 .../app-dynamic-error}/next.config.js | 0 test/production/app-types/app-types.test.ts | 143 ++ .../app-types/next.config.js | 0 .../app-types/package.json | 0 .../app/(newroot)/dashboard/another/page.tsx | 0 .../app-types/src/app/about/page.tsx | 0 .../src/app/blog/[category]/[id]/page.tsx | 0 .../src/app/dashboard/[...slug]/page.tsx | 0 .../app/dashboard/user/[[...slug]]/page.tsx | 0 .../app-types/src/app/layout.tsx | 0 .../app-types/src/app/mdx-test/page.mdx | 0 .../src/app/type-checks/config/page.tsx | 0 .../revalidate-with-seperators/page.tsx | 0 .../src/app/type-checks/form/page.tsx | 0 .../src/app/type-checks/layout/layout.tsx | 0 .../src/app/type-checks/link/page.tsx | 0 .../src/app/type-checks/redirect/page.tsx | 0 .../app/type-checks/route-handlers/route.ts | 0 .../src/app/type-checks/router/page.tsx | 0 .../app-types/src/pages/aaa.js | 0 .../app-types/tsconfig.test.json} | 0 .../auto-export-error-bail.test.ts | 23 + .../pages/app/_error.js | 0 .../auto-export-query-error.test.ts | 18 + .../auto-export-query-error/next.config.js | 0 .../auto-export-query-error/pages/hello.js | 0 .../auto-export-query-error/pages/ssg.js | 0 .../auto-export-query-error/pages/ssr.js | 0 .../babel-custom/babel-custom.test.ts | 50 + .../babel-custom/fixtures/babel-env/.babelrc | 0 .../fixtures/babel-env/pages/index.js | 0 .../fixtures/babel-json5/.babelrc | 0 .../fixtures/babel-json5/pages/index.js | 0 .../fixtures/targets-browsers/.babelrc | 0 .../fixtures/targets-browsers/pages/index.js | 0 .../fixtures/targets-string/.babelrc | 0 .../fixtures/targets-string/pages/index.js | 0 test/production/bfcache-routing/index.test.ts | 25 +- .../build-output/build-output.test.ts | 340 +++ .../fixtures/basic-app/pages/index.js | 0 .../[propsDuration]/[renderDuration].js | 0 .../fixtures/with-app/pages/_app.js | 0 .../fixtures/with-app/pages/index.js | 0 .../with-error-static/pages/_error.js | 0 .../fixtures/with-error-static/pages/index.js | 0 .../fixtures/with-error/pages/_error.js | 0 .../fixtures/with-error/pages/index.js | 0 .../with-parallel-routes/app/layout.js | 0 .../fixtures/with-parallel-routes/app/page.js | 0 .../app/root-page/@footer/default.js | 0 .../app/root-page/@footer/page.js | 0 .../app/root-page/@header/default.js | 0 .../app/root-page/@header/page.js | 0 .../app/root-page/layout.js | 0 .../app/root-page/page.js | 0 .../app/app/route1/route.js | 0 .../app/next.config.js | 0 ...build-trace-extra-entries-monorepo.test.ts | 29 + .../other/included.txt | 0 .../app/app/route1/route.js | 0 .../app/content/hello.json | 0 .../app/include-me/hello.txt | 0 .../app/include-me/second.txt | 0 .../app/lib/fetch-data.js | 0 .../app/lib/get-data.js | 0 .../app/next.config.js | 5 - .../nested-structure/constants/package.json | 0 .../nested-structure/dist/constants.js | 0 .../nested-structure/dist/index.js | 0 .../nested-structure/package.json | 0 .../app/node_modules/some-cms/index.js | 0 .../app/node_modules/some-cms/package.json | 0 .../app/pages/another.js | 0 .../app/pages/image-import.js | 0 .../app/pages/index.js | 0 .../app/public/another.jpg | Bin .../app/public/exclude-me/another.txt | 0 .../app/public/exclude-me/hello.txt | 0 .../app}/public/test.jpg | Bin .../build-trace-extra-entries-turbo.test.ts | 102 + .../app/app/route1/route.js | 0 .../app/content/hello.json | 0 .../app/include-me-global.txt | 0 .../include-me/.dot-folder/another-file.txt | 0 .../app/include-me/hello.txt | 0 .../app/include-me/second.txt | 0 .../app/include-me/some-dir/file.txt | 0 .../app/lib/fetch-data.js | 0 .../app/lib/get-data.js | 0 .../app/lib/my-component.js | 0 .../app/next.config.js | 0 .../nested-structure/constants/package.json | 0 .../nested-structure/dist/constants.js | 0 .../nested-structure/dist/index.js | 0 .../nested-structure/package.json | 0 .../app/node_modules/pkg-behind-symlink | 0 .../app/node_modules/pkg/index.js | 0 .../app/node_modules/pkg/package.json | 0 .../app/node_modules/some-cms/index.js | 0 .../app/node_modules/some-cms/package.json | 0 .../app/pages/another.js | 0 .../app/pages/image-import.js | 0 .../app/pages/index.js | 0 .../app/public/another.jpg | Bin .../app/public/exclude-me/another.txt | 0 .../app/public/exclude-me/hello.txt | 0 .../app}/public/test.jpg | Bin .../build-trace-extra-entries.test.ts | 162 ++ .../build-warnings/build-warnings.test.ts | 110 + .../build-warnings/next.config.js | 0 .../build-warnings/pages/index.js | 0 test/production/chunking/chunking.test.ts | 103 + .../chunking/components/one.js | 0 .../chunking/next.config.js | 0 .../chunking/pages/index.js | 0 .../chunking/pages/page1.js | 0 .../chunking/pages/page2.js | 0 .../chunking/pages/page3.js | 0 .../config-promise-error.test.ts | 38 + .../config-promise-error/pages/index.js | 0 .../config-resolve-alias.test.ts | 18 + .../config-resolve-alias/next.config.js | 0 .../config-resolve-alias/pages/index.js | 0 .../config-syntax-error.test.ts | 50 + .../config-syntax-error/pages/index.js | 0 .../config-validation.test.ts | 82 + .../config-validation/pages/index.js | 0 .../conflicting-ssg-paths.test.ts | 178 ++ .../cpu-profiling/cpu-profiling.test.ts | 47 + .../fixtures/basic-app/app/layout.tsx | 0 .../fixtures/basic-app/app/page.tsx | 0 .../__snapshots__/biome-config.test.ts.snap | 0 .../create-next-app/biome-config.test.ts | 12 +- .../create-next-app/eslint-config.test.ts | 12 +- .../create-next-app/examples.test.ts | 11 +- .../create-next-app/index.test.ts | 11 +- .../create-next-app/lib/specification.ts | 0 .../create-next-app/lib/test-pkg-paths.ts | 64 + .../create-next-app/lib/types.ts | 0 .../create-next-app/lib/utils.ts | 8 + .../package-manager/bun.test.ts | 11 +- .../package-manager/npm.test.ts | 11 +- .../package-manager/pnpm.test.ts | 12 +- .../package-manager/yarn.test.ts | 11 +- .../create-next-app/prompts.test.ts | 40 +- .../create-next-app/templates/app-api.test.ts | 11 +- .../create-next-app/templates/app.test.ts | 11 +- .../create-next-app/templates/matrix.test.ts | 107 + .../create-next-app/templates/pages.test.ts | 11 +- test/production/create-next-app/utils.ts | 148 ++ .../critical-css/components/hello.js | 0 .../critical-css/components/hello.module.css | 0 .../critical-css/critical-css.test.ts | 53 + test/production/critical-css/next.config.js | 1 + .../critical-css/pages/_app.js | 0 .../critical-css/pages/another.js | 0 .../critical-css/pages/index.js | 0 .../critical-css/styles/index.module.css | 0 .../critical-css/styles/styles.css | 0 .../css-customization.test.ts | 456 ++++ .../.postcssrc.json | 0 .../pages/_app.js | 0 .../pages/index.js | 0 .../styles/global.css | 0 .../.postcssrc.json | 0 .../pages/_app.js | 0 .../pages/index.js | 0 .../styles/global.css | 0 .../.postcssrc.json | 0 .../pages/_app.js | 0 .../pages/index.js | 0 .../styles/global.css | 0 .../.postcssrc.json | 0 .../pages/_app.js | 0 .../pages/index.js | 0 .../styles/global.css | 0 .../.postcssrc.json | 0 .../pages/_app.js | 0 .../pages/index.js | 0 .../styles/global.css | 0 .../.postcssrc.json | 0 .../pages/_app.js | 0 .../pages/index.js | 0 .../styles/global.css | 0 .../.postcssrc.json | 0 .../pages/_app.js | 0 .../pages/index.js | 0 .../styles/global.css | 0 .../pages/_app.js | 0 .../pages/index.js | 0 .../postcss.config.js | 0 .../styles/global.css | 0 .../pages/_app.js | 0 .../pages/index.js | 0 .../postcss.config.js | 0 .../styles/global.css | 0 .../bad-custom-configuration/.postcssrc.json | 0 .../bad-custom-configuration/next.config.js | 3 + .../bad-custom-configuration}/pages/_app.js | 0 .../bad-custom-configuration}/pages/index.js | 0 .../styles/global.css | 0 .../custom-configuration-arr/.postcssrc.json | 0 .../custom-configuration-arr/next.config.js | 3 + .../custom-configuration-arr}/pages/_app.js | 0 .../custom-configuration-arr}/pages/index.js | 0 .../styles/global.css | 0 .../next.config.js | 0 .../pages/index.js | 0 .../styles/index.css | 0 .../custom-configuration/.postcssrc.json | 0 .../custom-configuration/next.config.js | 3 + .../custom-configuration}/pages/_app.js | 0 .../custom-configuration}/pages/index.js | 0 .../custom-configuration/styles/global.css | 0 .../css-features/basic-global-support.test.ts | 550 +++++ .../css-features/browserslist.test.ts | 62 + .../css-features/css-compilation.test.ts | 874 +++++++ .../css-features/css-features.test.ts | 86 + .../css-features/css-modules-ordering.test.ts | 69 + .../css-features/css-modules-support.test.ts | 367 +++ .../css-features/css-modules.test.ts | 117 + .../css-features/css-rendering.test.ts | 290 +++ .../fixtures}/3rd-party-module/pages/index.js | 0 .../3rd-party-module/pages/index.module.css | 0 .../fixtures/basic-module}/pages/index.js | 0 .../basic-module}/pages/index.module.css | 0 .../fixtures/browsers-new/.browserslistrc | 1 + .../fixtures/browsers-new/package.json | 0 .../fixtures/browsers-new/pages/_app.js | 0 .../fixtures/browsers-new/pages/index.js | 0 .../fixtures/browsers-new/pages/styles.css | 0 .../fixtures/browsers-old/.browserslistrc | 3 + .../fixtures/browsers-old/package.json | 0 .../fixtures/browsers-old/pages/_app.js | 0 .../fixtures/browsers-old/pages/index.js | 0 .../fixtures/browsers-old/pages/styles.css | 0 .../pages/[...post]/55css.module.css | 0 .../catch-all-module/pages/[...post]/index.js | 0 .../pages/[...post]/index.module.css | 0 .../compilation-and-prefixing}/pages/_app.js | 0 .../compilation-and-prefixing/pages/index.js | 0 .../styles/global.css | 0 .../fixtures}/composes-basic/pages/index.js | 0 .../composes-basic/pages/index.module.css | 0 .../composes-external/pages/index.js | 0 .../composes-external/pages/index.module.css | 0 .../composes-external/pages/other.css | 0 .../fixtures/cp-el-modules/pages/index.js | 0 .../cp-el-modules/pages/styles.module.css | 0 .../fixtures/cp-global-modules/pages/index.js | 0 .../cp-global-modules/pages/styles.module.css | 0 .../fixtures/cp-ie-11/package.json | 0 .../fixtures/cp-ie-11/pages/_app.js | 0 .../fixtures/cp-ie-11/pages/index.js | 0 .../fixtures/cp-ie-11/pages/styles.css | 0 .../fixtures/cp-modern/package.json | 0 .../fixtures/cp-modern/pages/_app.js | 0 .../fixtures/cp-modern/pages/index.js | 0 .../fixtures/cp-modern/pages/styles.css | 0 .../csp-style-src-nonce/next.config.js | 0 .../csp-style-src-nonce/pages/_document.js | 0 .../csp-style-src-nonce/pages/index.js | 0 .../pages/index.module.css | 0 .../csp-style-src-nonce/pages/other.js | 0 .../pages/other.module.css | 0 .../pages/index.module.css | 0 .../cssmodules-pure-no-check/pages/index.tsx | 0 .../fixtures}/data-url/pages/index.js | 0 .../fixtures}/data-url/pages/index.module.css | 0 .../pages/[post]/index.js | 0 .../pages/[post]/index.module.css | 0 .../fixtures/hydrate-without-deps}/.gitignore | 0 .../hydrate-without-deps/pages/client.js | 0 .../pages/common.module.css | 0 .../hydrate-without-deps/pages/index.js | 0 .../pages/index.module.css | 0 .../node_modules/example/index.css | 0 .../node_modules/example/index.js | 0 .../node_modules/example/index.mjs | 0 .../node_modules/example/package.json | 0 .../import-global-from-module/pages/index.js | 0 .../fixtures/inline-comments/package.json | 0 .../fixtures/inline-comments}/pages/_app.js | 0 .../fixtures/inline-comments/pages/global.css | 0 .../fixtures/inline-comments/pages/index.js | 0 .../invalid-global-with-app/pages/_app.js | 0 .../invalid-global-with-app/pages/index.js | 0 .../invalid-global-with-app/styles/global.css | 0 .../fixtures}/invalid-global/pages/index.js | 0 .../invalid-global/styles/global.css | 0 .../pages/_document.js | 0 .../invalid-module-document/pages/index.js | 0 .../invalid-module-document/styles.module.css | 0 .../module-import-exports/package.json | 0 .../pages/colors.module.css | 0 .../module-import-exports/pages/index.js | 0 .../pages/styles.module.css | 0 .../module-import-global-invalid/package.json | 0 .../pages/index.js | 0 .../pages/styles.css | 0 .../pages/styles.module.css | 0 .../module-import-global/package.json | 0 .../module-import-global/pages/index.js | 0 .../module-import-global/pages/styles.css | 0 .../pages/styles.module.css | 0 .../multi-global-reversed}/.gitignore | 0 .../multi-global-reversed/pages/_app.js | 0 .../multi-global-reversed/pages/index.js | 0 .../multi-global-reversed}/styles/global1.css | 0 .../multi-global-reversed}/styles/global2.css | 0 .../fixtures/multi-global}/.gitignore | 0 .../fixtures/multi-global}/pages/_app.js | 0 .../fixtures}/multi-global/pages/index.js | 0 .../fixtures/multi-global/styles/global1.css} | 0 .../fixtures/multi-global/styles/global2.css | 3 + .../fixtures/multi-page}/.gitignore | 0 .../fixtures/multi-page}/pages/_app.js | 0 .../fixtures/multi-page/pages/page1.js | 12 + .../fixtures/multi-page/pages/page2.js | 12 + .../fixtures/multi-page/styles/global1.css} | 0 .../fixtures/multi-page/styles/global2.css | 3 + .../fixtures/nested-global}/.gitignore | 0 .../fixtures/nested-global}/pages/_app.js | 0 .../fixtures}/nested-global/pages/index.js | 0 .../nested-global/styles/global1.css | 0 .../nested-global/styles/global1b.css | 0 .../nested-global/styles/global2.css | 0 .../nested-global/styles/global2b.css | 0 .../fixtures/next-issue-15468}/.gitignore | 0 .../fixtures}/next-issue-15468/pages/_app.js | 0 .../fixtures}/next-issue-15468/pages/index.js | 0 .../next-issue-15468/styles/global.css | 0 .../node_modules/example/index.js | 0 .../node_modules/example/index.mjs | 0 .../node_modules/example/index.module.css | 0 .../node_modules/example/other.css | 0 .../node_modules/example/other2.css | 0 .../node_modules/example/other3.css | 0 .../node_modules/example/package.json | 0 .../fixtures}/nm-module-nested/pages/index.js | 0 .../nm-module/node_modules/example/index.js | 0 .../nm-module/node_modules/example/index.mjs | 0 .../node_modules/example/index.module.css | 3 + .../node_modules/example/package.json | 0 .../fixtures}/nm-module/pages/index.js | 0 .../fixtures/npm-import-bad}/.gitignore | 0 .../fixtures/npm-import-bad}/pages/_app.js | 0 .../fixtures}/npm-import-bad/pages/index.js | 0 .../npm-import-bad/styles/global.css | 0 .../fixtures/npm-import-nested}/.gitignore | 0 .../node_modules/example/index.js | 0 .../node_modules/example/index.mjs | 0 .../node_modules/example/other.css | 0 .../node_modules/example/package.json | 0 .../node_modules/example/test.css | 0 .../fixtures/npm-import-nested}/pages/_app.js | 0 .../npm-import-nested/pages/index.js | 0 .../npm-import-nested/styles/global.css | 0 .../fixtures/npm-import}/.gitignore | 0 .../fixtures/npm-import}/pages/_app.js | 0 .../fixtures}/npm-import/pages/index.js | 0 .../fixtures}/npm-import/styles/global.css | 0 .../fixtures/prod-module/pages/index.js | 9 + .../prod-module/pages/index.module.css | 3 + .../a+b}/.gitignore | 0 .../a+b}/pages/_app.js | 0 .../a+b/pages/index.js | 0 .../a+b}/styles/global.css | 0 .../fixtures/single-global-src}/.gitignore | 0 .../single-global-src/src/pages/_app.js | 0 .../single-global-src/src/pages/index.js | 0 .../single-global-src}/styles/global.css | 0 .../fixtures/single-global}/.gitignore | 0 .../fixtures/single-global}/pages/_app.js | 0 .../fixtures}/single-global/pages/index.js | 0 .../fixtures/single-global/styles/global.css | 3 + .../fixtures/transition-cleanup/.gitignore | 1 + .../pages/common.module.css | 0 .../transition-cleanup/pages/index.js | 0 .../transition-cleanup/pages/index.module.css | 0 .../transition-cleanup/pages/other.js | 0 .../transition-cleanup/pages/other.module.css | 0 .../fixtures/transition-react/.gitignore | 1 + .../fixtures/transition-react/pages/index.js | 11 + .../fixtures/transition-react/pages/other.js | 34 + .../transition-react/pages/other.module.css | 3 + .../fixtures/transition-reload/.gitignore | 1 + .../transition-reload/pages/common.module.css | 0 .../transition-reload/pages/index.js | 0 .../transition-reload/pages/index.module.css | 0 .../transition-reload/pages/other.js | 0 .../transition-reload/pages/other.module.css | 0 .../fixtures}/unresolved-css-url/global.css | 0 .../fixtures}/unresolved-css-url/global.scss | 0 .../unresolved-css-url/pages/_app.js | 0 .../unresolved-css-url/pages/another.js | 0 .../pages/another.module.scss | 0 .../unresolved-css-url/pages/index.js | 0 .../unresolved-css-url/pages/index.module.css | 0 .../unresolved-css-url/public/vercel.svg | 0 .../assets/light.svg | 0 .../url-global-asset-prefix-1/next.config.js | 7 + .../url-global-asset-prefix-1}/pages/_app.js | 0 .../url-global-asset-prefix-1/pages/index.js | 0 .../url-global-asset-prefix-1/styles/dark.svg | 0 .../styles/dark2.svg | 0 .../styles/global1.css | 0 .../styles/global2.css | 0 .../styles/global2b.css | 0 .../assets/light.svg | 0 .../url-global-asset-prefix-2/next.config.js | 7 + .../url-global-asset-prefix-2/pages/_app.js | 13 + .../url-global-asset-prefix-2/pages/index.js | 0 .../url-global-asset-prefix-2/styles/dark.svg | 0 .../styles/dark2.svg | 0 .../styles/global1.css | 0 .../styles/global2.css | 0 .../styles/global2b.css | 0 .../fixtures/url-global/.gitignore | 1 + .../fixtures}/url-global/assets/light.svg | 0 .../fixtures/url-global/pages/_app.js | 13 + .../fixtures}/url-global/pages/index.js | 0 .../fixtures}/url-global/styles/dark.svg | 0 .../fixtures}/url-global/styles/dark2.svg | 0 .../fixtures}/url-global/styles/global1.css | 0 .../fixtures}/url-global/styles/global2.css | 0 .../fixtures}/url-global/styles/global2b.css | 0 .../valid-and-invalid-global}/pages/_app.js | 0 .../valid-and-invalid-global/pages/index.js | 0 .../styles/global.css | 3 + .../css-features/valid-invalid-css.test.ts | 116 + test/production/css-minify/css-minify.test.ts | 23 + .../css-minify/pages/_app.js | 0 .../css-minify/pages/index.js | 0 .../css-minify/styles/global.css | 0 .../css-modules/css-modules.test.ts | 504 ++++ .../fixtures/3rd-party-module/pages/index.js | 5 + .../3rd-party-module/pages/index.module.css | 17 + .../fixtures/basic-module/pages/index.js | 9 + .../basic-module/pages/index.module.css | 3 + .../pages/[...post]/55css.module.css | 3 + .../catch-all-module/pages/[...post]/index.js | 8 + .../pages/[...post]/index.module.css | 3 + .../fixtures/composes-basic/pages/index.js | 9 + .../composes-basic/pages/index.module.css | 9 + .../fixtures/composes-external/pages/index.js | 9 + .../composes-external/pages/index.module.css | 4 + .../composes-external/pages/other.css | 4 + .../pages/index.module.css | 19 + .../cssmodules-pure-no-check/pages/index.tsx | 9 + .../pages/[post]/index.js | 7 + .../pages/[post]/index.module.css | 3 + .../node_modules/example/index.css | 0 .../node_modules/example/index.js | 0 .../node_modules/example/index.mjs | 0 .../node_modules/example/package.json | 0 .../invalid-global-module/pages/index.js | 0 .../node_modules/example/index.js | 0 .../node_modules/example/index.mjs | 0 .../node_modules/example/index.module.css | 0 .../node_modules/example/package.json | 0 .../fixtures}/invalid-module/pages/index.js | 0 .../node_modules/example/index.js | 3 + .../node_modules/example/index.mjs | 1 + .../node_modules/example/index.module.css | 6 + .../node_modules/example/other.css | 6 + .../node_modules/example/other2.css | 3 + .../node_modules/example/other3.css | 3 + .../node_modules/example/package.json | 0 .../fixtures/nm-module-nested/pages/index.js | 12 + .../nm-module/node_modules/example/index.js | 3 + .../nm-module/node_modules/example/index.mjs | 1 + .../node_modules/example/index.module.css | 3 + .../node_modules/example/package.json | 4 + .../fixtures/nm-module/pages/index.js | 12 + .../fixtures/prod-module/pages/index.js | 9 + .../prod-module/pages/index.module.css | 3 + .../custom-server-types/.gitignore | 0 .../custom-server-types.test.ts | 13 + .../custom-server-types/pages/index.tsx | 0 .../custom-server-types/server.ts | 0 .../custom-server-types/tsconfig.test.json} | 9 +- .../dedupes-scripts/components/hello.js | 0 .../dedupes-scripts/dedupes-scripts.test.ts | 22 + .../dedupes-scripts/pages/index.js | 0 .../css/404.module.css | 0 .../css/error.module.css | 0 .../document-file-dependencies/css/global.css | 0 .../css/index.module.css | 0 .../document-file-dependencies.test.ts | 71 + .../document-file-dependencies/pages/404.js | 0 .../document-file-dependencies/pages/_app.js | 0 .../pages/_error.js | 0 .../pages/error-trigger.js | 0 .../document-file-dependencies/pages/index.js | 0 .../error-in-error/error-in-error.test.ts | 22 + .../error-in-error/pages/_error.js | 0 .../error-in-error/pages/index.js | 0 .../error-load-fail/error-load-fail.test.ts | 34 + .../error-load-fail/pages/broken.js | 0 .../error-load-fail/pages/index.js | 0 .../error-plugin-stack-overflow.test.ts | 20 + .../next.config.js | 0 .../pages/index.js | 0 .../errors-on-output-to-public.test.ts | 32 + .../pages/index.js | 0 .../errors-on-output-to-static.test.ts | 16 + .../errors-on-output-to-static/next.config.js | 0 .../pages/index.js | 0 test/production/export-404/export-404.test.ts | 58 + .../export-404/next.config.js | 0 .../export-404/pages/404.js | 0 .../export-dynamic-pages.test.ts | 79 + .../export-dynamic-pages/next.config.js | 0 .../pages/regression/[slug].js | 0 .../export-fallback-true-error.test.ts | 19 + .../export-fallback-true-error/next.config.js | 0 .../pages/[slug].js | 0 .../export-getInitialProps-warn.test.ts | 15 + .../next.config.js | 0 .../pages/index.js | 0 .../export-image-default.test.ts | 16 + .../export-image-default}/next.config.js | 0 .../export-image-default/pages/index.js | 0 .../export-image-loader-legacy.test.ts | 116 + .../export-image-loader-legacy/next.config.js | 0 .../export-image-loader-legacy/pages/index.js | 0 .../export-image-loader}/dummy-loader.js | 0 .../export-image-loader.test.ts | 137 ++ .../export-image-loader/next.config.js | 0 .../export-image-loader/pages/index.js | 0 .../export-index-not-found-gsp.test.ts | 13 + .../next.config.js | 0 .../export-index-not-found-gsp/pages/index.js | 0 .../export-intent/export-intent.test.ts | 169 ++ .../fixtures/bad-export/.gitignore | 0 .../fixtures/bad-export}/next.config.js | 0 .../fixtures/bad-export/pages/index.js | 0 .../fixtures/custom-export/.gitignore | 0 .../fixtures/custom-export/next.config.js | 0 .../fixtures/custom-export/pages/index.js | 0 .../fixtures/custom-out/.gitignore | 0 .../fixtures/custom-out/next.config.js | 0 .../fixtures/custom-out/pages/index.js | 0 .../fixtures/default-export/.gitignore | 0 .../fixtures/default-export}/next.config.js | 0 .../fixtures/default-export/pages/index.js | 0 .../fixtures/no-export/.gitignore | 0 .../fixtures/no-export/pages/index.js | 0 .../export-subfolders.test.ts | 28 + .../export-subfolders}/next.config.js | 0 .../export-subfolders/pages/about.js | 0 .../export-subfolders/pages/index.js | 0 .../export-subfolders/pages/posts/index.js | 0 .../export-subfolders/pages/posts/single.js | 0 .../index.test.ts | 287 +-- .../externals-esm-loose.test.ts | 35 + .../externals-esm-loose/next.config.js | 0 .../node_modules/esm-package1/correct.mjs} | 0 .../node_modules/esm-package1/package.json | 0 .../node_modules/esm-package1/wrong.js | 0 .../node_modules/esm-package2/correct.js | 1 + .../node_modules/esm-package2/package.json | 0 .../node_modules/esm-package2/wrong.cjs | 0 .../node_modules/esm-package3/correct.cjs | 0 .../node_modules/esm-package3/package.json | 0 .../node_modules/esm-package3/wrong.js | 0 .../node_modules/preact/compat.js | 0 .../externals-esm-loose/pages/ssg.js | 0 .../externals-esm-loose/pages/ssr.js | 0 .../externals-esm-loose/pages/static.js | 0 .../fallback-modules/fallback-modules.test.ts | 43 + .../fallback-modules}/pages/index.js | 0 .../firebase-grpc/firebase-grpc.test.ts | 32 + .../firebase-grpc/pages/page-1.js | 0 .../firebase-grpc/pages/page-2.js | 0 test/production/future/future.test.ts | 19 + .../future}/next.config.js | 0 .../future/pages/index.js | 0 .../getserversideprops-export-error.test.ts | 17 + .../next.config.js | 0 .../pages/index.js | 0 .../graceful-shutdown/index.test.ts | 125 +- .../gsp-build-errors/gsp-build-errors.test.ts | 174 ++ .../gsp-build-errors}/next.config.js | 0 .../gsp-extension/gsp-extension.test.ts | 39 + .../gsp-extension/pages/[slug].js | 0 .../handles-export-errors.test.ts | 24 + .../handles-export-errors/next.config.mjs | 0 .../pages/blog/[slug].js | 0 .../pages/custom-error.js | 0 .../handles-export-errors/pages/page-1.js | 0 .../handles-export-errors/pages/page-10.js | 0 .../handles-export-errors/pages/page-11.js | 0 .../handles-export-errors/pages/page-12.js | 0 .../handles-export-errors/pages/page-13.js | 0 .../handles-export-errors/pages/page-2.js | 0 .../handles-export-errors/pages/page-3.js | 0 .../handles-export-errors/pages/page-4.js | 0 .../handles-export-errors/pages/page-5.js | 0 .../handles-export-errors/pages/page-6.js | 0 .../handles-export-errors/pages/page-7.js | 0 .../handles-export-errors/pages/page-8.js | 0 .../handles-export-errors/pages/page-9.js | 0 .../handles-export-errors/pages/page.js | 0 .../hydrate-then-render.test.ts | 29 + .../hydrate-then-render/pages/_app.js | 2 +- .../hydrate-then-render/pages/index.js | 0 .../hydrate-then-render/pages/other.js | 0 .../image-generation/image-generation.test.ts | 32 + .../image-generation}/pages/api/image.jsx | 0 .../invalid-config-values.test.ts | 31 + .../invalid-config-values}/pages/index.js | 0 .../invalid-document-image-import.test.ts | 53 + .../next.config.js | 0 .../pages/_document.js | 0 .../pages/index.js | 0 .../public/test.jpg | Bin ...page-automatic-static-optimization.test.ts | 63 + .../pages/also-invalid.js | 0 .../pages/also-valid.js | 0 .../pages/invalid.js | 0 .../pages/valid.js | 0 .../jsconfig-empty/jsconfig-empty.test.ts | 18 + .../jsconfig-empty/jsconfig.json | 0 .../jsconfig-empty}/next.config.js | 0 .../jsconfig-empty/pages/index.js | 0 .../jsconfig/jsconfig.json | 0 test/production/jsconfig/jsconfig.test.ts | 34 + .../jsconfig/pages/hello.js | 0 .../json-serialize-original-error.test.ts | 18 + .../pages/bigint.js | 0 .../middleware-build-errors.test.ts | 90 + .../middleware-build-errors/middleware.js | 0 .../middleware-build-errors}/pages/index.js | 0 .../middleware-prefetch.test.ts | 48 + .../middleware-prefetch/middleware.js | 0 .../middleware-prefetch/pages/index.js | 0 .../middleware-prefetch/pages/ssg-page-2.js | 0 .../middleware-prefetch/pages/ssg-page.js | 0 .../mixed-ssg-serverprops-error.test.ts | 106 + .../pages/index.js | 0 .../pages/index.js.alt | 0 .../next-image-legacy/basic/basic.test.ts | 419 ++++ .../next-image-legacy/basic/next.config.js | 0 .../basic/pages/client-side.js | 0 .../next-image-legacy/basic/pages/errors.js | 0 .../next-image-legacy/basic/pages/index.js | 0 .../next-image-legacy/basic/pages/lazy.js | 0 .../basic/pages/loader-prop.js | 0 .../next-image-legacy/basic/public/styles.css | 0 .../custom-resolver/custom-resolver.test.ts | 44 + .../custom-resolver/next.config.js | 0 .../custom-resolver/pages/client-side.js | 0 .../custom-resolver/pages/index.js | 0 .../next.config.js | 0 .../no-intersection-observer-fallback.test.ts | 56 + .../pages/_document.js | 0 .../pages/index.js | 0 .../pages/no-observer.js | 0 .../noscript/noscript.test.ts | 28 + .../next-image-legacy/noscript/pages/index.js | 0 .../react-virtualized/pages/index.js | 0 .../react-virtualized}/public/test.jpg | Bin .../react-virtualized.test.ts | 75 + .../react-virtualized/server.js | 47 + .../invalid-image-import.test.ts | 24 + .../invalid-image-import/pages/index.js | 14 + .../invalid-image-import/public/invalid.svg | 3 + .../react-virtualized/pages/index.js | 0 .../react-virtualized}/public/test.jpg | Bin .../react-virtualized.test.ts | 75 + .../react-virtualized/server.js | 49 + .../no-op-export/no-op-export.test.ts | 83 + .../app/node_modules/notnext/.gitignore | 0 .../app/node_modules/notnext/dist/index.js | 0 .../app/node_modules/notnext/package.json | 0 .../non-next-dist-exclude}/app/package.json | 2 +- .../non-next-dist-exclude/app/pages/index.js | 0 .../non-next-dist-exclude.test.ts | 19 + .../not-found-revalidate/data.txt | 0 .../not-found-revalidate.test.ts | 138 ++ .../not-found-revalidate/pages/404.js | 0 .../pages/fallback-blocking/[slug].js | 0 .../pages/fallback-true/[slug].js | 0 .../pages/initial-not-found/[slug].js | 0 .../pages/initial-not-found/index.js | 0 .../numeric-sep/numeric-sep.test.ts | 16 + .../numeric-sep/pages/index.js | 0 .../page-config/config/index.js | 0 .../page-config/lib/data.js | 0 .../page-config}/next.config.js | 0 .../page-config/page-config.test.ts | 88 + .../page-config/pages/blog/index.js | 0 .../page-config/pages/blog/post.js | 0 .../page-config/pages/index.js | 0 .../page-config/pages/invalid/export-from.js | 0 .../pages/invalid/import-export.js | 0 .../page-config/pages/invalid/no-init.js | 0 .../pages/invalid/spread-config.js | 0 .../pages/invalid/string-config.js | 0 .../page-config/pages/valid/config-import.js | 0 .../pages/valid/not-config-export.js | 0 .../pages/valid/not-config-import-export.js | 0 .../page-config/something.js | 0 .../page-extensions/page-extensions.test.ts | 60 + .../page-extensions/pages/index.js | 0 .../pages/invalidExtension.d.ts | 0 .../polyfilling-minimal/next.config.js} | 0 .../polyfilling-minimal/pages/index.js | 0 .../polyfilling-minimal.test.ts | 18 + .../polyfills/pages/fetch.js | 0 .../polyfills/pages/index.js | 0 .../polyfills/pages/process.js | 0 test/production/polyfills/polyfills.test.ts | 31 + .../preload-viewport/next.config.js | 0 .../preload-viewport/pages/[...rest].js | 0 .../preload-viewport/pages/another.js | 0 .../preload-viewport/pages/bot-user-agent.js | 0 .../preload-viewport/pages/de-duped.js | 0 .../preload-viewport/pages/dynamic/[hello].js | 0 .../preload-viewport/pages/first.js | 0 .../preload-viewport/pages/index.js | 0 .../pages/invalid-prefetch.js | 0 .../preload-viewport/pages/invalid-ref.js | 0 .../preload-viewport/pages/multi-prefetch.js | 0 .../preload-viewport/pages/not-de-duped.js | 0 .../preload-viewport/pages/opt-out.js | 0 .../pages/prefetch-disabled-ssg.js | 0 .../pages/prefetch-disabled.js | 0 .../pages/rewrite-prefetch.js | 0 .../preload-viewport/pages/ssg/basic.js | 0 .../pages/ssg/catch-all/[...slug].js | 0 .../ssg/dynamic-nested/[slug1]/[slug2].js | 0 .../pages/ssg/dynamic/[slug].js | 0 .../pages/ssg/fixture/index.js | 0 .../pages/ssg/fixture/mismatch.js | 0 .../preload-viewport/pages/ssg/slow.js | 0 .../preload-viewport/preload-viewport.test.ts | 596 +++++ test/production/preload-viewport/server.js | 58 + .../prerender-export/next.config.js | 0 .../prerender-export/pages/another/index.js | 0 .../pages/api-docs/[...slug].js | 0 .../prerender-export/pages/api/bad.js | 0 .../pages/blocking-fallback-once/[slug].js | 0 .../pages/blocking-fallback-some/[slug].js | 0 .../pages/blocking-fallback/[slug].js | 0 .../pages/blog/[post]/[comment].js | 0 .../pages/blog/[post]/index.js | 0 .../prerender-export/pages/blog/index.js | 0 .../pages/catchall-explicit/[...slug].js | 0 .../pages/catchall-optional/[[...slug]].js | 0 .../pages/catchall/[...slug].js | 0 .../pages/default-revalidate.js | 0 .../prerender-export/pages/dynamic/[slug].js | 0 .../pages/fallback-only/[slug].js | 0 .../prerender-export/pages/index.js | 0 .../prerender-export/pages/index/index.js | 0 .../pages/lang/[lang]/about.js | 0 .../pages/non-json-blocking/[p].js | 0 .../prerender-export/pages/non-json/[p].js | 0 .../prerender-export/pages/normal.js | 0 .../prerender-export/pages/something.js | 0 .../pages/user/[user]/profile.js | 0 .../prerender-export.test.ts} | 131 +- .../prerender-export/world.txt | 0 .../index.test.ts | 20 + .../pages/[...slug].js | 0 .../pages/[foo]/[post].js | 0 .../prerender-invalid-paths.test.ts | 22 + .../prerender-no-revalidate/pages/index.js | 0 .../prerender-no-revalidate/pages/named.js | 0 .../pages/nested/index.js | 0 .../pages/nested/named.js | 0 .../prerender-no-revalidate.test.ts | 62 + .../prerender-revalidate/pages/index.js | 0 .../prerender-revalidate/pages/named.js | 0 .../pages/nested/index.js | 0 .../pages/nested/named.js | 0 .../prerender-revalidate/pages/static.js | 0 .../prerender-revalidate.test.ts | 79 + .../production-build-dir/next.config.js} | 0 .../production-build-dir}/pages/index.js | 0 .../production-build-dir.test.ts | 21 + .../fixture-generateBuildId}/next.config.js | 5 +- .../fixture-generateBuildId}/pages/_app.js | 0 .../fixture-generateBuildId}/pages/index.js | 0 .../fixture-generateBuildId}/styles.css | 0 .../production-config/next.config.js | 0 .../production-config/pages/_app.js | 10 + .../production-config/pages/index.js | 23 + .../production-config.test.ts | 71 + test/production/production-config/styles.css | 4 + .../production-nav/next.config.js | 0 .../production-nav/pages/another.js | 0 .../production-nav/pages/index.js | 0 .../production-nav/production-nav.test.ts | 28 + .../production-start-no-build/next.config.js | 0 .../production-start-no-build.test.ts | 18 + .../query-with-encoding/pages/index.js | 0 .../query-with-encoding/pages/newline.js | 0 .../query-with-encoding/pages/percent.js | 0 .../query-with-encoding/pages/plus.js | 0 .../query-with-encoding/pages/space.js | 0 .../query-with-encoding.test.ts | 227 ++ .../component/child.js | 0 .../component/test.js | 0 .../pages/about.js | 0 .../pages/contact.js | 0 .../pages/index.js | 0 ...t-all-exports-from-page-disallowed.test.ts | 38 + .../world.txt | 0 .../pages/_error.js | 0 .../pages/index.js | 0 .../render-error-on-module-error.test.ts | 17 + .../render-error-on-top-level-error.test.ts | 34 + .../with-get-initial-props/pages/_error.js | 0 .../with-get-initial-props/pages/index.js | 0 .../without-get-initial-props/pages/_error.js | 0 .../without-get-initial-props/pages/index.js | 0 .../revalidate-as-path/pages/_app.js | 0 .../pages/another/index/index.js | 0 .../revalidate-as-path/pages/index.js | 0 .../revalidate-as-path.test.ts | 56 + .../app/[[...slug]]/page.js | 0 .../root-catchall-cache/app/layout.js | 0 .../root-catchall-cache/next.config.js | 0 .../root-catchall-cache.test.ts | 40 + .../pages/[[...slug]].js | 0 .../root-optional-revalidate.test.ts | 76 + .../route-load-cancel-css}/pages/index.js | 0 .../route-load-cancel-css/pages/page1.js | 0 .../pages/page1.module.css | 0 .../route-load-cancel-css}/pages/page2.js | 0 .../route-load-cancel-css.test.ts | 27 + .../route-load-cancel-css/pages/index.js | 26 + .../route-load-cancel-css/pages/page1.js | 16 + .../pages/page1.module.css | 3 + .../route-load-cancel-css/pages/page2.js | 5 + .../route-load-cancel-css.test.ts | 21 + .../invalid-module.test.ts | 28 + .../node_modules/example/index.js | 0 .../node_modules/example/index.mjs | 0 .../node_modules/example/index.module.scss | 0 .../node_modules/example/package.json | 4 + .../scss-invalid-module}/pages/index.js | 0 .../sharp-api}/pages/api/custom-sharp.js | 0 test/production/sharp-api/sharp-api.test.ts | 23 + .../static-404/pages/index.js | 0 test/production/static-404/static-404.test.ts | 52 + .../styled-jsx-plugin}/.babelrc.js | 0 .../styled-jsx-plugin}/pages/index.js | 0 .../styled-jsx-plugin/postcss.config.js | 6 + .../styled-jsx-plugin.test.ts | 31 + ...ndex.test.ts => tsconfig-verifier.test.ts} | 203 +- .../app/app/route1/route.js | 0 .../app/lib/fetch-data.js | 0 .../app/lib/get-data.js | 0 .../app/lib/my-component.js | 0 .../turborepo-access-trace/app/next.config.js | 0 .../app/node_modules/some-cms/index.js | 0 .../app/node_modules/some-cms/package.json | 0 .../app/pages/image-import.js | 0 .../turborepo-access-trace/app/pages/index.js | 0 .../app/public/another.jpg | Bin .../app/public/exclude-me/another.txt | 0 .../app/public/exclude-me/hello.txt | 0 .../app/public/test.jpg | Bin .../turborepo-access-trace.test.ts | 34 + .../app/content/hello.json | 0 .../app/include-me/hello.txt | 0 .../app/include-me/second.txt | 0 .../app/lib/fetch-data.js | 0 .../app/lib/get-data.js | 0 .../app/next.config.js | 0 .../nested-structure/constants/package.json | 0 .../nested-structure/lib/constants.js | 0 .../nested-structure/lib/index.js | 0 .../nested-structure/package.json | 0 .../app/node_modules/some-cms/index.js | 0 .../app/node_modules/some-cms/package.json | 0 .../app/pages/another.js | 0 .../app/pages/image-import.js | 0 .../app/pages/index.js | 0 .../app/public/another.jpg | Bin .../app/public/exclude-me/another.txt | 0 .../app/public/exclude-me/hello.txt | 0 .../app/public/test.jpg | Bin .../turbotrace-with-webpack-worker.test.ts | 80 + .../app/node_modules/comps/index.js | 2 +- .../app/node_modules/comps/package.json | 0 .../typeof-window-replace}/app/package.json | 2 +- .../typeof-window-replace/app/pages/index.js | 0 .../typeof-window-replace.test.ts | 76 + .../typescript-custom-tsconfig/next.config.js | 0 .../pages/index.tsx | 0 .../typescript-custom-tsconfig.test.ts | 22 + .../web.tsconfig.json | 0 .../pages/contest.tsx | 0 .../typescript-filtered-files.test.ts | 20 + .../typescript-ignore-errors/pages/index.tsx | 0 .../tsconfig.test.json} | 0 .../typescript-ignore-errors.test.ts | 87 + .../webpack-bun-externals/pages/index.js | 0 .../webpack-bun-externals.test.ts | 45 + .../components/TsxComponent.tsx | 0 .../next.config.js | 0 .../pages/pagewithimport.js | 0 .../webpack-config-extensionalias.test.ts} | 15 +- .../webpack-config-mainjs/client/polyfills.js | 0 .../webpack-config-mainjs/next.config.js | 0 .../webpack-config-mainjs/pages/static.js | 0 .../webpack-config-mainjs.test.ts} | 17 +- .../with-electron}/pages/about.js | 0 .../with-electron}/pages/index.js | 0 .../with-electron/with-electron.test.ts | 31 + .../link-without-router/components/hello.js | 0 .../link-without-router.test.tsx | 15 + test/use-node-streams-tests-manifest.json | 1 - 2936 files changed, 39965 insertions(+), 48651 deletions(-) create mode 100644 scripts/pr-logs.js create mode 100644 test/development/app-aspath/app-aspath.test.ts rename test/{integration => development}/app-aspath/pages/_app.js (100%) rename test/{integration => development}/app-aspath/pages/index.js (100%) create mode 100644 test/development/app-config-asset-prefix/app-config-asset-prefix.test.ts rename test/{integration => development}/app-config-asset-prefix/app/layout.js (100%) rename test/{integration => development}/app-config-asset-prefix/app/page.js (100%) rename test/{integration => development}/app-config-asset-prefix/next.config.js (100%) rename test/{integration/app-document-add-hmr/test/index.test.ts => development/app-document-add-hmr/app-document-add-hmr.test.ts} (56%) rename test/{integration => development}/app-document-add-hmr/pages/index.js (100%) create mode 100644 test/development/app-document-remove-hmr/app-document-remove-hmr.test.ts rename test/{integration => development}/app-document-remove-hmr/pages/_app.js (100%) rename test/{integration => development}/app-document-remove-hmr/pages/_document.js (100%) rename test/{integration => development}/app-document-remove-hmr/pages/index.js (100%) create mode 100644 test/development/app-functional/app-functional.test.ts rename test/{integration => development}/app-functional/next.config.js (100%) rename test/{integration => development}/app-functional/pages/_app.js (100%) rename test/{integration => development}/app-functional/pages/index.js (100%) rename test/{integration => development}/app-functional/shared-module.js (100%) rename test/{integration => development}/babel-next-image/.babelrc (100%) rename test/{integration => development}/babel-next-image/app/layout.js (100%) rename test/{integration => development}/babel-next-image/app/page.js (100%) create mode 100644 test/development/babel-next-image/babel-next-image.test.ts create mode 100644 test/development/broken-webpack-plugin/broken-webpack-plugin.test.ts rename test/{integration => development}/broken-webpack-plugin/next.config.js (100%) rename test/{integration => development}/broken-webpack-plugin/pages/index.js (100%) rename test/{integration/client-navigation-a11y/test/index.test.ts => development/client-navigation-a11y/client-navigation-a11y.test.ts} (57%) rename test/{integration => development}/client-navigation-a11y/pages/index.js (100%) rename test/{integration => development}/client-navigation-a11y/pages/page-with-h1-and-title.js (100%) rename test/{integration => development}/client-navigation-a11y/pages/page-with-h1.js (100%) rename test/{integration => development}/client-navigation-a11y/pages/page-with-title.js (100%) rename test/{integration => development}/client-navigation-a11y/pages/page-without-h1-or-title.js (100%) create mode 100644 test/development/compression/compression.test.ts rename test/{integration => development}/compression/pages/index.js (100%) rename test/{integration/config-devtool-dev/test/index.test.ts => development/config-devtool-dev/config-devtool-dev.test.ts} (51%) rename test/{integration => development}/config-devtool-dev/next.config.js (100%) rename test/{integration => development}/config-devtool-dev/pages/index.js (100%) rename test/{integration => development}/config-mjs/.gitignore (100%) rename test/{integration => development}/config-mjs/components/hello-webpack-css.css (100%) rename test/{integration => development}/config-mjs/components/hello-webpack-css.js (100%) rename test/{integration => development}/config-mjs/components/hello-webpack-sass.scss (100%) create mode 100644 test/development/config-mjs/config-mjs.test.ts rename test/{integration => development}/config-mjs/next.config.mjs (100%) rename test/{integration => development}/config-mjs/node_modules/css-framework/framework.css (100%) rename test/{integration => development}/config-mjs/node_modules/module-only-package/modern.js (100%) rename test/{integration => development}/config-mjs/node_modules/module-only-package/package.json (100%) rename test/{integration => development}/config-mjs/pages/module-only-content.js (100%) rename test/{integration => development}/config-mjs/pages/next-config.js (100%) create mode 100644 test/development/config-output-export/config-output-export.test.ts rename test/{integration => development}/config-output-export/next.config.js (100%) rename test/{integration/bundle-size-profiling => development/config-output-export}/pages/index.js (100%) rename test/{integration => development}/config/.gitignore (100%) rename test/{integration => development}/config/components/hello-webpack-css.css (100%) rename test/{integration => development}/config/components/hello-webpack-css.js (100%) rename test/{integration => development}/config/components/hello-webpack-sass.scss (100%) create mode 100644 test/development/config/config.test.ts rename test/{integration => development}/config/next.config.js (100%) create mode 100644 test/development/config/node_modules/css-framework/framework.css rename test/{integration => development}/config/node_modules/module-only-package/modern.js (100%) rename test/{integration => development}/config/node_modules/module-only-package/package.json (100%) rename test/{integration => development}/config/pages/build-id.js (100%) rename test/{integration => development}/config/pages/module-only-content.js (100%) rename test/{integration => development}/config/pages/next-config.js (100%) create mode 100644 test/development/css-features/css-modules-support.test.ts create mode 100644 test/development/css-features/dev-css-handling.test.ts rename test/{integration/404-page-app => development/css-features/fixtures/dev-module}/next.config.js (100%) rename test/{integration/css-fixtures/basic-module => development/css-features/fixtures/dev-module}/pages/index.js (100%) rename test/{integration/css-fixtures/basic-module => development/css-features/fixtures/dev-module}/pages/index.module.css (100%) rename test/{integration/css-fixtures => development/css-features/fixtures}/hmr-module/pages/index.js (100%) rename test/{integration/css-fixtures/dev-module => development/css-features/fixtures/hmr-module}/pages/index.module.css (100%) rename test/{integration/css-fixtures/composes-ordering => development/css-features/fixtures/multi-page}/.gitignore (100%) rename test/{integration/css-fixtures/multi-global => development/css-features/fixtures/multi-page}/pages/_app.js (100%) rename test/{integration/css-fixtures => development/css-features/fixtures}/multi-page/pages/page1.js (100%) rename test/{integration/css-fixtures => development/css-features/fixtures}/multi-page/pages/page2.js (100%) rename test/{integration/css-fixtures/multi-global-reversed => development/css-features/fixtures/multi-page}/styles/global1.css (100%) rename test/{integration/css-fixtures/multi-global-reversed => development/css-features/fixtures/multi-page}/styles/global2.css (100%) rename test/{integration/css-fixtures/global-and-module-ordering => development/css-features/fixtures/transition-react}/.gitignore (100%) rename test/{integration/css-fixtures => development/css-features/fixtures}/transition-react/pages/index.js (100%) rename test/{integration/css-fixtures => development/css-features/fixtures}/transition-react/pages/other.js (100%) rename test/{integration/css-fixtures => development/css-features/fixtures}/transition-react/pages/other.module.css (100%) rename test/{integration/css-fixtures/bad-custom-configuration-arr-1 => development/css-features/fixtures/unused}/pages/index.js (100%) create mode 100644 test/development/css-modules/css-modules.test.ts rename test/{integration/404-page-ssg => development/css-modules/fixtures/dev-module}/next.config.js (100%) rename test/{integration/css-fixtures => development/css-modules/fixtures}/dev-module/pages/index.js (100%) rename test/{integration/css-fixtures/hmr-module => development/css-modules/fixtures/dev-module}/pages/index.module.css (100%) create mode 100644 test/development/css-modules/fixtures/hmr-module/pages/index.js rename test/{integration/css-fixtures/nm-module/node_modules/example => development/css-modules/fixtures/hmr-module/pages}/index.module.css (100%) create mode 100644 test/development/development-hmr-refresh/development-hmr-refresh.test.ts rename test/{integration => development}/development-hmr-refresh/pages/with+Special&Chars=.js (100%) create mode 100644 test/development/document-head-warnings/document-head-warnings.test.ts rename test/{integration => development}/document-head-warnings/pages/_document.js (100%) rename test/{integration => development}/document-head-warnings/pages/index.js (100%) create mode 100644 test/development/dynamic-require/index.test.ts rename test/{integration => development}/dynamic-require/locales/en.js (100%) rename test/{integration => development}/dynamic-require/locales/ru.js (100%) rename test/{integration => development}/dynamic-require/pages/index.js (100%) create mode 100644 test/development/dynamic-route-rename/dynamic-route-rename.test.ts rename test/{integration => development}/dynamic-route-rename/pages/[pid].js (100%) create mode 100644 test/development/empty-object-getInitialProps/empty-object-getInitialProps.test.ts rename test/{integration => development}/empty-object-getInitialProps/pages/another.js (100%) rename test/{integration => development}/empty-object-getInitialProps/pages/index.js (100%) rename test/{integration => development}/empty-object-getInitialProps/pages/static.js (100%) create mode 100644 test/development/empty-project/empty-project.test.ts rename test/{integration/client-404 => development/empty-project}/next.config.js (100%) rename test/{integration => development}/empty-project/pages/.gitkeep (100%) create mode 100644 test/development/gssp-redirect-with-rewrites/gssp-redirect-with-rewrites.test.ts rename test/{integration => development}/gssp-redirect-with-rewrites/next.config.js (100%) rename test/{integration => development}/gssp-redirect-with-rewrites/pages/main-content.js (100%) rename test/{integration => development}/gssp-redirect-with-rewrites/pages/redirector.js (100%) create mode 100644 test/development/invalid-revalidate-values/invalid-revalidate-values.test.ts rename test/{integration => development}/invalid-revalidate-values/pages/ssg.js (100%) rename test/{integration => development}/jsconfig-paths-wildcard/.gitignore (100%) create mode 100644 test/development/jsconfig-paths-wildcard/jsconfig-paths-wildcard.test.ts rename test/{integration => development}/jsconfig-paths-wildcard/jsconfig.json (100%) rename test/{integration/disable-js => development/jsconfig-paths-wildcard}/next.config.js (100%) rename test/{integration => development}/jsconfig-paths-wildcard/node_modules/mypackage/data.js (100%) rename test/{integration => development}/jsconfig-paths-wildcard/node_modules/mypackage/myfile.js (100%) rename test/{integration => development}/jsconfig-paths-wildcard/pages/wildcard-alias.js (100%) create mode 100644 test/development/link-with-encoding/link-with-encoding.test.ts rename test/{integration => development}/link-with-encoding/pages/index.js (100%) rename test/{integration => development}/link-with-encoding/pages/query.js (100%) rename test/{integration => development}/link-with-encoding/pages/single/[slug].js (100%) create mode 100644 test/development/middleware-dev-update/middleware-dev-update.test.ts rename test/{integration => development}/middleware-dev-update/middleware.js (100%) rename test/{integration => development}/middleware-dev-update/pages/index.js (100%) create mode 100644 test/development/middleware-overrides-node.js-api/middleware-overrides-node.js-api.test.ts rename test/{integration => development}/middleware-overrides-node.js-api/middleware.js (100%) rename test/{integration/middleware-build-errors => development/middleware-overrides-node.js-api}/pages/index.js (100%) rename test/{integration/missing-document-component-error/test/index.test.ts => development/missing-document-component-error/missing-document-component-error.test.ts} (76%) rename test/{integration => development}/missing-document-component-error/pages/index.js (100%) create mode 100644 test/development/next-image-new/export-config/export-config.test.ts rename test/{integration/export-image-default => development/next-image-new/export-config}/next.config.js (100%) rename test/{integration => development}/next-image-new/export-config/pages/index.js (100%) rename test/{integration/image-optimizer/app => development/next-image-new/export-config}/public/test.webp (100%) create mode 100644 test/development/next-image-new/invalid-image-import/invalid-image-import.test.ts rename test/{integration => development}/next-image-new/invalid-image-import/pages/index.js (100%) rename test/{integration => development}/next-image-new/invalid-image-import/public/invalid.svg (100%) create mode 100644 test/development/next-image-new/middleware/middleware-intercept.test.ts rename test/{integration => development}/next-image-new/middleware/middleware.js (100%) create mode 100644 test/development/next-image-new/middleware/middleware.test.ts rename test/{integration => development}/next-image-new/middleware/pages/index.js (100%) rename test/{integration/next-image-legacy/default => development/next-image-new/middleware}/public/small.jpg (100%) create mode 100644 test/development/no-override-next-props/no-override-next-props.test.ts rename test/{integration => development}/no-override-next-props/pages/_app.js (100%) rename test/{integration => development}/no-override-next-props/pages/index.js (100%) rename test/{integration => development}/ondemand/components/hello.js (100%) rename test/{integration => development}/ondemand/next.config.js (100%) create mode 100644 test/development/ondemand/ondemand.test.ts rename test/{integration => development}/ondemand/pages/about.js (100%) rename test/{integration => development}/ondemand/pages/index.js (100%) rename test/{integration => development}/ondemand/pages/nav/dynamic.js (100%) rename test/{integration => development}/ondemand/pages/nav/index.js (100%) rename test/{integration => development}/ondemand/pages/third.js (100%) rename test/{integration => development}/ondemand/server.js (65%) rename test/{integration => development}/plugin-mdx-rs/components/button.js (100%) rename test/{integration => development}/plugin-mdx-rs/components/marker.js (100%) rename test/{integration => development}/plugin-mdx-rs/mdx-components.js (100%) rename test/{integration => development}/plugin-mdx-rs/next.config.js (100%) rename test/{integration => development}/plugin-mdx-rs/pages/button.mdx (100%) rename test/{integration => development}/plugin-mdx-rs/pages/gfm.mdx (100%) rename test/{integration => development}/plugin-mdx-rs/pages/index.mdx (100%) rename test/{integration => development}/plugin-mdx-rs/pages/provider.mdx (100%) create mode 100644 test/development/plugin-mdx-rs/plugin-mdx-rs.test.ts rename test/{integration => development}/prerender/pages/blog/[post]/index.js (100%) create mode 100644 test/development/prerender/prerender.test.ts rename test/{integration => development}/server-side-dev-errors/pages/api/blog/[slug].js (100%) rename test/{integration => development}/server-side-dev-errors/pages/api/hello.js (100%) rename test/{integration => development}/server-side-dev-errors/pages/blog/[slug].js (100%) rename test/{integration => development}/server-side-dev-errors/pages/gsp.js (100%) rename test/{integration => development}/server-side-dev-errors/pages/gssp.js (100%) rename test/{integration => development}/server-side-dev-errors/pages/uncaught-empty-exception.js (100%) rename test/{integration => development}/server-side-dev-errors/pages/uncaught-empty-rejection.js (100%) rename test/{integration => development}/server-side-dev-errors/pages/uncaught-exception.js (100%) rename test/{integration => development}/server-side-dev-errors/pages/uncaught-rejection.js (100%) create mode 100644 test/development/server-side-dev-errors/server-side-dev-errors.test.ts rename test/{integration => development}/trailing-slash-dist/next.config.js (100%) rename test/{integration/404-page-app => development/trailing-slash-dist}/pages/index.js (100%) create mode 100644 test/development/trailing-slash-dist/trailing-slash-dist.test.ts rename test/{integration/404-page => development/turbopack-unsupported-log/fixtures/empty-config}/next.config.js (100%) rename test/{integration/turbopack-unsupported-log/app => development/turbopack-unsupported-log/fixtures/empty-config}/pages/index.js (100%) create mode 100644 test/development/turbopack-unsupported-log/fixtures/no-config/pages/index.js rename test/{integration/script-loader/partytown => development/turbopack-unsupported-log/fixtures/unsupported-config}/next.config.js (60%) create mode 100644 test/development/turbopack-unsupported-log/fixtures/unsupported-config/pages/index.js create mode 100644 test/development/turbopack-unsupported-log/turbopack-unsupported-log.test.ts rename test/{integration/custom-server-types/next-env.d.ts => development/typescript-app-type-declarations/next-env.strictRouteTypes.d.ts} (75%) rename test/{integration => development}/typescript-app-type-declarations/pages/index.tsx (100%) create mode 100644 test/development/typescript-app-type-declarations/typescript-app-type-declarations.test.ts create mode 100644 test/development/typescript-external-dir/.gitignore rename test/{integration => development}/typescript-external-dir/project/components/world.tsx (100%) rename test/{integration => development}/typescript-external-dir/project/next.config.js (100%) rename test/{integration => development}/typescript-external-dir/project/pages/index.tsx (100%) rename test/{integration => development}/typescript-external-dir/project/tsconfig.json (100%) rename test/{integration => development}/typescript-external-dir/shared/components/counter.tsx (100%) rename test/{integration => development}/typescript-external-dir/shared/libs/inc.ts (100%) rename test/{integration => development}/typescript-external-dir/shared/tsconfig.json (100%) create mode 100644 test/development/typescript-external-dir/typescript-external-dir.test.ts rename test/{integration/empty-project => development/typescript-hmr}/next.config.js (100%) rename test/{integration => development}/typescript-hmr/pages/hello.tsx (100%) rename test/{integration => development}/typescript-hmr/pages/type-error-recover.tsx (100%) create mode 100644 test/development/typescript-hmr/typescript-hmr.test.ts create mode 100644 test/e2e/404-page-app/404-page-app.test.ts rename test/{integration/500-page => e2e/404-page-app}/next.config.js (100%) rename test/{integration => e2e}/404-page-app/pages/404.js (100%) rename test/{integration => e2e}/404-page-app/pages/_app.js (100%) rename test/{integration => e2e}/404-page-app/pages/err.js (100%) rename test/{integration/404-page-custom-error => e2e/404-page-app}/pages/index.js (100%) create mode 100644 test/e2e/404-page-custom-error/404-page-custom-error.test.ts rename test/{integration => e2e}/404-page-custom-error/pages/_error.js (100%) rename test/{integration => e2e}/404-page-custom-error/pages/err.js (100%) rename test/{integration/404-page-ssg => e2e/404-page-custom-error}/pages/index.js (100%) create mode 100644 test/e2e/404-page-ssg/404-page-ssg.test.ts rename test/{integration/app-dynamic-error => e2e/404-page-ssg}/next.config.js (100%) rename test/{integration => e2e}/404-page-ssg/pages/404.js (100%) rename test/{integration => e2e}/404-page-ssg/pages/_app.js (100%) rename test/{integration => e2e}/404-page-ssg/pages/err.js (100%) rename test/{integration/404-page => e2e/404-page-ssg}/pages/index.js (100%) create mode 100644 test/e2e/404-page/404-page.test.ts rename test/{integration/css-fixtures/dev-module => e2e/404-page}/next.config.js (100%) rename test/{integration => e2e}/404-page/pages/404.js (100%) rename test/{integration => e2e}/404-page/pages/err.js (100%) rename test/{integration/500-page => e2e/404-page}/pages/index.js (100%) rename test/{integration => e2e}/404-page/pages/invalidExtension.d.ts (100%) create mode 100644 test/e2e/500-page/500-page-build.test.ts create mode 100644 test/e2e/500-page/500-page.test.ts rename test/{integration/dynamic-optional-routing-root-fallback => e2e/500-page}/next.config.js (100%) rename test/{integration => e2e}/500-page/pages/500.js (100%) rename test/{integration => e2e}/500-page/pages/err.js (100%) rename test/{integration/port-env-var => e2e/500-page}/pages/index.js (100%) create mode 100644 test/e2e/api-body-parser/api-body-parser.test.ts rename test/{integration => e2e}/api-body-parser/pages/api/index.js (100%) rename test/{integration => e2e}/api-body-parser/server.js (61%) create mode 100644 test/e2e/api-catch-all/api-catch-all.test.ts rename test/{integration => e2e}/api-catch-all/pages/api/users/[...slug].js (100%) rename test/{integration => e2e}/api-catch-all/pages/api/users/index.js (100%) rename test/{integration/api-support/test/index.test.ts => e2e/api-support/api-support.test.ts} (52%) rename test/{integration => e2e}/api-support/big.json (100%) rename test/{integration => e2e}/api-support/pages/api-conflict.js (100%) rename test/{integration => e2e}/api-support/pages/api/[post]/[comment].js (100%) rename test/{integration => e2e}/api-support/pages/api/[post]/comments.js (100%) rename test/{integration => e2e}/api-support/pages/api/[post]/index.js (100%) rename test/{integration => e2e}/api-support/pages/api/auth/[...nextauth].js (100%) rename test/{integration => e2e}/api-support/pages/api/big-parse.js (100%) rename test/{integration => e2e}/api-support/pages/api/big-parse.ts (100%) rename test/{integration => e2e}/api-support/pages/api/blog/[post]/comment/[id].js (100%) rename test/{integration => e2e}/api-support/pages/api/blog/index.js (100%) rename test/{integration => e2e}/api-support/pages/api/bool.js (100%) rename test/{integration => e2e}/api-support/pages/api/child-process.js (100%) rename test/{integration => e2e}/api-support/pages/api/cookies.js (100%) rename test/{integration => e2e}/api-support/pages/api/cors.js (100%) rename test/{integration => e2e}/api-support/pages/api/error.js (100%) rename test/{integration => e2e}/api-support/pages/api/external-resolver-false-positive.js (100%) rename test/{integration => e2e}/api-support/pages/api/external-resolver.js (100%) rename test/{integration => e2e}/api-support/pages/api/index.js (100%) rename test/{integration => e2e}/api-support/pages/api/json-null.js (100%) rename test/{integration => e2e}/api-support/pages/api/json-string.js (100%) rename test/{integration => e2e}/api-support/pages/api/json-undefined.js (100%) rename test/{integration => e2e}/api-support/pages/api/large-chunked-response.js (100%) rename test/{integration => e2e}/api-support/pages/api/large-response-with-config-size.js (100%) rename test/{integration => e2e}/api-support/pages/api/large-response-with-config.js (100%) rename test/{integration => e2e}/api-support/pages/api/large-response.js (100%) rename test/{integration => e2e}/api-support/pages/api/no-parsing.js (100%) rename test/{integration => e2e}/api-support/pages/api/nullable-payload.js (100%) rename test/{integration => e2e}/api-support/pages/api/parse.js (100%) rename test/{integration => e2e}/api-support/pages/api/parsing.js (100%) rename test/{integration => e2e}/api-support/pages/api/proxy-self.js (100%) rename test/{integration => e2e}/api-support/pages/api/query.js (100%) rename test/{integration => e2e}/api-support/pages/api/redirect-301.js (100%) rename test/{integration => e2e}/api-support/pages/api/redirect-307.js (100%) rename test/{integration => e2e}/api-support/pages/api/redirect-error.js (100%) rename test/{integration => e2e}/api-support/pages/api/redirect-null.js (100%) rename test/{integration => e2e}/api-support/pages/api/status-204.js (100%) rename test/{integration => e2e}/api-support/pages/api/test-no-end.js (100%) rename test/{integration => e2e}/api-support/pages/api/test-res-pipe.js (100%) rename test/{integration => e2e}/api-support/pages/api/user-error-async.js (100%) rename test/{integration => e2e}/api-support/pages/api/user-error.js (100%) rename test/{integration => e2e}/api-support/pages/api/users.js (100%) rename test/{integration => e2e}/api-support/pages/index.js (100%) rename test/{integration => e2e}/api-support/pages/user.js (100%) delete mode 100644 test/e2e/app-dir/scss/invalid-module/invalid-module.test.ts create mode 100644 test/e2e/app-document-import-order/app-document-import-order.test.ts rename test/{integration => e2e}/app-document-import-order/next.config.js (100%) rename test/{integration => e2e}/app-document-import-order/pages/_app.js (100%) rename test/{integration => e2e}/app-document-import-order/pages/_document.js (100%) rename test/{integration => e2e}/app-document-import-order/pages/index.js (100%) rename test/{integration => e2e}/app-document-import-order/requiredByApp.js (100%) rename test/{integration => e2e}/app-document-import-order/requiredByPage.js (100%) rename test/{integration => e2e}/app-document-import-order/sideEffectModule.js (100%) create mode 100644 test/e2e/app-tree/app-tree.test.ts rename test/{integration => e2e}/app-tree/pages/_app.tsx (100%) rename test/{integration => e2e}/app-tree/pages/another.js (100%) rename test/{integration => e2e}/app-tree/pages/hello.tsx (100%) rename test/{integration => e2e}/app-tree/pages/index.js (100%) create mode 100644 test/e2e/auto-export/auto-export.test.ts rename test/{integration => e2e}/auto-export/pages/[post]/[cmnt].js (100%) rename test/{integration => e2e}/auto-export/pages/[post]/index.js (100%) rename test/{integration => e2e}/auto-export/pages/commonjs1.js (100%) rename test/{integration => e2e}/auto-export/pages/commonjs2.js (100%) create mode 100644 test/e2e/basepath-root-catch-all/basepath-root-catch-all.test.ts rename test/{integration => e2e}/basepath-root-catch-all/next.config.js (100%) rename test/{integration => e2e}/basepath-root-catch-all/pages/[...parts].js (100%) rename test/{integration => e2e}/basepath-root-catch-all/pages/hello.js (100%) create mode 100644 test/e2e/bigint/bigint.test.ts rename test/{integration => e2e}/bigint/pages/api/bigint.js (100%) create mode 100644 test/e2e/catches-missing-getStaticProps/catches-missing-getStaticProps.test.ts rename test/{integration => e2e}/catches-missing-getStaticProps/pages/[slug].js (100%) rename test/{integration => e2e}/cli/basic/file with spaces to --require.js (100%) rename test/{integration => e2e}/cli/basic/file with spaces to-require-with-node-require-option.js (100%) rename test/{integration => e2e}/cli/basic/pages/index.js (100%) rename test/{integration => e2e}/cli/certificates/localhost-key.pem (100%) rename test/{integration => e2e}/cli/certificates/localhost.pem (100%) create mode 100644 test/e2e/cli/cli.test.ts rename test/{integration => e2e}/cli/duplicate-sass/package.json (100%) rename test/{integration => e2e}/cli/duplicate-sass/pages/index.js (100%) create mode 100644 test/e2e/client-404/client-404.test.ts rename test/{integration/jsconfig-baseurl => e2e/client-404}/next.config.js (100%) rename test/{integration => e2e}/client-404/pages/_error.js (100%) rename test/{integration => e2e}/client-404/pages/index.js (100%) rename test/{integration => e2e}/client-404/pages/invalid-link.js (100%) rename test/{integration => e2e}/client-404/pages/missing.js (100%) rename test/{integration => e2e}/client-404/pages/to-missing-link.js (100%) create mode 100644 test/e2e/client-shallow-routing/client-shallow-routing.test.ts rename test/{integration => e2e}/client-shallow-routing/pages/[slug].js (100%) create mode 100644 test/e2e/config-experimental-warning/config-experimental-warning.test.ts rename test/{integration => e2e}/config-experimental-warning/pages/index.js (100%) create mode 100644 test/e2e/conflicting-public-file-page/conflicting-public-file-page.test.ts rename test/{integration => e2e}/conflicting-public-file-page/pages/another/conflict.js (100%) rename test/{integration => e2e}/conflicting-public-file-page/pages/another/index.js (100%) rename test/{integration => e2e}/conflicting-public-file-page/pages/hello.js (100%) rename test/{integration => e2e}/conflicting-public-file-page/public/another/conflict (100%) rename test/{integration => e2e}/conflicting-public-file-page/public/another/index (100%) rename test/{integration => e2e}/conflicting-public-file-page/public/hello (100%) rename test/{integration => e2e}/conflicting-public-file-page/public/normal.txt (100%) create mode 100644 test/e2e/css-client-nav/css-client-nav.test.ts rename test/{integration/css-fixtures => e2e/css-client-nav}/next.config.js (70%) rename test/{integration/css-features/fixtures/inline-comments => e2e/css-client-nav}/pages/_app.js (100%) rename test/{integration/css-fixtures/multi-module => e2e/css-client-nav}/pages/blue.js (100%) rename test/{integration/css-fixtures/multi-module => e2e/css-client-nav}/pages/blue.module.css (100%) rename test/{integration/css-fixtures/multi-module => e2e/css-client-nav}/pages/global.css (100%) rename test/{integration/css-fixtures/multi-module => e2e/css-client-nav}/pages/none.js (100%) rename test/{integration/css-fixtures/multi-module => e2e/css-client-nav}/pages/red.js (100%) rename test/{integration/css-fixtures/multi-module => e2e/css-client-nav}/pages/red.module.css (100%) create mode 100644 test/e2e/css-features/css-and-styled-jsx.test.ts create mode 100644 test/e2e/css-features/css-modules-ordering.test.ts rename test/{integration/css-fixtures/hydrate-without-deps => e2e/css-features/fixtures/composes-ordering}/.gitignore (100%) rename test/{integration/css-fixtures => e2e/css-features/fixtures}/composes-ordering/pages/common.module.css (100%) rename test/{integration/css-fixtures => e2e/css-features/fixtures}/composes-ordering/pages/index.js (100%) rename test/{integration/css-fixtures => e2e/css-features/fixtures}/composes-ordering/pages/index.module.css (100%) rename test/{integration/css-fixtures => e2e/css-features/fixtures}/composes-ordering/pages/other.js (100%) rename test/{integration/css-fixtures => e2e/css-features/fixtures}/composes-ordering/pages/other.module.css (100%) rename test/{integration/css-fixtures/multi-global-reversed => e2e/css-features/fixtures/global-and-module-ordering}/.gitignore (100%) rename test/{integration/css-fixtures/bad-custom-configuration-arr-1 => e2e/css-features/fixtures/global-and-module-ordering}/pages/_app.js (100%) rename test/{integration/css-fixtures => e2e/css-features/fixtures}/global-and-module-ordering/pages/index.js (100%) rename test/{integration/css-fixtures => e2e/css-features/fixtures}/global-and-module-ordering/pages/index.module.css (100%) rename test/{integration/css-fixtures => e2e/css-features/fixtures}/global-and-module-ordering/pages/index2.module.css (100%) rename test/{integration/css-fixtures => e2e/css-features/fixtures}/global-and-module-ordering/styles/global.css (100%) rename test/{integration/css-fixtures/multi-global => e2e/css-features/fixtures/multi-page}/.gitignore (100%) rename test/{integration/css-fixtures => e2e/css-features/fixtures}/multi-page/pages/_app.js (100%) create mode 100644 test/e2e/css-features/fixtures/multi-page/pages/page1.js create mode 100644 test/e2e/css-features/fixtures/multi-page/pages/page2.js rename test/{integration/css-fixtures/multi-global => e2e/css-features/fixtures/multi-page}/styles/global1.css (100%) rename test/{integration/css-fixtures/multi-global => e2e/css-features/fixtures/multi-page}/styles/global2.css (100%) rename test/{integration/css-fixtures/multi-page => e2e/css-features/fixtures/next-issue-12343}/.gitignore (100%) rename test/{integration/css-fixtures => e2e/css-features/fixtures}/next-issue-12343/components/button.jsx (100%) rename test/{integration/css-fixtures => e2e/css-features/fixtures}/next-issue-12343/components/button.module.css (100%) rename test/{integration/css-fixtures => e2e/css-features/fixtures}/next-issue-12343/pages/another-page.js (100%) rename test/{integration/css-fixtures => e2e/css-features/fixtures}/next-issue-12343/pages/homepage.module.css (100%) rename test/{integration/css-fixtures => e2e/css-features/fixtures}/next-issue-12343/pages/index.js (100%) rename test/{integration/css-fixtures/nested-global => e2e/css-features/fixtures/transition-react}/.gitignore (100%) create mode 100644 test/e2e/css-features/fixtures/transition-react/pages/index.js create mode 100644 test/e2e/css-features/fixtures/transition-react/pages/other.js create mode 100644 test/e2e/css-features/fixtures/transition-react/pages/other.module.css rename test/{integration/css-fixtures/bad-custom-configuration-arr-2 => e2e/css-features/fixtures/with-styled-jsx}/pages/_app.js (100%) rename test/{integration/css-fixtures => e2e/css-features/fixtures}/with-styled-jsx/pages/index.js (100%) rename test/{integration/css-fixtures => e2e/css-features/fixtures}/with-styled-jsx/styles/global.css (100%) create mode 100644 test/e2e/custom-error-page-exception/custom-error-page-exception.test.ts rename test/{integration => e2e}/custom-error-page-exception/pages/_error.js (100%) rename test/{integration => e2e}/custom-error-page-exception/pages/index.js (100%) create mode 100644 test/e2e/custom-error/custom-error.test.ts rename test/{integration => e2e}/custom-error/pages/_error.js (100%) rename test/{integration => e2e}/custom-error/pages/index.js (100%) create mode 100644 test/e2e/custom-page-extension/custom-page-extension.test.ts rename test/{integration => e2e}/custom-page-extension/next.config.js (100%) rename test/{integration => e2e}/custom-page-extension/pages/blog/[pid].page.js (100%) rename test/{integration => e2e}/custom-page-extension/pages/blog/index.page.js (100%) create mode 100644 test/e2e/custom-routes-catchall/custom-routes-catchall.test.ts rename test/{integration => e2e}/custom-routes-catchall/next.config.js (100%) rename test/{integration => e2e}/custom-routes-catchall/pages/hello.js (100%) rename test/{integration => e2e}/custom-routes-catchall/public/another.txt (100%) rename test/{integration => e2e}/custom-routes-catchall/public/static/data.json (100%) create mode 100644 test/e2e/custom-routes-i18n-index-redirect/custom-routes-i18n-index-redirect.test.ts rename test/{integration => e2e}/custom-routes-i18n-index-redirect/next.config.js (100%) rename test/{integration => e2e}/custom-routes-i18n-index-redirect/pages/index.js (100%) rename test/{integration/custom-routes-i18n/test/index.test.ts => e2e/custom-routes-i18n/custom-routes-i18n.test.ts} (61%) rename test/{integration => e2e}/custom-routes-i18n/next.config.js (100%) rename test/{integration => e2e}/custom-routes-i18n/pages/links.js (100%) rename test/{integration/custom-routes/test/index.test.ts => e2e/custom-routes/custom-routes.test.ts} (57%) rename test/{integration => e2e}/custom-routes/next.config.js (100%) create mode 100644 test/e2e/custom-routes/package.json rename test/{integration => e2e}/custom-routes/pages/_sport/[slug]/index.js (100%) rename test/{integration => e2e}/custom-routes/pages/_sport/[slug]/test.js (100%) rename test/{integration => e2e}/custom-routes/pages/another/[id].js (100%) rename test/{integration => e2e}/custom-routes/pages/api/dynamic/[slug].js (100%) rename test/{integration => e2e}/custom-routes/pages/api/hello.js (100%) rename test/{integration => e2e}/custom-routes/pages/auto-export/[slug].js (100%) rename test/{integration => e2e}/custom-routes/pages/auto-export/another.js (100%) rename test/{integration => e2e}/custom-routes/pages/blog-catchall/[...slug].js (100%) rename test/{integration => e2e}/custom-routes/pages/blog/[post]/index.js (100%) rename test/{integration => e2e}/custom-routes/pages/docs/v2/more/now-for-github.js (100%) rename test/{integration => e2e}/custom-routes/pages/hello-again.js (100%) rename test/{integration => e2e}/custom-routes/pages/hello.js (100%) rename test/{integration => e2e}/custom-routes/pages/multi-rewrites.js (100%) rename test/{integration => e2e}/custom-routes/pages/nav.js (100%) rename test/{integration => e2e}/custom-routes/pages/overridden.js (100%) rename test/{integration => e2e}/custom-routes/pages/overridden/[slug].js (100%) rename test/{integration => e2e}/custom-routes/pages/redirect-override.js (100%) rename test/{integration => e2e}/custom-routes/pages/with-params.js (100%) rename test/{integration => e2e}/custom-routes/public/blog/data.json (100%) rename test/{integration => e2e}/custom-routes/public/static/hello.txt (100%) create mode 100644 test/e2e/custom-server/custom-server.test.ts rename test/{integration => e2e}/custom-server/middleware.js (100%) rename test/{integration => e2e}/custom-server/next.config.js (100%) rename test/{integration => e2e}/custom-server/pages/404.js (100%) rename test/{integration => e2e}/custom-server/pages/500.js (100%) create mode 100644 test/e2e/custom-server/pages/asset.js rename test/{integration => e2e}/custom-server/pages/dashboard/index.js (100%) rename test/{integration => e2e}/custom-server/pages/dynamic-dashboard/index.js (100%) rename test/{integration => e2e}/custom-server/pages/index.js (100%) rename test/{integration => e2e}/custom-server/pages/middleware-augmented.js (100%) rename test/{integration => e2e}/custom-server/pages/no-query.js (100%) rename test/{integration => e2e}/custom-server/server.js (82%) create mode 100644 test/e2e/custom-server/ssh/ca-key.pem create mode 100644 test/e2e/custom-server/ssh/ca.pem create mode 100644 test/e2e/custom-server/ssh/localhost-key.pem create mode 100644 test/e2e/custom-server/ssh/localhost.pem rename test/{integration => e2e}/custom-server/static/hello.txt (100%) create mode 100644 test/e2e/data-fetching-errors/data-fetching-errors.test.ts rename test/{integration => e2e}/data-fetching-errors/pages/index.js (100%) create mode 100644 test/e2e/disable-js/disable-js.test.ts rename test/{integration/jsconfig-empty => e2e/disable-js}/next.config.js (100%) rename test/{integration => e2e}/disable-js/pages/index.js (100%) create mode 100644 test/e2e/dist-dir/dist-dir.test.ts rename test/{integration => e2e}/dist-dir/next.config.js (100%) rename test/{integration => e2e}/dist-dir/pages/index.js (100%) create mode 100644 test/e2e/draft-mode/draft-mode.test.ts rename test/{integration => e2e}/draft-mode/pages/another.tsx (100%) rename test/{integration => e2e}/draft-mode/pages/api/disable.ts (100%) rename test/{integration => e2e}/draft-mode/pages/api/enable.ts (100%) rename test/{integration => e2e}/draft-mode/pages/api/read.ts (100%) rename test/{integration => e2e}/draft-mode/pages/index.tsx (100%) rename test/{integration => e2e}/draft-mode/pages/ssp.tsx (100%) rename test/{integration => e2e}/draft-mode/pages/to-index.tsx (100%) create mode 100644 test/e2e/dynamic-optional-routing-root-fallback/dynamic-optional-routing-root-fallback.test.ts rename test/{integration/dynamic-optional-routing-root-static-paths => e2e/dynamic-optional-routing-root-fallback}/next.config.js (100%) rename test/{integration => e2e}/dynamic-optional-routing-root-fallback/pages/[[...optionalName]].js (100%) create mode 100644 test/e2e/dynamic-optional-routing-root-static-paths/dynamic-optional-routing-root-static-paths.test.ts rename test/{integration => e2e}/dynamic-optional-routing-root-static-paths/pages/[[...optionalName]].js (100%) create mode 100644 test/e2e/dynamic-optional-routing/dynamic-optional-routing.test.ts rename test/{integration => e2e}/dynamic-optional-routing/next.config.js (100%) rename test/{integration => e2e}/dynamic-optional-routing/pages/[[...optionalName]].js (100%) rename test/{integration => e2e}/dynamic-optional-routing/pages/about.js (100%) rename test/{integration => e2e}/dynamic-optional-routing/pages/api/post/[[...slug]].js (100%) rename test/{integration => e2e}/dynamic-optional-routing/pages/get-static-paths-fallback/[[...slug]].js (100%) rename test/{integration => e2e}/dynamic-optional-routing/pages/get-static-paths-false/[[...slug]].js (100%) rename test/{integration => e2e}/dynamic-optional-routing/pages/get-static-paths-null/[[...slug]].js (100%) rename test/{integration => e2e}/dynamic-optional-routing/pages/get-static-paths-undefined/[[...slug]].js (100%) rename test/{integration => e2e}/dynamic-optional-routing/pages/get-static-paths/[[...slug]].js (100%) rename test/{integration => e2e}/dynamic-optional-routing/pages/nested/[[...optionalName]].js (100%) create mode 100644 test/e2e/dynamic-routing-middleware/dynamic-routing-middleware.test.ts create mode 100644 test/e2e/dynamic-routing/dynamic-routing.test.ts rename test/{integration => e2e}/dynamic-routing/pages/[name]/[comment].js (100%) rename test/{integration => e2e}/dynamic-routing/pages/[name]/[comment]/[...rest].js (100%) rename test/{integration => e2e}/dynamic-routing/pages/[name]/comments.js (100%) rename test/{integration => e2e}/dynamic-routing/pages/[name]/index.js (100%) rename test/{integration => e2e}/dynamic-routing/pages/[name]/on-mount-redir.js (100%) rename test/{integration => e2e}/dynamic-routing/pages/_app.js (100%) rename test/{integration => e2e}/dynamic-routing/pages/another.js (100%) rename test/{integration => e2e}/dynamic-routing/pages/b/[123].js (100%) rename test/{integration => e2e}/dynamic-routing/pages/blog/[name]/comment/[id].js (100%) rename test/{integration => e2e}/dynamic-routing/pages/c/[alongparamnameshouldbeallowedeventhoughweird].js (100%) rename test/{integration => e2e}/dynamic-routing/pages/catchall-dash/[...hello-world].js (100%) rename test/{integration => e2e}/dynamic-routing/pages/d/[id].js (100%) rename test/{integration => e2e}/dynamic-routing/pages/dash/[hello-world].js (100%) rename test/{integration => e2e}/dynamic-routing/pages/index.js (100%) rename test/{integration => e2e}/dynamic-routing/pages/index/[...slug].js (100%) rename test/{integration => e2e}/dynamic-routing/pages/on-mount/[post].js (100%) rename test/{integration => e2e}/dynamic-routing/pages/p1/p2/all-ssg/[...rest].js (100%) rename test/{integration => e2e}/dynamic-routing/pages/p1/p2/all-ssr/[...rest].js (100%) rename test/{integration => e2e}/dynamic-routing/pages/p1/p2/nested-all-ssg/[...rest]/index.js (100%) rename test/{integration => e2e}/dynamic-routing/pages/p1/p2/nested-all-ssg/[...rest]/styles.module.css (100%) rename test/{integration => e2e}/dynamic-routing/pages/p1/p2/predefined-ssg/[...rest].js (100%) rename test/{integration => e2e}/dynamic-routing/public/hello copy.txt (100%) rename test/{integration => e2e}/dynamic-routing/public/hello%20copy.txt (100%) rename test/{integration => e2e}/dynamic-routing/public/hello+copy.txt (100%) rename test/{integration => e2e}/dynamic-routing/public/hello.txt (100%) rename test/{integration/dynamic-routing/test/index.test.ts => e2e/dynamic-routing/shared.ts} (53%) rename test/{integration => e2e}/dynamic-routing/static/hello copy.txt (100%) rename test/{integration => e2e}/dynamic-routing/static/hello%20copy.txt (100%) rename test/{integration => e2e}/dynamic-routing/static/hello+copy.txt (100%) rename test/{integration => e2e}/dynamic-routing/static/hello.txt (100%) rename test/{integration/edge-runtime-configurable-guards/node_modules => e2e/edge-runtime-configurable-guards}/.pnpm/test/node_modules/lib/index.js (100%) rename test/{integration/edge-runtime-configurable-guards/node_modules => e2e/edge-runtime-configurable-guards}/.pnpm/test/node_modules/lib/package.json (100%) create mode 100644 test/e2e/edge-runtime-configurable-guards/edge-runtime-configurable-guards.test.ts rename test/{integration => e2e}/edge-runtime-configurable-guards/middleware.js (100%) create mode 100644 test/e2e/edge-runtime-configurable-guards/node_modules/lib/index.js create mode 100644 test/e2e/edge-runtime-configurable-guards/node_modules/lib/package.json rename test/{integration => e2e}/edge-runtime-configurable-guards/pages/api/route.js (100%) rename test/{integration => e2e}/edge-runtime-configurable-guards/pages/index.js (100%) create mode 100644 test/e2e/edge-runtime-dynamic-code/edge-runtime-dynamic-code.test.ts rename test/{integration => e2e}/edge-runtime-dynamic-code/lib/square.wasm (100%) rename test/{integration => e2e}/edge-runtime-dynamic-code/lib/utils.js (100%) rename test/{integration => e2e}/edge-runtime-dynamic-code/lib/wasm.js (100%) rename test/{integration => e2e}/edge-runtime-dynamic-code/middleware.js (100%) rename test/{integration => e2e}/edge-runtime-dynamic-code/next.config.js (100%) rename test/{integration => e2e}/edge-runtime-dynamic-code/pages/api/route.js (100%) rename test/{integration => e2e}/edge-runtime-dynamic-code/pages/index.js (100%) create mode 100644 test/e2e/edge-runtime-module-errors/edge-runtime-module-errors.test.ts rename test/{integration => e2e}/edge-runtime-module-errors/lib.js (100%) rename test/{integration => e2e}/edge-runtime-module-errors/middleware.js (100%) rename test/{integration => e2e}/edge-runtime-module-errors/pages/api/route.js (100%) rename test/{integration => e2e}/edge-runtime-module-errors/pages/index.js (100%) create mode 100644 test/e2e/edge-runtime-response-error/edge-runtime-response-error.test.ts rename test/{integration => e2e}/edge-runtime-response-error/lib.js (100%) rename test/{integration => e2e}/edge-runtime-response-error/middleware.js (100%) rename test/{integration => e2e}/edge-runtime-response-error/pages/api/route.js (100%) rename test/{integration => e2e}/edge-runtime-response-error/pages/index.js (100%) create mode 100644 test/e2e/edge-runtime-streaming-error/edge-runtime-streaming-error.test.ts rename test/{integration => e2e}/edge-runtime-streaming-error/pages/api/test.js (100%) create mode 100644 test/e2e/edge-runtime-with-node.js-apis/edge-runtime-with-node.js-apis.test.ts rename test/{integration => e2e}/edge-runtime-with-node.js-apis/lib/utils.js (100%) rename test/{integration => e2e}/edge-runtime-with-node.js-apis/middleware.js (100%) rename test/{integration => e2e}/edge-runtime-with-node.js-apis/pages/api/route.js (100%) rename test/{integration => e2e}/edge-runtime-with-node.js-apis/pages/index.js (100%) rename test/{integration/env-config/app => e2e/env-config}/.env (100%) rename test/{integration/env-config/app => e2e/env-config}/.env.development (100%) rename test/{integration/env-config/app => e2e/env-config}/.env.development.local (100%) rename test/{integration/env-config/app => e2e/env-config}/.env.local (100%) rename test/{integration/env-config/app => e2e/env-config}/.env.production (100%) rename test/{integration/env-config/app => e2e/env-config}/.env.production.local (100%) rename test/{integration/env-config/app => e2e/env-config}/.env.test (100%) rename test/{integration/env-config/app => e2e/env-config}/.env.test.local (100%) create mode 100644 test/e2e/env-config/.gitignore create mode 100644 test/e2e/env-config/env-config.test.ts rename test/{integration/env-config/app => e2e/env-config}/next.config.js (100%) rename test/{integration/env-config/app => e2e/env-config}/pages/another-global.js (100%) rename test/{integration/env-config/app => e2e/env-config}/pages/api/all.js (100%) rename test/{integration/env-config/app => e2e/env-config}/pages/global.js (100%) rename test/{integration/env-config/app => e2e/env-config}/pages/index.js (100%) rename test/{integration/env-config/app => e2e/env-config}/pages/some-ssg.js (100%) rename test/{integration/env-config/app => e2e/env-config}/pages/some-ssp.js (100%) rename test/{integration/cli/duplicate-sass => e2e/externals-pages-bundle}/.gitignore (100%) create mode 100644 test/e2e/externals-pages-bundle/externals-pages-bundle.test.ts rename test/{integration => e2e}/externals-pages-bundle/next.config.js (100%) rename test/{integration => e2e}/externals-pages-bundle/node_modules/external-package/index.js (100%) rename test/{integration => e2e}/externals-pages-bundle/node_modules/external-package/package.json (100%) rename test/{integration => e2e}/externals-pages-bundle/node_modules/opted-out-external-package/index.js (100%) rename test/{integration => e2e}/externals-pages-bundle/node_modules/opted-out-external-package/package.json (100%) rename test/{integration => e2e}/externals-pages-bundle/pages/index.js (100%) rename test/{integration/fallback-false-rewrite/test/index.test.ts => e2e/fallback-false-rewrite/fallback-false-rewrite.test.ts} (58%) rename test/{integration => e2e}/fallback-false-rewrite/next.config.js (100%) rename test/{integration => e2e}/fallback-false-rewrite/pages/[slug].js (100%) rename test/{integration => e2e}/fallback-false-rewrite/pages/another.js (100%) create mode 100644 test/e2e/fallback-route-params/fallback-route-params.test.ts rename test/{integration => e2e}/fallback-route-params/pages/[slug].js (100%) rename test/{integration => e2e}/fetch-polyfill-ky-universal/api-server.js (100%) rename test/{integration => e2e}/fetch-polyfill-ky-universal/api/api-route.js (100%) create mode 100644 test/e2e/fetch-polyfill-ky-universal/fetch-polyfill-ky-universal.test.ts rename test/{integration => e2e}/fetch-polyfill-ky-universal/pages/getinitialprops.js (100%) rename test/{integration => e2e}/fetch-polyfill-ky-universal/pages/ssr.js (100%) rename test/{integration => e2e}/fetch-polyfill-ky-universal/pages/static.js (100%) create mode 100644 test/e2e/fetch-polyfill/fetch-polyfill.test.ts rename test/{integration => e2e}/fetch-polyfill/pages/api/api-route.js (100%) rename test/{integration => e2e}/fetch-polyfill/pages/getinitialprops.js (100%) rename test/{integration => e2e}/fetch-polyfill/pages/ssr.js (100%) rename test/{integration => e2e}/fetch-polyfill/pages/static.js (100%) rename test/{integration => e2e}/fetch-polyfill/pages/user/[username].js (100%) rename test/{integration/file-serving/test/index.test.ts => e2e/file-serving/file-serving.test.ts} (98%) rename test/{integration/errors-on-output-to-public => e2e/file-serving}/pages/index.js (100%) rename test/{integration => e2e}/file-serving/public/hello world.txt (100%) rename test/{integration => e2e}/file-serving/public/vercel-icon-dark.avif (100%) rename test/{integration => e2e}/file-serving/static/hello world.txt (100%) rename test/{integration => e2e}/file-serving/test-file.txt (100%) create mode 100644 test/e2e/filesystempublicroutes/filesystempublicroutes.test.ts rename test/{integration => e2e}/filesystempublicroutes/next.config.js (100%) rename test/{integration => e2e}/filesystempublicroutes/pages/exportpathmap-route.js (100%) rename test/{integration => e2e}/filesystempublicroutes/pages/index.js (100%) rename test/{integration => e2e}/filesystempublicroutes/public/hello.txt (100%) rename test/{integration => e2e}/filesystempublicroutes/server.js (64%) create mode 100644 test/e2e/getinitialprops/getinitialprops.test.ts rename test/{integration => e2e}/getinitialprops/next.config.js (100%) rename test/{integration => e2e}/getinitialprops/pages/blog/[post].js (100%) rename test/{integration => e2e}/getinitialprops/pages/index.js (100%) rename test/{integration => e2e}/getinitialprops/pages/normal.js (100%) create mode 100644 test/e2e/getserversideprops-preview/getserversideprops-preview.test.ts rename test/{integration => e2e}/getserversideprops-preview/pages/api/preview.js (100%) rename test/{integration => e2e}/getserversideprops-preview/pages/api/reset.js (100%) rename test/{integration => e2e}/getserversideprops-preview/pages/index.js (100%) rename test/{integration => e2e}/getserversideprops-preview/pages/to-index.js (100%) create mode 100644 test/e2e/gip-identifier/gip-identifier.test.ts rename test/{integration/errors-on-output-to-static => e2e/gip-identifier}/pages/index.js (100%) create mode 100644 test/e2e/gssp-pageProps-merge/gssp-pageProps-merge.test.ts rename test/{integration => e2e}/gssp-pageProps-merge/pages/_app.js (100%) rename test/{integration => e2e}/gssp-pageProps-merge/pages/gsp.js (100%) rename test/{integration => e2e}/gssp-pageProps-merge/pages/gssp.js (100%) rename test/{integration/gssp-redirect-base-path/test/index.test.ts => e2e/gssp-redirect-base-path/gssp-redirect-base-path.test.ts} (62%) rename test/{integration => e2e}/gssp-redirect-base-path/next.config.js (100%) rename test/{integration => e2e}/gssp-redirect-base-path/pages/404.js (100%) rename test/{integration => e2e}/gssp-redirect-base-path/pages/another.js (100%) rename test/{integration => e2e}/gssp-redirect-base-path/pages/gsp-blog/[post].js (100%) rename test/{integration => e2e}/gssp-redirect-base-path/pages/gssp-blog/[post].js (100%) rename test/{integration => e2e}/gssp-redirect-base-path/pages/index.js (100%) rename test/{integration/gssp-redirect/test/index.test.ts => e2e/gssp-redirect/gssp-redirect.test.ts} (56%) rename test/{integration => e2e}/gssp-redirect/pages/404.js (100%) rename test/{integration => e2e}/gssp-redirect/pages/another.js (100%) rename test/{integration => e2e}/gssp-redirect/pages/gsp-blog-blocking/[post].js (100%) rename test/{integration => e2e}/gssp-redirect/pages/gsp-blog/[post].js (100%) rename test/{integration => e2e}/gssp-redirect/pages/gssp-blog/[post].js (100%) rename test/{integration => e2e}/gssp-redirect/pages/index.js (100%) create mode 100644 test/e2e/hashbang/hashbang.test.ts rename test/{integration => e2e}/hashbang/src/cases/cjs.cjs (100%) rename test/{integration => e2e}/hashbang/src/cases/js.js (100%) rename test/{integration => e2e}/hashbang/src/cases/mjs.mjs (100%) rename test/{integration => e2e}/hashbang/src/pages/index.js (100%) create mode 100644 test/e2e/hydration/hydration.test.ts rename test/{integration => e2e}/hydration/pages/404.js (100%) rename test/{integration => e2e}/hydration/pages/_app.js (100%) rename test/{integration => e2e}/hydration/pages/_document.js (100%) rename test/{integration => e2e}/hydration/pages/details.js (100%) rename test/{integration => e2e}/hydration/pages/index.js (100%) create mode 100644 test/e2e/i18n-support-base-path/i18n-support-base-path.test.ts rename test/{integration => e2e}/i18n-support-base-path/next.config.js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/404.js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/[post]/[comment].js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/[post]/index.js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/_app.js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/another.js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/api/hello.js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/api/post/[slug].js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/auto-export/index.js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/developments/index.js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/dynamic/[slug].js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/frank.js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/gsp/fallback/[slug].js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/gsp/index.js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/gsp/no-fallback/[slug].js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/gssp/[slug].js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/gssp/index.js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/index.js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/links.js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/locale-false.js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/mixed.js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/not-found/blocking-fallback/[slug].js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/not-found/fallback/[slug].js (100%) rename test/{integration => e2e}/i18n-support-base-path/pages/not-found/index.js (100%) rename test/{integration => e2e}/i18n-support-base-path/public/files/texts/file.txt (100%) rename test/{integration/i18n-support-catchall/test/index.test.ts => e2e/i18n-support-catchall/i18n-support-catchall.test.ts} (70%) rename test/{integration => e2e}/i18n-support-catchall/next.config.js (100%) rename test/{integration => e2e}/i18n-support-catchall/pages/[[...slug]].js (100%) rename test/{integration/i18n-support-custom-error/test/index.test.ts => e2e/i18n-support-custom-error/i18n-support-custom-error.test.ts} (55%) rename test/{integration => e2e}/i18n-support-custom-error/next.config.js (100%) rename test/{integration => e2e}/i18n-support-custom-error/pages/[slug].js (100%) rename test/{integration => e2e}/i18n-support-custom-error/pages/_error.js (100%) rename test/{integration => e2e}/i18n-support-custom-error/pages/index.js (100%) create mode 100644 test/e2e/i18n-support-fallback-rewrite-legacy/i18n-support-fallback-rewrite-legacy.test.ts rename test/{integration => e2e}/i18n-support-fallback-rewrite-legacy/next.config.js (100%) rename test/{integration => e2e}/i18n-support-fallback-rewrite-legacy/pages/another.js (100%) rename test/{integration => e2e}/i18n-support-fallback-rewrite-legacy/pages/dynamic/[slug].js (100%) rename test/{integration => e2e}/i18n-support-fallback-rewrite-legacy/pages/index.js (100%) create mode 100644 test/e2e/i18n-support-fallback-rewrite/i18n-support-fallback-rewrite.test.ts rename test/{integration => e2e}/i18n-support-fallback-rewrite/next.config.js (100%) rename test/{integration => e2e}/i18n-support-fallback-rewrite/pages/another.js (100%) rename test/{integration => e2e}/i18n-support-fallback-rewrite/pages/dynamic/[slug].js (100%) rename test/{integration => e2e}/i18n-support-fallback-rewrite/pages/index.js (100%) rename test/{integration/i18n-support-index-rewrite/test/index.test.ts => e2e/i18n-support-index-rewrite/i18n-support-index-rewrite.test.ts} (52%) rename test/{integration => e2e}/i18n-support-index-rewrite/next.config.js (100%) rename test/{integration => e2e}/i18n-support-index-rewrite/pages/[...slug].js (100%) create mode 100644 test/e2e/i18n-support-same-page-hash-change/i18n-support-same-page-hash-change.test.ts rename test/{integration => e2e}/i18n-support-same-page-hash-change/next.config.js (100%) rename test/{integration => e2e}/i18n-support-same-page-hash-change/pages/about.js (100%) rename test/{integration => e2e}/i18n-support-same-page-hash-change/pages/posts/[...slug].js (100%) rename test/{integration/i18n-support/test/index.test.ts => e2e/i18n-support/i18n-support.test.ts} (55%) rename test/{integration => e2e}/i18n-support/next.config.js (100%) rename test/{integration => e2e}/i18n-support/pages/404.js (100%) rename test/{integration => e2e}/i18n-support/pages/[post]/[comment].js (100%) rename test/{integration => e2e}/i18n-support/pages/[post]/index.js (100%) rename test/{integration => e2e}/i18n-support/pages/_app.js (53%) rename test/{integration => e2e}/i18n-support/pages/another.js (100%) rename test/{integration => e2e}/i18n-support/pages/api/hello.js (100%) rename test/{integration => e2e}/i18n-support/pages/api/post/[slug].js (100%) rename test/{integration => e2e}/i18n-support/pages/auto-export/index.js (100%) rename test/{integration => e2e}/i18n-support/pages/developments/index.js (100%) rename test/{integration => e2e}/i18n-support/pages/dynamic/[slug].js (100%) rename test/{integration => e2e}/i18n-support/pages/frank.js (100%) rename test/{integration => e2e}/i18n-support/pages/gsp/fallback/[slug].js (100%) rename test/{integration => e2e}/i18n-support/pages/gsp/index.js (100%) rename test/{integration => e2e}/i18n-support/pages/gsp/no-fallback/[slug].js (100%) rename test/{integration => e2e}/i18n-support/pages/gssp/[slug].js (100%) rename test/{integration => e2e}/i18n-support/pages/gssp/index.js (100%) rename test/{integration => e2e}/i18n-support/pages/index.js (100%) rename test/{integration => e2e}/i18n-support/pages/links.js (100%) rename test/{integration => e2e}/i18n-support/pages/locale-false.js (100%) rename test/{integration => e2e}/i18n-support/pages/mixed.js (100%) rename test/{integration => e2e}/i18n-support/pages/not-found/blocking-fallback/[slug].js (100%) rename test/{integration => e2e}/i18n-support/pages/not-found/fallback/[slug].js (100%) rename test/{integration => e2e}/i18n-support/pages/not-found/index.js (100%) rename test/{integration => e2e}/i18n-support/public/files/texts/file.txt (100%) rename test/{integration/i18n-support/test => e2e/i18n-support}/shared.ts (99%) rename test/{integration => e2e}/image-optimizer/app/.gitignore (100%) rename test/{integration => e2e}/image-optimizer/app/pages/api/application.svg.js (100%) rename test/{integration => e2e}/image-optimizer/app/pages/api/comma.svg.js (100%) rename test/{integration => e2e}/image-optimizer/app/pages/api/conditional-cookie.js (100%) rename test/{integration => e2e}/image-optimizer/app/pages/api/no-header.js (100%) rename test/{integration => e2e}/image-optimizer/app/pages/api/stateful/test.png.js (100%) rename test/{integration => e2e}/image-optimizer/app/pages/api/uppercase.svg.js (100%) rename test/{integration => e2e}/image-optimizer/app/pages/api/wrong-header.svg.js (100%) rename test/{integration => e2e}/image-optimizer/app/pages/index.js (100%) rename test/{integration => e2e}/image-optimizer/app/public/animated.gif (100%) rename test/{integration => e2e}/image-optimizer/app/public/animated.png (100%) rename test/{integration => e2e}/image-optimizer/app/public/animated.webp (100%) rename test/{integration => e2e}/image-optimizer/app/public/animated2.png (100%) rename test/{integration => e2e}/image-optimizer/app/public/grayscale.png (100%) rename test/{integration => e2e}/image-optimizer/app/public/mountains.jpg (100%) rename test/{integration => e2e}/image-optimizer/app/public/png-as-octet-stream (100%) rename test/{integration => e2e}/image-optimizer/app/public/test.avif (100%) rename test/{integration => e2e}/image-optimizer/app/public/test.bmp (100%) rename test/{integration => e2e}/image-optimizer/app/public/test.gif (100%) rename test/{integration => e2e}/image-optimizer/app/public/test.heic (100%) rename test/{integration => e2e}/image-optimizer/app/public/test.icns (100%) rename test/{integration => e2e}/image-optimizer/app/public/test.ico (100%) rename test/{integration => e2e}/image-optimizer/app/public/test.jp2 (100%) rename test/{integration/build-trace-extra-entries-turbo => e2e/image-optimizer}/app/public/test.jpg (100%) rename test/{integration => e2e}/image-optimizer/app/public/test.jxl (100%) rename test/{integration => e2e}/image-optimizer/app/public/test.pdf (100%) rename test/{integration => e2e}/image-optimizer/app/public/test.pic (100%) rename test/{integration => e2e}/image-optimizer/app/public/test.png (100%) rename test/{integration => e2e}/image-optimizer/app/public/test.svg (100%) rename test/{integration => e2e}/image-optimizer/app/public/test.tiff (100%) rename test/{integration/next-image-legacy/base-path => e2e/image-optimizer/app}/public/test.webp (100%) rename test/{integration => e2e}/image-optimizer/app/public/text.txt (100%) rename "test/integration/image-optimizer/app/public/\303\244\303\266\303\274\305\241\304\215\305\231\303\255.png" => "test/e2e/image-optimizer/app/public/\303\244\303\266\303\274\305\241\304\215\305\231\303\255.png" (100%) rename test/{integration/image-optimizer/test => e2e/image-optimizer}/content-disposition-type.test.ts (68%) rename test/{integration/image-optimizer/test => e2e/image-optimizer}/dangerously-allow-svg.test.ts (66%) rename test/{integration/image-optimizer/test => e2e/image-optimizer}/disable-write-to-cache-dir.test.ts (67%) create mode 100644 test/e2e/image-optimizer/image-optimizer.test.ts rename test/{integration/image-optimizer/test => e2e/image-optimizer}/max-disk-size-cache-85kb.test.ts (69%) rename test/{integration/image-optimizer/test => e2e/image-optimizer}/max-disk-size-cache-zero.test.ts (68%) rename test/{integration/image-optimizer/test => e2e/image-optimizer}/maximum-redirects-0.test.ts (66%) rename test/{integration/image-optimizer/test => e2e/image-optimizer}/maximum-redirects-1.test.ts (66%) rename test/{integration/image-optimizer/test => e2e/image-optimizer}/minimum-cache-ttl.test.ts (54%) create mode 100644 test/e2e/image-optimizer/sharp.test.ts rename test/{integration/image-optimizer/test => e2e/image-optimizer}/util.ts (76%) rename test/{integration => e2e}/import-assertion/data (100%) rename test/{integration => e2e}/import-assertion/data.d.ts (100%) create mode 100644 test/e2e/import-assertion/import-assertion.test.ts rename test/{integration/import-attributes => e2e/import-assertion}/pages/es.js (100%) rename test/{integration/import-attributes => e2e/import-assertion}/pages/ts.ts (100%) rename test/{integration => e2e}/import-attributes/data (100%) rename test/{integration => e2e}/import-attributes/data.d.ts (100%) create mode 100644 test/e2e/import-attributes/import-attributes.test.ts rename test/{integration/import-assertion => e2e/import-attributes}/pages/es.js (50%) rename test/{integration/import-assertion => e2e/import-attributes}/pages/ts.ts (50%) create mode 100644 test/e2e/index-index/index-index.test.ts rename test/{integration/future => e2e/index-index}/next.config.js (100%) rename test/{integration => e2e}/index-index/pages/index.js (100%) rename test/{integration => e2e}/index-index/pages/index/index.js (100%) rename test/{integration => e2e}/index-index/pages/index/index/index.js (100%) rename test/{integration => e2e}/index-index/pages/index/project/index.js (100%) rename test/{integration => e2e}/index-index/pages/index/user.js (100%) rename test/{integration => e2e}/index-index/pages/links.js (100%) create mode 100644 test/e2e/initial-ref/initial-ref.test.ts rename test/{integration => e2e}/initial-ref/pages/index.js (100%) create mode 100644 test/e2e/invalid-custom-routes/invalid-custom-routes.test.ts rename test/{integration/gsp-build-errors => e2e/invalid-custom-routes}/next.config.js (100%) rename test/{integration/file-serving => e2e/invalid-custom-routes}/pages/index.js (100%) create mode 100644 test/e2e/invalid-href/invalid-href.test.ts rename test/{integration => e2e}/invalid-href/pages/[post].js (100%) rename test/{integration => e2e}/invalid-href/pages/dynamic-route-mismatch-manual.js (100%) rename test/{integration => e2e}/invalid-href/pages/dynamic-route-mismatch.js (100%) rename test/{integration => e2e}/invalid-href/pages/exotic-href.js (100%) rename test/{integration => e2e}/invalid-href/pages/first.js (100%) rename test/{integration => e2e}/invalid-href/pages/index.js (100%) rename test/{integration => e2e}/invalid-href/pages/invalid-relative.js (100%) rename test/{integration => e2e}/invalid-href/pages/second.js (100%) rename test/{integration => e2e}/invalid-href/pages/third.js (100%) create mode 100644 test/e2e/invalid-middleware-matchers/invalid-middleware-matchers.test.ts rename test/{integration/gip-identifier => e2e/invalid-middleware-matchers}/pages/index.js (100%) create mode 100644 test/e2e/invalid-multi-match/invalid-multi-match.test.ts rename test/{integration => e2e}/invalid-multi-match/next.config.js (100%) rename test/{integration => e2e}/invalid-multi-match/pages/hello.js (100%) rename test/{integration/invalid-server-options/test/index.test.ts => e2e/invalid-server-options/invalid-server-options.test.ts} (97%) rename test/{integration => e2e}/invalid-server-options/pages/index.js (100%) rename test/{integration => e2e}/jsconfig-baseurl/components/world.js (100%) create mode 100644 test/e2e/jsconfig-baseurl/jsconfig-baseurl.test.ts rename test/{integration => e2e}/jsconfig-baseurl/jsconfig.json (100%) rename test/{integration/jsconfig-paths-wildcard => e2e/jsconfig-baseurl}/next.config.js (100%) rename test/{integration => e2e}/jsconfig-baseurl/pages/hello.js (100%) rename test/{integration => e2e}/jsconfig-paths/.gitignore (100%) rename test/{integration => e2e}/jsconfig-paths/components/hello.js (100%) rename test/{integration => e2e}/jsconfig-paths/components/world.js (100%) create mode 100644 test/e2e/jsconfig-paths/jsconfig-paths.test.ts rename test/{integration => e2e}/jsconfig-paths/jsconfig.json (100%) rename test/{integration => e2e}/jsconfig-paths/lib/a/api.js (100%) rename test/{integration => e2e}/jsconfig-paths/lib/b/api.js (100%) rename test/{integration => e2e}/jsconfig-paths/lib/b/b-only.js (100%) rename test/{integration => e2e}/jsconfig-paths/next.config.js (100%) rename test/{integration => e2e}/jsconfig-paths/node_modules/mypackage/data.js (100%) rename test/{integration => e2e}/jsconfig-paths/node_modules/mypackage/myfile.js (100%) rename test/{integration => e2e}/jsconfig-paths/pages/basic-alias.js (100%) rename test/{integration => e2e}/jsconfig-paths/pages/resolve-fallback.js (100%) rename test/{integration => e2e}/jsconfig-paths/pages/resolve-order.js (100%) rename test/{integration => e2e}/jsconfig-paths/pages/single-alias.js (100%) rename test/{integration => e2e}/link-ref-app/app/child-ref-func-cleanup/page.js (100%) rename test/{integration => e2e}/link-ref-app/app/child-ref-func/page.js (100%) rename test/{integration => e2e}/link-ref-app/app/child-ref/page.js (100%) rename test/{integration => e2e}/link-ref-app/app/class/page.js (100%) rename test/{integration => e2e}/link-ref-app/app/click-away-race-condition/page.js (100%) rename test/{integration => e2e}/link-ref-app/app/function/page.js (100%) rename test/{integration => e2e}/link-ref-app/app/layout.js (100%) rename test/{integration => e2e}/link-ref-app/app/page.js (100%) create mode 100644 test/e2e/link-ref-app/link-ref-app.test.ts create mode 100644 test/e2e/link-ref-pages/link-ref-pages.test.ts rename test/{integration => e2e}/link-ref-pages/pages/child-ref-func-cleanup.js (100%) rename test/{integration => e2e}/link-ref-pages/pages/child-ref-func.js (100%) rename test/{integration => e2e}/link-ref-pages/pages/child-ref.js (100%) rename test/{integration => e2e}/link-ref-pages/pages/class.js (100%) rename test/{integration => e2e}/link-ref-pages/pages/click-away-race-condition.js (100%) rename test/{integration => e2e}/link-ref-pages/pages/function.js (100%) rename test/{integration/invalid-config-values => e2e/link-ref-pages}/pages/index.js (100%) create mode 100644 test/e2e/middleware-basic/middleware-basic.test.ts rename test/{integration => e2e}/middleware-basic/middleware.ts (100%) rename test/{integration => e2e}/middleware-basic/next.config.js (100%) rename test/{integration => e2e}/middleware-basic/pages/index.js (100%) create mode 100644 test/e2e/middleware-src-node/middleware-src-node.test.ts rename test/{integration/index-index => e2e/middleware-src-node}/next.config.js (100%) rename test/{integration => e2e}/middleware-src-node/src/middleware.js (100%) rename test/{integration => e2e}/middleware-src-node/src/middleware.ts (100%) rename test/{integration => e2e}/middleware-src-node/src/pages/index.js (100%) create mode 100644 test/e2e/middleware-src/middleware-src.test.ts rename test/{integration => e2e}/middleware-src/src/middleware.js (100%) rename test/{integration => e2e}/middleware-src/src/middleware.ts (100%) rename test/{integration => e2e}/middleware-src/src/pages/index.js (100%) rename test/{integration => e2e}/module-ids/components/CustomComponent.tsx (100%) create mode 100644 test/e2e/module-ids/module-ids.test.ts rename test/{integration => e2e}/module-ids/module-with-long-name.js (100%) rename test/{integration => e2e}/module-ids/next.config.js (100%) rename test/{integration => e2e}/module-ids/node_modules/external-module-with-long-name.js (100%) rename test/{integration => e2e}/module-ids/pages/index.js (100%) create mode 100644 test/e2e/next-dynamic-css-asset-prefix/next-dynamic-css-asset-prefix.test.ts rename test/{integration/absolute-assetprefix => e2e/next-dynamic-css-asset-prefix}/next.config.js (100%) rename test/{integration => e2e}/next-dynamic-css-asset-prefix/src/Component2.jsx (100%) rename test/{integration => e2e}/next-dynamic-css-asset-prefix/src/Component2.module.scss (100%) rename test/{integration => e2e}/next-dynamic-css-asset-prefix/src/Content.jsx (100%) rename test/{integration => e2e}/next-dynamic-css-asset-prefix/src/Content.module.css (100%) rename test/{integration => e2e}/next-dynamic-css-asset-prefix/src/Content4.module.css (100%) rename test/{integration => e2e}/next-dynamic-css-asset-prefix/src/app/layout.tsx (100%) rename test/{integration => e2e}/next-dynamic-css-asset-prefix/src/app/test-app/page.tsx (100%) rename test/{integration => e2e}/next-dynamic-css-asset-prefix/src/inner/k.jsx (100%) rename test/{integration => e2e}/next-dynamic-css-asset-prefix/src/pages/index.jsx (100%) create mode 100644 test/e2e/next-dynamic-css/next-dynamic-css.test.ts rename test/{integration/middleware-src-node => e2e/next-dynamic-css}/next.config.js (100%) rename test/{integration => e2e}/next-dynamic-css/src/Component2.jsx (100%) rename test/{integration => e2e}/next-dynamic-css/src/Component2.module.scss (100%) rename test/{integration => e2e}/next-dynamic-css/src/Content.jsx (100%) rename test/{integration => e2e}/next-dynamic-css/src/Content.module.css (100%) rename test/{integration => e2e}/next-dynamic-css/src/Content4.module.css (100%) rename test/{integration => e2e}/next-dynamic-css/src/app/layout.tsx (100%) rename test/{integration => e2e}/next-dynamic-css/src/app/test-app/page.tsx (100%) rename test/{integration => e2e}/next-dynamic-css/src/inner/k.jsx (100%) rename test/{integration => e2e}/next-dynamic-css/src/pages/index.jsx (100%) rename test/{integration => e2e}/next-dynamic-lazy-compilation/.babelrc (100%) rename test/{integration => e2e}/next-dynamic-lazy-compilation/apples/index.js (100%) rename test/{integration => e2e}/next-dynamic-lazy-compilation/components/four.js (100%) rename test/{integration => e2e}/next-dynamic-lazy-compilation/components/one.js (100%) rename test/{integration => e2e}/next-dynamic-lazy-compilation/components/three.js (100%) rename test/{integration => e2e}/next-dynamic-lazy-compilation/components/two.js (100%) create mode 100644 test/e2e/next-dynamic-lazy-compilation/next-dynamic-lazy-compilation.test.ts rename test/{integration => e2e}/next-dynamic-lazy-compilation/next.config.js (100%) rename test/{integration => e2e}/next-dynamic-lazy-compilation/pages/index.js (100%) rename test/{integration => e2e}/next-dynamic/apples/index.js (100%) rename test/{integration => e2e}/next-dynamic/components/four.js (100%) rename test/{integration => e2e}/next-dynamic/components/one.js (100%) rename test/{integration => e2e}/next-dynamic/components/three.js (100%) rename test/{integration => e2e}/next-dynamic/components/two.js (100%) create mode 100644 test/e2e/next-dynamic/next-dynamic.test.ts rename test/{integration => e2e}/next-dynamic/pages/index.js (100%) create mode 100644 test/e2e/next-image-legacy/asset-prefix/asset-prefix.test.ts rename test/{integration => e2e}/next-image-legacy/asset-prefix/next.config.js (100%) rename test/{integration => e2e}/next-image-legacy/asset-prefix/pages/index.js (100%) rename test/{integration/build-trace-extra-entries/app => e2e/next-image-legacy/asset-prefix}/public/test.jpg (100%) rename test/{integration/next-image-legacy/base-path/test/static.test.ts => e2e/next-image-legacy/base-path/base-path-static.test.ts} (71%) create mode 100644 test/e2e/next-image-legacy/base-path/base-path.test.ts rename test/{integration => e2e}/next-image-legacy/base-path/components/TallImage.js (100%) rename test/{integration => e2e}/next-image-legacy/base-path/components/tall.png (100%) rename test/{integration => e2e}/next-image-legacy/base-path/next.config.js (100%) rename test/{integration => e2e}/next-image-legacy/base-path/pages/flex.js (100%) rename test/{integration => e2e}/next-image-legacy/base-path/pages/hidden-parent.js (100%) rename test/{integration => e2e}/next-image-legacy/base-path/pages/index.js (100%) rename test/{integration => e2e}/next-image-legacy/base-path/pages/invalid-src-proto-relative.js (100%) rename test/{integration => e2e}/next-image-legacy/base-path/pages/invalid-src.js (100%) rename test/{integration => e2e}/next-image-legacy/base-path/pages/layout-fill.js (100%) rename test/{integration => e2e}/next-image-legacy/base-path/pages/layout-fixed.js (100%) rename test/{integration => e2e}/next-image-legacy/base-path/pages/layout-intrinsic.js (100%) rename test/{integration => e2e}/next-image-legacy/base-path/pages/layout-responsive.js (100%) rename test/{integration => e2e}/next-image-legacy/base-path/pages/missing-src.js (100%) rename test/{integration => e2e}/next-image-legacy/base-path/pages/prose.js (100%) rename test/{integration => e2e}/next-image-legacy/base-path/pages/prose.module.css (100%) rename test/{integration => e2e}/next-image-legacy/base-path/pages/rotated.js (100%) rename test/{integration => e2e}/next-image-legacy/base-path/pages/sizes.js (100%) rename test/{integration => e2e}/next-image-legacy/base-path/pages/static-img.js (100%) rename test/{integration => e2e}/next-image-legacy/base-path/pages/update.js (100%) rename test/{integration => e2e}/next-image-legacy/base-path/public/exif-rotation.jpg (100%) rename test/{integration => e2e}/next-image-legacy/base-path/public/foo/test-rect.jpg (100%) rename test/{integration => e2e}/next-image-legacy/base-path/public/test.avif (100%) rename test/{integration => e2e}/next-image-legacy/base-path/public/test.bmp (100%) rename test/{integration => e2e}/next-image-legacy/base-path/public/test.gif (100%) rename test/{integration => e2e}/next-image-legacy/base-path/public/test.ico (100%) rename test/{integration/image-optimizer/app => e2e/next-image-legacy/base-path}/public/test.jpg (100%) rename test/{integration => e2e}/next-image-legacy/base-path/public/test.png (100%) rename test/{integration => e2e}/next-image-legacy/base-path/public/test.svg (100%) rename test/{integration => e2e}/next-image-legacy/base-path/public/test.tiff (100%) rename test/{integration/next-image-legacy/default => e2e/next-image-legacy/base-path}/public/test.webp (100%) rename test/{integration => e2e}/next-image-legacy/base-path/public/wide.png (100%) rename test/{integration => e2e}/next-image-legacy/default/components/TallImage.js (100%) rename test/{integration => e2e}/next-image-legacy/default/components/static-img.js (100%) rename test/{integration => e2e}/next-image-legacy/default/components/tall.png (100%) create mode 100644 test/e2e/next-image-legacy/default/default-static.test.ts create mode 100644 test/e2e/next-image-legacy/default/default.test.ts rename test/{integration => e2e}/next-image-legacy/default/pages/_document.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/blob.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/blurry-placeholder.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/drop-srcset.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/dynamic-static-img.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/flex.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/hidden-parent.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/index.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/inside-paragraph.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/invalid-loader.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/invalid-placeholder-blur-static.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/invalid-placeholder-blur.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/invalid-sizes.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/invalid-src-proto-relative.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/invalid-src.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/invalid-unsized.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/invalid-width-or-height.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/layout-fill-inside-nonrelative.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/layout-fill.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/layout-fixed.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/layout-intrinsic.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/layout-responsive-inside-flex.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/layout-responsive.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/lazy-src-change.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/lazy-withref.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/loader-svg.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/missing-src.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/on-error.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/on-load.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/on-loading-complete.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/priority-missing-warning.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/priority.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/prose.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/prose.module.css (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/quality-50.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/rotated.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/sizes.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/small-img-import.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/static-img.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/style-filter.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/style-inheritance.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/style-prop.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/update.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/valid-html-w3c.js (100%) rename test/{integration => e2e}/next-image-legacy/default/pages/warning-once.js (100%) rename test/{integration => e2e}/next-image-legacy/default/public/exif-rotation.jpg (100%) rename test/{integration => e2e}/next-image-legacy/default/public/foo/test-rect.jpg (100%) rename test/{integration/next-image-new/app-dir => e2e/next-image-legacy/default}/public/small.jpg (100%) rename test/{integration => e2e}/next-image-legacy/default/public/test.avif (100%) rename test/{integration => e2e}/next-image-legacy/default/public/test.bmp (100%) rename test/{integration => e2e}/next-image-legacy/default/public/test.gif (100%) rename test/{integration => e2e}/next-image-legacy/default/public/test.ico (100%) rename test/{integration/invalid-document-image-import => e2e/next-image-legacy/default}/public/test.jpg (100%) rename test/{integration => e2e}/next-image-legacy/default/public/test.png (100%) rename test/{integration => e2e}/next-image-legacy/default/public/test.svg (100%) rename test/{integration => e2e}/next-image-legacy/default/public/test.tiff (100%) rename test/{integration/next-image-legacy/unoptimized => e2e/next-image-legacy/default}/public/test.webp (100%) rename test/{integration => e2e}/next-image-legacy/default/public/wide.png (100%) rename test/{integration => e2e}/next-image-legacy/default/style.module.css (100%) create mode 100644 test/e2e/next-image-legacy/image-from-node-modules/image-from-node-modules.test.ts rename test/{integration => e2e}/next-image-legacy/image-from-node-modules/next.config.js (100%) rename test/{integration => e2e}/next-image-legacy/image-from-node-modules/node_modules/my-cool-image/index.js (100%) rename test/{integration => e2e}/next-image-legacy/image-from-node-modules/node_modules/my-cool-image/package.json (100%) rename test/{integration => e2e}/next-image-legacy/image-from-node-modules/pages/image-from-node-modules.js (100%) rename test/{integration => e2e}/next-image-legacy/trailing-slash/next.config.js (100%) rename test/{integration => e2e}/next-image-legacy/trailing-slash/pages/index.js (100%) rename test/{integration/next-image-legacy/asset-prefix => e2e/next-image-legacy/trailing-slash}/public/test.jpg (100%) create mode 100644 test/e2e/next-image-legacy/trailing-slash/trailing-slash.test.ts rename test/{integration => e2e}/next-image-legacy/typescript/components/image-card.tsx (100%) rename test/{integration => e2e}/next-image-legacy/typescript/components/image-dynamic-src.tsx (100%) rename test/{integration => e2e}/next-image-legacy/typescript/next.config.js (100%) rename test/{integration => e2e}/next-image-legacy/typescript/pages/invalid.tsx (100%) rename test/{integration => e2e}/next-image-legacy/typescript/pages/valid.tsx (100%) rename test/{integration => e2e}/next-image-legacy/typescript/public/tall.png (100%) rename test/{integration => e2e}/next-image-legacy/typescript/public/test.avif (100%) rename test/{integration => e2e}/next-image-legacy/typescript/public/test.svg (100%) create mode 100644 test/e2e/next-image-legacy/typescript/typescript.test.ts rename test/{integration => e2e}/next-image-legacy/unicode/next.config.js (100%) rename test/{integration => e2e}/next-image-legacy/unicode/pages/index.js (100%) rename test/{integration => e2e}/next-image-legacy/unicode/public/hello world.jpg (100%) rename "test/integration/next-image-legacy/unicode/public/\303\244\303\266\303\274\305\241\304\215\305\231\303\255.png" => "test/e2e/next-image-legacy/unicode/public/\303\244\303\266\303\274\305\241\304\215\305\231\303\255.png" (100%) rename test/{integration/next-image-legacy/unicode/test/index.test.ts => e2e/next-image-legacy/unicode/unicode.test.ts} (53%) rename test/{integration => e2e}/next-image-legacy/unoptimized/next.config.js (100%) rename test/{integration => e2e}/next-image-legacy/unoptimized/pages/index.js (100%) rename test/{integration/next-image-legacy/base-path => e2e/next-image-legacy/unoptimized}/public/test.jpg (100%) rename test/{integration => e2e}/next-image-legacy/unoptimized/public/test.png (100%) rename test/{integration/next-image-new/app-dir => e2e/next-image-legacy/unoptimized}/public/test.webp (100%) rename test/{integration/next-image-legacy/unoptimized/test/index.test.ts => e2e/next-image-legacy/unoptimized/unoptimized.test.ts} (64%) create mode 100644 test/e2e/next-image-new/app-dir-image-from-node-modules/app-dir-image-from-node-modules.test.ts rename test/{integration => e2e}/next-image-new/app-dir-image-from-node-modules/app/layout.js (100%) rename test/{integration => e2e}/next-image-new/app-dir-image-from-node-modules/app/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir-image-from-node-modules/next.config.js (100%) rename test/{integration => e2e}/next-image-new/app-dir-image-from-node-modules/node_modules/my-cool-image/index.js (100%) rename test/{integration => e2e}/next-image-new/app-dir-image-from-node-modules/node_modules/my-cool-image/package.json (100%) rename test/{integration/next-image-new/app-dir-localpatterns/test/index.test.ts => e2e/next-image-new/app-dir-localpatterns/app-dir-localpatterns.test.ts} (61%) rename test/{integration => e2e}/next-image-new/app-dir-localpatterns/app/does-not-exist/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir-localpatterns/app/images/static-img.png (100%) rename test/{integration => e2e}/next-image-new/app-dir-localpatterns/app/layout.js (100%) rename test/{integration => e2e}/next-image-new/app-dir-localpatterns/app/nested-assets-query/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir-localpatterns/app/nested-blocked/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir-localpatterns/app/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir-localpatterns/app/top-level/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir-localpatterns/next.config.js (100%) rename test/{integration => e2e}/next-image-new/app-dir-localpatterns/public/assets/test.png (100%) rename test/{integration => e2e}/next-image-new/app-dir-localpatterns/public/blocked/test.png (100%) rename test/{integration => e2e}/next-image-new/app-dir-localpatterns/public/test.png (100%) rename test/{integration => e2e}/next-image-new/app-dir-localpatterns/style.module.css (100%) rename test/{integration/next-image-new/app-dir-qualities/test/index.test.ts => e2e/next-image-new/app-dir-qualities/app-dir-qualities.test.ts} (51%) rename test/{integration => e2e}/next-image-new/app-dir-qualities/app/images/test.png (100%) rename test/{integration => e2e}/next-image-new/app-dir-qualities/app/invalid-quality/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir-qualities/app/layout.js (100%) rename test/{integration => e2e}/next-image-new/app-dir-qualities/app/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir-qualities/next.config.js (100%) create mode 100644 test/e2e/next-image-new/app-dir/app-dir-static.test.ts rename test/{integration/next-image-new/app-dir/test/index.test.ts => e2e/next-image-new/app-dir/app-dir.test.ts} (58%) rename test/{integration => e2e}/next-image-new/app-dir/app/blob/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/blurry-placeholder/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/data-url-placeholder/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/data-url-with-fill-and-sizes/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/drop-srcset/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/dynamic-static-img/async-image.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/dynamic-static-img/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/empty-string-src/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/fill-blur/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/fill-data-url-placeholder/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/fill-warnings/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/fill/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/flex/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/hidden-parent/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/inside-paragraph/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/invalid-Infinity-width/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/invalid-fill-position/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/invalid-fill-width/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/invalid-height/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/invalid-loader/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/invalid-placeholder-blur-static/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/invalid-placeholder-blur/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/invalid-src-leading-space/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/invalid-src-null/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/invalid-src-proto-relative/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/invalid-src-trailing-space/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/invalid-src/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/invalid-width/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/layout.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/legacy-layout-fill/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/legacy-layout-responsive/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/loader-svg/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/missing-alt/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/missing-height/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/missing-src/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/missing-width/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/on-error-before-hydration/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/on-error/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/on-load/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/on-loading-complete/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/override-src/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/picture/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/placeholder-blur/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/preload-missing-warning/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/preload/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/priority/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/prose/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/prose/prose.module.css (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/quality-50/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/ref-cleanup/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/rotated/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/should-not-warn-unmount/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/sizes/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/small-img-import/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/static-img/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/style-filter/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/style-inheritance/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/style-prop/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/update/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/valid-html-w3c/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/warning-once/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/app/wrapper-div/page.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/components/TallImage.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/components/static-img.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/components/tall.png (100%) rename test/{integration/next-dynamic-css => e2e/next-image-new/app-dir}/next.config.js (100%) rename test/{integration => e2e}/next-image-new/app-dir/public/exif-rotation.jpg (100%) rename test/{integration => e2e}/next-image-new/app-dir/public/foo/test-rect.jpg (100%) rename test/{integration/next-image-new/default => e2e/next-image-new/app-dir}/public/small.jpg (100%) rename test/{integration => e2e}/next-image-new/app-dir/public/super-wide.png (100%) rename test/{integration => e2e}/next-image-new/app-dir/public/test.avif (100%) rename test/{integration => e2e}/next-image-new/app-dir/public/test.bmp (100%) rename test/{integration => e2e}/next-image-new/app-dir/public/test.gif (100%) rename test/{integration => e2e}/next-image-new/app-dir/public/test.ico (100%) rename test/{integration/next-image-legacy/default => e2e/next-image-new/app-dir}/public/test.jpg (100%) rename test/{integration => e2e}/next-image-new/app-dir/public/test.png (100%) rename test/{integration => e2e}/next-image-new/app-dir/public/test.svg (100%) rename test/{integration => e2e}/next-image-new/app-dir/public/test.tiff (100%) rename test/{integration/next-image-new/base-path => e2e/next-image-new/app-dir}/public/test.webp (100%) rename test/{integration => e2e}/next-image-new/app-dir/public/test_light.png (100%) rename test/{integration => e2e}/next-image-new/app-dir/public/wide.png (100%) rename test/{integration => e2e}/next-image-new/app-dir/style.module.css (100%) create mode 100644 test/e2e/next-image-new/asset-prefix/asset-prefix.test.ts rename test/{integration => e2e}/next-image-new/asset-prefix/next.config.js (100%) rename test/{integration => e2e}/next-image-new/asset-prefix/pages/index.js (100%) rename test/{integration/next-image-legacy/react-virtualized => e2e/next-image-new/asset-prefix}/public/test.jpg (100%) rename test/{integration/next-image-new/base-path/test/static.test.ts => e2e/next-image-new/base-path/base-path-static.test.ts} (77%) create mode 100644 test/e2e/next-image-new/base-path/base-path.test.ts rename test/{integration => e2e}/next-image-new/base-path/components/TallImage.js (100%) rename test/{integration => e2e}/next-image-new/base-path/components/tall.png (100%) rename test/{integration => e2e}/next-image-new/base-path/next.config.js (100%) rename test/{integration => e2e}/next-image-new/base-path/pages/flex.js (100%) rename test/{integration => e2e}/next-image-new/base-path/pages/hidden-parent.js (100%) rename test/{integration => e2e}/next-image-new/base-path/pages/index.js (100%) rename test/{integration => e2e}/next-image-new/base-path/pages/invalid-src-proto-relative.js (100%) rename test/{integration => e2e}/next-image-new/base-path/pages/invalid-src.js (100%) rename test/{integration => e2e}/next-image-new/base-path/pages/missing-src.js (100%) rename test/{integration => e2e}/next-image-new/base-path/pages/prose.js (100%) rename test/{integration => e2e}/next-image-new/base-path/pages/prose.module.css (100%) rename test/{integration => e2e}/next-image-new/base-path/pages/sizes.js (100%) rename test/{integration => e2e}/next-image-new/base-path/pages/static-img.js (100%) rename test/{integration => e2e}/next-image-new/base-path/pages/update.js (100%) rename test/{integration => e2e}/next-image-new/base-path/public/exif-rotation.jpg (100%) rename test/{integration => e2e}/next-image-new/base-path/public/foo/test-rect.jpg (100%) rename test/{integration => e2e}/next-image-new/base-path/public/test.avif (100%) rename test/{integration => e2e}/next-image-new/base-path/public/test.bmp (100%) rename test/{integration => e2e}/next-image-new/base-path/public/test.gif (100%) rename test/{integration => e2e}/next-image-new/base-path/public/test.ico (100%) rename test/{integration/next-image-legacy/trailing-slash => e2e/next-image-new/base-path}/public/test.jpg (100%) rename test/{integration => e2e}/next-image-new/base-path/public/test.png (100%) rename test/{integration => e2e}/next-image-new/base-path/public/test.svg (100%) rename test/{integration => e2e}/next-image-new/base-path/public/test.tiff (100%) rename test/{integration/next-image-new/default => e2e/next-image-new/base-path}/public/test.webp (100%) rename test/{integration => e2e}/next-image-new/base-path/public/wide.png (100%) create mode 100644 test/e2e/next-image-new/both-basepath-trailingslash/both-basepath-trailingslash.test.ts rename test/{integration => e2e}/next-image-new/both-basepath-trailingslash/next.config.js (100%) rename test/{integration => e2e}/next-image-new/both-basepath-trailingslash/pages/index.js (100%) rename test/{integration/next-image-legacy/unoptimized => e2e/next-image-new/both-basepath-trailingslash}/public/test.jpg (100%) rename test/{integration => e2e}/next-image-new/default/components/TallImage.js (100%) rename test/{integration => e2e}/next-image-new/default/components/static-img.js (100%) rename test/{integration => e2e}/next-image-new/default/components/tall.png (100%) rename test/{integration/next-image-new/default/test/static.test.ts => e2e/next-image-new/default/default-static.test.ts} (84%) rename test/{integration/next-image-new/default/test/index.test.ts => e2e/next-image-new/default/default.test.ts} (58%) rename test/{integration => e2e}/next-image-new/default/pages/_document.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/blob.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/blurry-placeholder.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/data-url-placeholder.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/data-url-with-fill-and-sizes.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/drop-srcset.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/dynamic-static-img.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/edge.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/empty-string-src.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/fill-blur.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/fill-data-url-placeholder.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/fill-warnings.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/fill.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/flex.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/hidden-parent.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/index.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/inside-paragraph.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/invalid-Infinity-width.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/invalid-fill-position.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/invalid-fill-width.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/invalid-height.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/invalid-loader.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/invalid-placeholder-blur-static.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/invalid-placeholder-blur.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/invalid-src-leading-space.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/invalid-src-null.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/invalid-src-proto-relative.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/invalid-src-trailing-space.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/invalid-src.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/invalid-width.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/legacy-layout-fill.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/legacy-layout-responsive.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/loader-svg.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/missing-alt.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/missing-height.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/missing-src.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/missing-width.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/on-error-before-hydration.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/on-error.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/on-load.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/on-loading-complete.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/override-src.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/picture.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/placeholder-blur.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/preload-missing-warning.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/preload.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/priority.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/prose.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/prose.module.css (100%) rename test/{integration => e2e}/next-image-new/default/pages/quality-50.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/rotated.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/should-not-warn-unmount.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/sizes.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/small-img-import.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/static-img.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/style-filter.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/style-inheritance.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/style-prop.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/update.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/valid-html-w3c.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/warning-once.js (100%) rename test/{integration => e2e}/next-image-new/default/pages/wrapper-div.js (100%) rename test/{integration => e2e}/next-image-new/default/public/exif-rotation.jpg (100%) rename test/{integration => e2e}/next-image-new/default/public/foo/test-rect.jpg (100%) rename test/{integration/next-image-new/middleware => e2e/next-image-new/default}/public/small.jpg (100%) rename test/{integration => e2e}/next-image-new/default/public/super-wide.png (100%) rename test/{integration => e2e}/next-image-new/default/public/test.avif (100%) rename test/{integration => e2e}/next-image-new/default/public/test.bmp (100%) rename test/{integration => e2e}/next-image-new/default/public/test.gif (100%) rename test/{integration => e2e}/next-image-new/default/public/test.ico (100%) rename test/{integration/next-image-new/app-dir => e2e/next-image-new/default}/public/test.jpg (100%) rename test/{integration => e2e}/next-image-new/default/public/test.png (100%) rename test/{integration => e2e}/next-image-new/default/public/test.svg (100%) rename test/{integration => e2e}/next-image-new/default/public/test.tiff (100%) rename test/{integration/next-image-new/export-config => e2e/next-image-new/default}/public/test.webp (100%) rename test/{integration => e2e}/next-image-new/default/public/test_light.png (100%) rename test/{integration => e2e}/next-image-new/default/public/wide.png (100%) rename test/{integration => e2e}/next-image-new/default/style.module.css (100%) create mode 100644 test/e2e/next-image-new/image-from-node-modules/image-from-node-modules.test.ts rename test/{integration => e2e}/next-image-new/image-from-node-modules/next.config.js (100%) rename test/{integration => e2e}/next-image-new/image-from-node-modules/node_modules/my-cool-image/index.js (100%) rename test/{integration => e2e}/next-image-new/image-from-node-modules/node_modules/my-cool-image/package.json (100%) rename test/{integration => e2e}/next-image-new/image-from-node-modules/pages/index.js (100%) rename test/{integration => e2e}/next-image-new/loader-config-default-loader-with-file/dummy-loader.js (100%) create mode 100644 test/e2e/next-image-new/loader-config-default-loader-with-file/loader-config-default-loader-with-file.test.ts rename test/{integration => e2e}/next-image-new/loader-config-default-loader-with-file/next.config.js (100%) rename test/{integration => e2e}/next-image-new/loader-config-default-loader-with-file/pages/get-img-props.js (100%) rename test/{integration => e2e}/next-image-new/loader-config-default-loader-with-file/pages/index.js (100%) rename test/{integration => e2e}/next-image-new/loader-config-default-loader-with-file/public/logo.png (100%) rename test/{integration/export-image-loader => e2e/next-image-new/loader-config-edge-runtime}/dummy-loader.js (100%) create mode 100644 test/e2e/next-image-new/loader-config-edge-runtime/loader-config-edge-runtime.test.ts rename test/{integration => e2e}/next-image-new/loader-config-edge-runtime/next.config.js (100%) rename test/{integration => e2e}/next-image-new/loader-config-edge-runtime/pages/index.js (100%) rename test/{integration => e2e}/next-image-new/loader-config-edge-runtime/public/logo.png (100%) rename test/{integration/next-image-new/loader-config-edge-runtime => e2e/next-image-new/loader-config}/dummy-loader.js (100%) create mode 100644 test/e2e/next-image-new/loader-config/loader-config.test.ts rename test/{integration => e2e}/next-image-new/loader-config/next.config.js (100%) rename test/{integration => e2e}/next-image-new/loader-config/pages/get-img-props.js (100%) rename test/{integration => e2e}/next-image-new/loader-config/pages/index.js (100%) rename test/{integration => e2e}/next-image-new/loader-config/public/logo.png (100%) rename test/{integration => e2e}/next-image-new/trailing-slash/next.config.js (100%) rename test/{integration => e2e}/next-image-new/trailing-slash/pages/index.js (100%) rename test/{integration/next-image-new/asset-prefix => e2e/next-image-new/trailing-slash}/public/test.jpg (100%) create mode 100644 test/e2e/next-image-new/trailing-slash/trailing-slash.test.ts rename test/{integration => e2e}/next-image-new/typescript/components/image-card.tsx (100%) rename test/{integration => e2e}/next-image-new/typescript/components/image-dynamic-src.tsx (100%) rename test/{integration => e2e}/next-image-new/typescript/components/image-with-loader.tsx (100%) rename test/{integration => e2e}/next-image-new/typescript/next.config.js (100%) rename test/{integration => e2e}/next-image-new/typescript/pages/invalid.tsx (100%) rename test/{integration => e2e}/next-image-new/typescript/pages/valid.tsx (100%) rename test/{integration => e2e}/next-image-new/typescript/public/tall.png (100%) rename test/{integration => e2e}/next-image-new/typescript/public/test.avif (100%) rename test/{integration => e2e}/next-image-new/typescript/public/test.svg (100%) create mode 100644 test/e2e/next-image-new/typescript/typescript.test.ts rename test/{integration => e2e}/next-image-new/unicode/next.config.js (100%) rename test/{integration => e2e}/next-image-new/unicode/pages/index.js (100%) rename test/{integration => e2e}/next-image-new/unicode/public/hello world.jpg (100%) rename "test/integration/next-image-new/unicode/public/\303\244\303\266\303\274\305\241\304\215\305\231\303\255.png" => "test/e2e/next-image-new/unicode/public/\303\244\303\266\303\274\305\241\304\215\305\231\303\255.png" (100%) rename test/{integration/next-image-new/unicode/test/index.test.ts => e2e/next-image-new/unicode/unicode.test.ts} (63%) rename test/{integration => e2e}/next-image-new/unoptimized/next.config.js (100%) rename test/{integration => e2e}/next-image-new/unoptimized/pages/get-img-props.js (100%) rename test/{integration => e2e}/next-image-new/unoptimized/pages/index.js (100%) rename test/{integration/next-image-new/base-path => e2e/next-image-new/unoptimized}/public/test.jpg (100%) rename test/{integration => e2e}/next-image-new/unoptimized/public/test.png (100%) rename test/{integration => e2e}/next-image-new/unoptimized/public/test.webp (100%) create mode 100644 test/e2e/next-image-new/unoptimized/unoptimized.test.ts rename test/{integration/no-page-props/test/index.test.ts => e2e/no-page-props/no-page-props.test.ts} (66%) rename test/{integration => e2e}/no-page-props/pages/_app.js (100%) rename test/{integration => e2e}/no-page-props/pages/gsp.js (100%) rename test/{integration => e2e}/no-page-props/pages/gssp.js (100%) rename test/{integration => e2e}/no-page-props/pages/index.js (100%) create mode 100644 test/e2e/node-fetch-keep-alive/node-fetch-keep-alive.test.ts rename test/{integration => e2e}/node-fetch-keep-alive/pages/api/json.js (100%) rename test/{integration => e2e}/node-fetch-keep-alive/pages/blog/[slug].js (100%) rename test/{integration => e2e}/node-fetch-keep-alive/pages/ssg.js (100%) rename test/{integration => e2e}/node-fetch-keep-alive/pages/ssr.js (100%) create mode 100644 test/e2e/non-standard-node-env-warning/non-standard-node-env-warning.test.ts rename test/{integration => e2e}/non-standard-node-env-warning/pages/index.js (100%) rename test/{integration => e2e}/non-standard-node-env-warning/server.js (53%) rename test/{integration/next-image-new/app-dir => e2e/nullish-config}/next.config.js (100%) create mode 100644 test/e2e/nullish-config/nullish-config.test.ts rename test/{integration/config-output-export => e2e/nullish-config}/pages/index.js (100%) create mode 100644 test/e2e/optional-chaining-nullish-coalescing/optional-chaining-nullish-coalescing.test.ts rename test/{integration => e2e}/optional-chaining-nullish-coalescing/pages/nullish-coalescing.js (100%) rename test/{integration => e2e}/optional-chaining-nullish-coalescing/pages/optional-chaining.js (100%) rename test/{integration/trailing-slash-dist => e2e/port-env-var}/pages/index.js (100%) create mode 100644 test/e2e/port-env-var/port-env-var.test.ts rename test/{integration => e2e}/prerender-fallback-encoding/next.config.js (100%) rename test/{integration => e2e}/prerender-fallback-encoding/pages/fallback-blocking/[slug].js (100%) rename test/{integration => e2e}/prerender-fallback-encoding/pages/fallback-false/[slug].js (100%) rename test/{integration => e2e}/prerender-fallback-encoding/pages/fallback-true/[slug].js (100%) rename test/{integration => e2e}/prerender-fallback-encoding/paths.js (100%) rename test/{integration/prerender-fallback-encoding/test/index.test.ts => e2e/prerender-fallback-encoding/prerender-fallback-encoding.test.ts} (59%) rename test/{integration => e2e}/prerender-preview/pages/api/preview.js (100%) rename test/{integration => e2e}/prerender-preview/pages/api/read.js (100%) rename test/{integration => e2e}/prerender-preview/pages/api/reset.js (100%) rename test/{integration => e2e}/prerender-preview/pages/index.js (100%) rename test/{integration => e2e}/prerender-preview/pages/to-index.js (100%) create mode 100644 test/e2e/prerender-preview/prerender-preview.test.ts rename test/{integration => e2e}/preview-fallback/pages/api/disable.js (100%) rename test/{integration => e2e}/preview-fallback/pages/api/enable.js (100%) rename test/{integration => e2e}/preview-fallback/pages/fallback/[post].js (100%) rename test/{integration => e2e}/preview-fallback/pages/index.js (100%) rename test/{integration => e2e}/preview-fallback/pages/no-fallback/[post].js (100%) rename test/{integration/preview-fallback/test/index.test.ts => e2e/preview-fallback/preview-fallback.test.ts} (64%) rename test/{integration => e2e}/react-current-version/app/components/foo.js (100%) rename test/{integration => e2e}/react-current-version/app/components/red.tsx (100%) rename test/{integration => e2e}/react-current-version/app/components/streaming-data.js (100%) rename test/{integration => e2e}/react-current-version/app/next.config.js (100%) create mode 100644 test/e2e/react-current-version/app/package.json rename test/{integration => e2e}/react-current-version/app/pages/dynamic.js (100%) rename test/{integration => e2e}/react-current-version/app/pages/index.js (100%) rename test/{integration => e2e}/react-current-version/app/pages/use-flush-effect/styled-jsx.tsx (100%) rename test/{integration => e2e}/react-current-version/app/pages/use-id.js (100%) create mode 100644 test/e2e/react-current-version/react-current-version.test.ts rename test/{integration => e2e}/relay-graphql-swc-multi-project/project-a/__generated__/pagesAQuery.graphql.ts (100%) rename test/{integration => e2e}/relay-graphql-swc-multi-project/project-a/next.config.js (100%) rename test/{integration => e2e}/relay-graphql-swc-multi-project/project-a/pages/api/query.ts (100%) rename test/{integration => e2e}/relay-graphql-swc-multi-project/project-a/pages/index.tsx (100%) rename test/{integration => e2e}/relay-graphql-swc-multi-project/project-b/__generated__/pagesBQuery.graphql.ts (100%) rename test/{integration => e2e}/relay-graphql-swc-multi-project/project-b/next.config.js (100%) rename test/{integration => e2e}/relay-graphql-swc-multi-project/project-b/pages/api/query.ts (100%) rename test/{integration => e2e}/relay-graphql-swc-multi-project/project-b/pages/index.tsx (100%) create mode 100644 test/e2e/relay-graphql-swc-multi-project/relay-graphql-swc-multi-project.test.ts rename test/{integration => e2e}/relay-graphql-swc-multi-project/relay.config.js (100%) rename test/{integration => e2e}/relay-graphql-swc-multi-project/schema.graphql (100%) rename test/{integration => e2e}/relay-graphql-swc-single-project/next.config.js (100%) rename test/{integration => e2e}/relay-graphql-swc-single-project/pages/api/query.ts (100%) rename test/{integration => e2e}/relay-graphql-swc-single-project/pages/index.tsx (100%) rename test/{integration => e2e}/relay-graphql-swc-single-project/queries/__generated__/pagesQuery.graphql.ts (100%) rename test/{integration => e2e}/relay-graphql-swc-single-project/queries/pagesQuery.js (100%) create mode 100644 test/e2e/relay-graphql-swc-single-project/relay-graphql-swc-single-project.test.ts rename test/{integration => e2e}/relay-graphql-swc-single-project/relay.config.js (100%) rename test/{integration => e2e}/relay-graphql-swc-single-project/schema.graphql (100%) rename test/{integration => e2e}/repeated-slashes/app/next.config.js (100%) rename test/{integration => e2e}/repeated-slashes/app/pages/_error.js (100%) rename test/{integration => e2e}/repeated-slashes/app/pages/another.js (100%) rename test/{integration => e2e}/repeated-slashes/app/pages/index.js (100%) rename test/{integration => e2e}/repeated-slashes/app/pages/invalid.js (100%) rename test/{integration/repeated-slashes/test/index.test.ts => e2e/repeated-slashes/repeated-slashes.test.ts} (64%) rename test/{integration => e2e}/rewrite-with-browser-history/next.config.js (100%) rename test/{integration => e2e}/rewrite-with-browser-history/pages/dynamic-page/[[...param]].js (100%) rename test/{integration => e2e}/rewrite-with-browser-history/pages/index.js (100%) create mode 100644 test/e2e/rewrite-with-browser-history/rewrite-with-browser-history.test.ts rename test/{integration => e2e}/rewrites-client-resolving/next.config.js (100%) rename test/{integration => e2e}/rewrites-client-resolving/pages/404.js (100%) rename test/{integration => e2e}/rewrites-client-resolving/pages/category/[...slug].js (100%) rename test/{integration => e2e}/rewrites-client-resolving/pages/category/index.js (100%) rename test/{integration => e2e}/rewrites-client-resolving/pages/index.js (100%) rename test/{integration => e2e}/rewrites-client-resolving/pages/product/[productId].js (100%) rename test/{integration => e2e}/rewrites-client-resolving/pages/product/index.js (100%) rename test/{integration/rewrites-client-resolving/test/index.test.ts => e2e/rewrites-client-resolving/rewrites-client-resolving.test.ts} (57%) rename test/{integration => e2e}/rewrites-destination-query-array/next.config.js (100%) rename test/{integration => e2e}/rewrites-destination-query-array/pages/index.js (100%) create mode 100644 test/e2e/rewrites-destination-query-array/rewrites-destination-query-array.test.ts rename test/{integration => e2e}/rewrites-has-condition/next.config.js (100%) rename test/{integration => e2e}/rewrites-has-condition/pages/another.js (100%) rename test/{integration => e2e}/rewrites-has-condition/pages/index.js (100%) rename test/{integration/rewrites-has-condition/test/index.test.ts => e2e/rewrites-has-condition/rewrites-has-condition.test.ts} (50%) rename test/{integration => e2e}/rewrites-manual-href-as/next.config.js (100%) rename test/{integration => e2e}/rewrites-manual-href-as/pages/another.js (100%) rename test/{integration => e2e}/rewrites-manual-href-as/pages/index.js (100%) rename test/{integration => e2e}/rewrites-manual-href-as/pages/news/[[...slugs]].js (100%) rename test/{integration => e2e}/rewrites-manual-href-as/pages/preview/[slug].js (100%) rename test/{integration/rewrites-manual-href-as/test/index.test.ts => e2e/rewrites-manual-href-as/rewrites-manual-href-as.test.ts} (77%) rename test/{integration => e2e}/route-index/pages/index/index.js (100%) create mode 100644 test/e2e/route-index/route-index.test.ts rename test/{integration => e2e}/route-indexes/pages/api/sub/[id].js (100%) rename test/{integration => e2e}/route-indexes/pages/api/sub/index.js (100%) rename test/{integration => e2e}/route-indexes/pages/index.js (100%) rename test/{integration => e2e}/route-indexes/pages/nested-index/index/index.js (100%) rename test/{integration => e2e}/route-indexes/pages/sub/[id].js (100%) rename test/{integration => e2e}/route-indexes/pages/sub/index.js (100%) rename test/{integration/route-indexes/test/index.test.ts => e2e/route-indexes/route-indexes.test.ts} (59%) rename test/{integration/route-load-cancel-css => e2e/route-load-cancel}/pages/index.js (100%) rename test/{integration => e2e}/route-load-cancel/pages/page1.js (100%) rename test/{integration/route-load-cancel-css => e2e/route-load-cancel}/pages/page2.js (100%) create mode 100644 test/e2e/route-load-cancel/route-load-cancel.test.ts rename test/{integration => e2e}/router-hash-navigation/pages/index.js (100%) create mode 100644 test/e2e/router-hash-navigation/router-hash-navigation.test.ts rename test/{integration => e2e}/router-is-ready-app-gip/pages/_app.js (100%) rename test/{integration => e2e}/router-is-ready-app-gip/pages/appGip.js (100%) rename test/{integration => e2e}/router-is-ready-app-gip/pages/gsp.js (100%) rename test/{integration => e2e}/router-is-ready-app-gip/pages/invalid.js (100%) create mode 100644 test/e2e/router-is-ready-app-gip/router-is-ready-app-gip.test.ts rename test/{integration => e2e}/router-is-ready/pages/auto-export/[slug].js (100%) rename test/{integration => e2e}/router-is-ready/pages/auto-export/index.js (100%) rename test/{integration => e2e}/router-is-ready/pages/gip.js (100%) rename test/{integration => e2e}/router-is-ready/pages/gsp.js (100%) rename test/{integration => e2e}/router-is-ready/pages/gssp.js (100%) rename test/{integration => e2e}/router-is-ready/pages/invalid.js (100%) create mode 100644 test/e2e/router-is-ready/router-is-ready.test.ts rename test/{integration => e2e}/router-prefetch/pages/another-page.js (100%) rename test/{integration => e2e}/router-prefetch/pages/index.js (100%) create mode 100644 test/e2e/router-prefetch/router-prefetch.test.ts rename test/{integration => e2e}/router-rerender/middleware.js (100%) rename test/{integration => e2e}/router-rerender/next.config.js (100%) rename test/{integration => e2e}/router-rerender/pages/index.js (100%) create mode 100644 test/e2e/router-rerender/router-rerender.test.ts rename test/{integration/script-loader/base => e2e/script-loader}/next.config.js (100%) rename test/{integration/script-loader/base => e2e/script-loader}/pages/_app.js (100%) rename test/{integration/script-loader/base => e2e/script-loader}/pages/_document.js (100%) rename test/{integration/script-loader/base => e2e/script-loader}/pages/index.js (100%) rename test/{integration/script-loader/base => e2e/script-loader}/pages/page1.js (100%) rename test/{integration/script-loader/base => e2e/script-loader}/pages/page10.js (100%) rename test/{integration/script-loader/base => e2e/script-loader}/pages/page3.js (100%) rename test/{integration/script-loader/base => e2e/script-loader}/pages/page4.js (91%) rename test/{integration/script-loader/base => e2e/script-loader}/pages/page5.js (100%) rename test/{integration/script-loader/base => e2e/script-loader}/pages/page6.js (100%) rename test/{integration/script-loader/base => e2e/script-loader}/pages/page7.js (100%) rename test/{integration/script-loader/base => e2e/script-loader}/pages/page8.js (100%) rename test/{integration/script-loader/base => e2e/script-loader}/pages/page9.js (100%) create mode 100644 test/e2e/script-loader/partytown-missing.test.ts rename test/{integration => e2e}/script-loader/partytown-missing/next.config.js (100%) rename test/{integration => e2e}/script-loader/partytown-missing/pages/index.js (100%) create mode 100644 test/e2e/script-loader/script-loader.test.ts rename test/{integration/script-loader/base => e2e/script-loader}/styles/styles.css (100%) rename test/{integration => e2e}/scroll-back-restoration/next.config.js (100%) rename test/{integration => e2e}/scroll-back-restoration/pages/another.js (100%) rename test/{integration => e2e}/scroll-back-restoration/pages/index.js (100%) rename test/{integration/scroll-back-restoration/test/index.test.ts => e2e/scroll-back-restoration/scroll-back-restoration.test.ts} (56%) rename test/{integration => e2e}/scroll-forward-restoration/next.config.js (100%) rename test/{integration => e2e}/scroll-forward-restoration/pages/another.js (100%) rename test/{integration => e2e}/scroll-forward-restoration/pages/index.js (100%) create mode 100644 test/e2e/scroll-forward-restoration/scroll-forward-restoration.test.ts rename test/{integration => e2e}/server-asset-modules/my-data.json (100%) rename test/{integration => e2e}/server-asset-modules/pages/api/test.js (100%) create mode 100644 test/e2e/server-asset-modules/server-asset-modules.test.ts rename test/{integration => e2e}/src-dir-support-double-dir/pages/index.js (100%) create mode 100644 test/e2e/src-dir-support-double-dir/src-dir-support-double-dir.test.ts rename test/{integration => e2e}/src-dir-support-double-dir/src/pages/hello.js (100%) rename test/{integration => e2e}/src-dir-support-double-dir/src/pages/index.js (100%) create mode 100644 test/e2e/src-dir-support/src-dir-support.test.ts rename test/{integration => e2e}/src-dir-support/src/pages/[name]/[comment].js (100%) rename test/{integration => e2e}/src-dir-support/src/pages/[name]/comments.js (100%) rename test/{integration => e2e}/src-dir-support/src/pages/[name]/index.js (100%) rename test/{integration => e2e}/src-dir-support/src/pages/another.js (100%) rename test/{integration => e2e}/src-dir-support/src/pages/blog/[name]/comment/[id].js (100%) rename test/{integration => e2e}/src-dir-support/src/pages/index.js (100%) rename test/{integration => e2e}/src-dir-support/src/pages/on-mount/[post].js (100%) rename test/{integration => e2e}/ssg-dynamic-routes-404-page/pages/404.js (100%) rename test/{integration => e2e}/ssg-dynamic-routes-404-page/pages/post/[id].js (100%) create mode 100644 test/e2e/ssg-dynamic-routes-404-page/ssg-dynamic-routes-404-page.test.ts rename test/{integration => e2e}/static-page-name/pages/index.js (100%) rename test/{integration => e2e}/static-page-name/pages/static.js (100%) create mode 100644 test/e2e/static-page-name/static-page-name.test.ts rename test/{integration => e2e}/telemetry/.babelrc.default (100%) rename test/{integration => e2e}/telemetry/.babelrc.plugin (100%) rename test/{integration => e2e}/telemetry/.babelrc.preset (100%) rename test/{integration => e2e}/telemetry/_app/layout.jsx (100%) rename test/{integration => e2e}/telemetry/_app/use-cache/funtion-level-use-cache/page.jsx (100%) rename test/{integration => e2e}/telemetry/_app/use-cache/page.jsx (100%) rename test/{integration => e2e}/telemetry/_app/use-cache/page2/page.jsx (100%) rename test/{integration => e2e}/telemetry/adapter.js (100%) rename test/{integration => e2e}/telemetry/app/app-dir/page.js (100%) rename test/{integration => e2e}/telemetry/app/hello/page.js (100%) rename test/{integration => e2e}/telemetry/app/layout.js (100%) create mode 100644 test/e2e/telemetry/config.test.ts rename test/{integration => e2e}/telemetry/jsconfig.swc (100%) rename test/{integration => e2e}/telemetry/next.config.adapter-path (100%) rename test/{integration => e2e}/telemetry/next.config.cache-components (100%) rename test/{integration => e2e}/telemetry/next.config.custom-routes (100%) rename test/{integration => e2e}/telemetry/next.config.filesystem-cache (100%) rename test/{integration => e2e}/telemetry/next.config.i18n-images (100%) rename test/{integration => e2e}/telemetry/next.config.middleware-options (100%) rename test/{integration => e2e}/telemetry/next.config.next-script-workers (100%) rename test/{integration => e2e}/telemetry/next.config.optimize-css (100%) rename test/{integration => e2e}/telemetry/next.config.reactCompiler-base (100%) rename test/{integration => e2e}/telemetry/next.config.reactCompiler-options (100%) rename test/{integration => e2e}/telemetry/next.config.swc (100%) rename test/{integration => e2e}/telemetry/next.config.swc-plugins (100%) rename test/{integration => e2e}/telemetry/next.config.transpile-packages (100%) rename test/{integration => e2e}/telemetry/next.config.use-cache (100%) rename test/{integration => e2e}/telemetry/next.config.webpack (100%) rename test/{integration => e2e}/telemetry/package.babel (100%) rename test/{integration/react-current-version/app => e2e/telemetry}/package.json (65%) rename test/{integration => e2e}/telemetry/package.swc-plugins (100%) create mode 100644 test/e2e/telemetry/page-features.test.ts rename test/{integration => e2e}/telemetry/pages/__ytho__/lel.js (100%) rename test/{integration => e2e}/telemetry/pages/_app_withoutreportwebvitals.empty (100%) rename test/{integration => e2e}/telemetry/pages/_app_withreportwebvitals.empty (100%) rename test/{integration => e2e}/telemetry/pages/about.js (100%) rename test/{integration => e2e}/telemetry/pages/api/og.jsx (100%) rename test/{integration => e2e}/telemetry/pages/data.json (100%) rename test/{integration => e2e}/telemetry/pages/dynamic-file-imports/index.js (100%) rename test/{integration => e2e}/telemetry/pages/edge.js (100%) rename test/{integration => e2e}/telemetry/pages/gip.js (100%) rename test/{integration => e2e}/telemetry/pages/gssp-again.js (100%) rename test/{integration => e2e}/telemetry/pages/gssp.js (100%) rename test/{integration => e2e}/telemetry/pages/hello.test.skip (100%) rename test/{integration/nullish-config => e2e/telemetry}/pages/index.js (100%) rename test/{integration => e2e}/telemetry/pages/script.js (100%) rename test/{integration => e2e}/telemetry/pages/ssg.js (100%) rename test/{integration => e2e}/telemetry/pages/ssg/[dynamic].js (100%) rename test/{integration => e2e}/telemetry/pages/warning.skip (100%) rename test/{integration => e2e}/telemetry/public/small.jpg (100%) create mode 100644 test/e2e/telemetry/telemetry.test.ts rename test/{integration => e2e}/trailing-slashes-href-resolving/next.config.js (100%) rename test/{integration => e2e}/trailing-slashes-href-resolving/pages/404.js (100%) rename test/{integration => e2e}/trailing-slashes-href-resolving/pages/[slug].js (100%) rename test/{integration => e2e}/trailing-slashes-href-resolving/pages/another.js (100%) rename test/{integration => e2e}/trailing-slashes-href-resolving/pages/blog/[slug].js (100%) rename test/{integration => e2e}/trailing-slashes-href-resolving/pages/blog/another.js (100%) rename test/{integration => e2e}/trailing-slashes-href-resolving/pages/catch-all/[...slug].js (100%) rename test/{integration => e2e}/trailing-slashes-href-resolving/pages/catch-all/first.js (100%) rename test/{integration => e2e}/trailing-slashes-href-resolving/pages/index.js (100%) rename test/{integration/trailing-slashes-href-resolving/test/index.test.ts => e2e/trailing-slashes-href-resolving/trailing-slashes-href-resolving.test.ts} (55%) rename test/{integration => e2e}/trailing-slashes-rewrite/next.config.js (73%) rename test/{integration => e2e}/trailing-slashes-rewrite/pages/catch-all/[...slug].js (100%) rename test/{integration => e2e}/trailing-slashes-rewrite/pages/index.js (100%) rename test/{integration => e2e}/trailing-slashes-rewrite/pages/products/[product].js (100%) rename test/{integration => e2e}/trailing-slashes-rewrite/pages/products/index.js (100%) rename test/{integration => e2e}/trailing-slashes-rewrite/server.js (100%) create mode 100644 test/e2e/trailing-slashes-rewrite/trailing-slashes-rewrite.test.ts rename test/{integration => e2e}/typescript-only-remove-type-imports/.babelrc (100%) rename test/{integration => e2e}/typescript-only-remove-type-imports/User.ts (100%) rename test/{integration => e2e}/typescript-only-remove-type-imports/UserStatistics.ts (100%) rename test/{integration => e2e}/typescript-only-remove-type-imports/pages/index.tsx (100%) rename test/{integration => e2e}/typescript-only-remove-type-imports/pages/normal.tsx (100%) create mode 100644 test/e2e/typescript-only-remove-type-imports/typescript-only-remove-type-imports.test.ts rename test/{integration => e2e}/typescript/components/angle-bracket-type-assertions.ts (100%) rename test/{integration => e2e}/typescript/components/generics.ts (100%) rename test/{integration => e2e}/typescript/components/hello.module.css (100%) rename test/{integration => e2e}/typescript/components/hello.module.sass (100%) rename test/{integration => e2e}/typescript/components/hello.module.scss (100%) rename test/{integration => e2e}/typescript/components/hello.ts (100%) rename test/{integration => e2e}/typescript/components/image-legacy.tsx (100%) rename test/{integration => e2e}/typescript/components/image.tsx (100%) rename test/{integration => e2e}/typescript/components/link.tsx (100%) rename test/{integration => e2e}/typescript/components/router.tsx (100%) rename test/{integration => e2e}/typescript/components/world.tsx (100%) rename test/{integration => e2e}/typescript/extension-order/js-first.js (100%) rename test/{integration => e2e}/typescript/extension-order/js-first.ts (100%) rename test/{integration/typescript-hmr => e2e/typescript}/next.config.js (100%) rename test/{integration => e2e}/typescript/pages/_app.tsx (100%) rename test/{integration => e2e}/typescript/pages/_document.tsx (100%) rename test/{integration => e2e}/typescript/pages/_error.tsx (100%) rename test/{integration => e2e}/typescript/pages/angle-bracket-type-assertions.tsx (100%) rename test/{integration => e2e}/typescript/pages/api/async.tsx (100%) rename test/{integration => e2e}/typescript/pages/api/sync.tsx (100%) rename test/{integration => e2e}/typescript/pages/generics.tsx (100%) rename test/{integration => e2e}/typescript/pages/hello.tsx (100%) rename test/{integration => e2e}/typescript/pages/ssg/[slug].tsx (100%) rename test/{integration => e2e}/typescript/pages/ssg/blog/[post].tsx (100%) rename test/{integration => e2e}/typescript/pages/ssg/blog/index.tsx (100%) rename test/{integration => e2e}/typescript/pages/ssr/[slug].tsx (100%) rename test/{integration => e2e}/typescript/pages/ssr/blog/[post].tsx (100%) rename test/{integration => e2e}/typescript/pages/ssr/cookies.tsx (100%) rename test/{integration => e2e}/typescript/pages/ssr/promise.tsx (100%) create mode 100644 test/e2e/typescript/typescript.test.ts rename test/{integration => e2e}/undefined-webpack-config/next.config.js (100%) rename test/{integration => e2e}/undefined-webpack-config/pages/index.js (100%) create mode 100644 test/e2e/undefined-webpack-config/undefined-webpack-config.test.ts rename test/{integration => e2e}/webpack-require-hook/next.config.js (100%) rename test/{integration => e2e}/webpack-require-hook/pages/hello.js (100%) create mode 100644 test/e2e/webpack-require-hook/webpack-require-hook.test.ts rename test/{integration => e2e}/worker-webpack5/lib/sharedCode.js (100%) rename test/{integration => e2e}/worker-webpack5/lib/worker.js (100%) rename test/{integration/page-config => e2e/worker-webpack5}/next.config.js (100%) rename test/{integration => e2e}/worker-webpack5/pages/index.js (100%) create mode 100644 test/e2e/worker-webpack5/worker-webpack5.test.ts delete mode 100644 test/integration/404-page-app/test/index.test.ts delete mode 100644 test/integration/404-page-custom-error/test/index.test.ts delete mode 100644 test/integration/404-page-ssg/test/index.test.ts delete mode 100644 test/integration/404-page/test/index.test.ts delete mode 100644 test/integration/500-page/test/gsp-gssp.test.ts delete mode 100644 test/integration/500-page/test/index.test.ts delete mode 100644 test/integration/absolute-assetprefix/test/index.test.ts delete mode 100644 test/integration/api-body-parser/test/index.test.ts delete mode 100644 test/integration/api-catch-all/test/index.test.ts delete mode 100644 test/integration/app-aspath/test/index.test.ts delete mode 100644 test/integration/app-config-asset-prefix/test/index.test.ts delete mode 100644 test/integration/app-document-import-order/test/index.test.ts delete mode 100644 test/integration/app-document-remove-hmr/test/index.test.ts delete mode 100644 test/integration/app-document-style-fragment/test/index.test.ts delete mode 100644 test/integration/app-dynamic-error/test/index.test.ts delete mode 100644 test/integration/app-functional/test/index.test.ts delete mode 100644 test/integration/app-tree/test/index.test.ts delete mode 100644 test/integration/app-tree/tsconfig.json delete mode 100644 test/integration/app-types/app-types.test.ts delete mode 100644 test/integration/auto-export-error-bail/test/index.test.ts delete mode 100644 test/integration/auto-export-query-error/test/index.test.ts delete mode 100644 test/integration/auto-export/test/index.test.ts delete mode 100644 test/integration/babel-custom/test/.babelrc delete mode 100644 test/integration/babel-custom/test/index.test.ts delete mode 100644 test/integration/babel-next-image/babel-next-image.test.ts delete mode 100644 test/integration/basepath-root-catch-all/test/index.test.ts delete mode 100644 test/integration/bigint/test/index.test.ts delete mode 100644 test/integration/broken-webpack-plugin/test/index.test.ts delete mode 100644 test/integration/build-output/test/index.test.ts delete mode 100644 test/integration/build-trace-extra-entries-monorepo/test/index.test.ts delete mode 100644 test/integration/build-trace-extra-entries-turbo/test/index.test.ts delete mode 100644 test/integration/build-trace-extra-entries/test/index.test.ts delete mode 100644 test/integration/build-warnings/test/index.test.ts delete mode 100644 test/integration/bundle-size-profiling/next.config.js delete mode 100644 test/integration/catches-missing-getStaticProps/test/index.test.ts delete mode 100644 test/integration/chunking/test/index.test.ts delete mode 100644 test/integration/cli/duplicate-sass/node_modules/node-sass/package.json delete mode 100644 test/integration/cli/duplicate-sass/node_modules/sass/package.json delete mode 100644 test/integration/cli/test/index.test.ts delete mode 100644 test/integration/client-404/test/index.test.ts delete mode 100644 test/integration/client-shallow-routing/test/index.test.ts delete mode 100644 test/integration/compression/test/index.test.ts delete mode 100644 test/integration/config-experimental-warning/test/index.test.ts delete mode 100644 test/integration/config-mjs/test/index.test.ts delete mode 100644 test/integration/config-output-export/test/index.test.ts delete mode 100644 test/integration/config-promise-error/test/index.test.ts delete mode 100644 test/integration/config-resolve-alias/test/index.test.ts delete mode 100644 test/integration/config-syntax-error/test/index.test.ts delete mode 100644 test/integration/config-validation/test/index.test.ts delete mode 100644 test/integration/config/node_modules/css-framework/framework.css delete mode 100644 test/integration/config/test/index.test.ts delete mode 100644 test/integration/conflicting-public-file-page/test/index.test.ts delete mode 100644 test/integration/conflicting-ssg-paths/test/index.test.ts delete mode 100644 test/integration/cpu-profiling/fixtures/basic-app/tsconfig.json delete mode 100644 test/integration/cpu-profiling/test/index.test.ts delete mode 100644 test/integration/create-next-app/templates/matrix.test.ts delete mode 100644 test/integration/create-next-app/utils.ts delete mode 100644 test/integration/critical-css/test/index.test.ts delete mode 100644 test/integration/css-client-nav/test/index.test.ts delete mode 100644 test/integration/css-customization/test/index.test.ts delete mode 100644 test/integration/css-features/test/browserslist.test.ts delete mode 100644 test/integration/css-features/test/css-modules.test.ts delete mode 100644 test/integration/css-features/test/index.test.ts delete mode 100644 test/integration/css-fixtures/cssmodules-pure-no-check/tsconfig.json delete mode 100644 test/integration/css-fixtures/multi-module/next.config.js delete mode 100644 test/integration/css-fixtures/url-global-asset-prefix-1/next.config.js delete mode 100644 test/integration/css-fixtures/url-global-asset-prefix-2/next.config.js delete mode 100644 test/integration/css-fixtures/with-tailwindcss-and-purgecss/pages/_app.js delete mode 100644 test/integration/css-fixtures/with-tailwindcss-and-purgecss/pages/index.js delete mode 100644 test/integration/css-fixtures/with-tailwindcss-and-purgecss/postcss.config.js delete mode 100644 test/integration/css-fixtures/with-tailwindcss-and-purgecss/styles/global.css delete mode 100644 test/integration/css-minify/test/index.test.ts delete mode 100644 test/integration/css-modules/test/index.test.ts delete mode 100644 test/integration/css/test/basic-global-support.test.ts delete mode 100644 test/integration/css/test/css-and-styled-jsx.test.ts delete mode 100644 test/integration/css/test/css-compilation.test.ts delete mode 100644 test/integration/css/test/css-modules.test.ts delete mode 100644 test/integration/css/test/css-rendering.test.ts delete mode 100644 test/integration/css/test/dev-css-handling.test.ts delete mode 100644 test/integration/css/test/valid-invalid-css.test.ts delete mode 100644 test/integration/custom-error-page-exception/test/index.test.ts delete mode 100644 test/integration/custom-error/test/index.test.ts delete mode 100644 test/integration/custom-page-extension/test/index.test.ts delete mode 100644 test/integration/custom-routes-catchall/test/index.test.ts delete mode 100644 test/integration/custom-routes-i18n-index-redirect/test/index.test.ts delete mode 100644 test/integration/custom-server-types/test/index.test.ts delete mode 100644 test/integration/custom-server-types/tsconfig.json delete mode 100644 test/integration/custom-server/pages/asset.js delete mode 100644 test/integration/custom-server/ssh/localhost-key.pem delete mode 100644 test/integration/custom-server/ssh/localhost.pem delete mode 100644 test/integration/custom-server/test/index.test.ts delete mode 100644 test/integration/data-fetching-errors/test/index.test.ts delete mode 100644 test/integration/dedupes-scripts/test/index.test.ts delete mode 100644 test/integration/development-hmr-refresh/test/index.test.ts delete mode 100644 test/integration/disable-js/test/index.test.ts delete mode 100644 test/integration/dist-dir/test/index.test.ts delete mode 100644 test/integration/document-file-dependencies/test/index.test.ts delete mode 100644 test/integration/document-head-warnings/test/index.test.ts delete mode 100644 test/integration/draft-mode/test/index.test.ts delete mode 100644 test/integration/draft-mode/tsconfig.json delete mode 100644 test/integration/dynamic-optional-routing-root-fallback/test/index.test.ts delete mode 100644 test/integration/dynamic-optional-routing-root-static-paths/test/index.test.ts delete mode 100644 test/integration/dynamic-optional-routing/test/index.test.ts delete mode 100644 test/integration/dynamic-require/test/index.test.ts delete mode 100644 test/integration/dynamic-route-rename/test/index.test.ts delete mode 100644 test/integration/dynamic-routing/test/middleware.test.ts delete mode 120000 test/integration/edge-runtime-configurable-guards/node_modules/lib delete mode 100644 test/integration/edge-runtime-configurable-guards/test/index.test.ts delete mode 100644 test/integration/edge-runtime-dynamic-code/test/index.test.ts delete mode 100644 test/integration/edge-runtime-module-errors/test/index.test.ts delete mode 100644 test/integration/edge-runtime-module-errors/test/module-imports.test.ts delete mode 100644 test/integration/edge-runtime-module-errors/test/utils.js delete mode 100644 test/integration/edge-runtime-response-error/test/index.test.ts delete mode 100644 test/integration/edge-runtime-streaming-error/test/index.test.ts delete mode 100644 test/integration/edge-runtime-with-node.js-apis/test/index.test.ts delete mode 100644 test/integration/empty-object-getInitialProps/test/index.test.ts delete mode 100644 test/integration/empty-project/test/index.test.ts delete mode 100644 test/integration/env-config/app/package.json delete mode 100644 test/integration/env-config/test/index.test.ts delete mode 100644 test/integration/error-in-error/test/index.test.ts delete mode 100644 test/integration/error-load-fail/test/index.test.ts delete mode 100644 test/integration/error-plugin-stack-overflow/test/index.test.ts delete mode 100644 test/integration/errors-on-output-to-public/test/index.test.ts delete mode 100644 test/integration/errors-on-output-to-static/test/index.test.ts delete mode 100644 test/integration/export-404/test/index.test.ts delete mode 100644 test/integration/export-dynamic-pages/test/index.test.ts delete mode 100644 test/integration/export-fallback-true-error/test/index.test.ts delete mode 100644 test/integration/export-getInitialProps-warn/test/index.test.ts delete mode 100644 test/integration/export-image-default/test/index.test.ts delete mode 100644 test/integration/export-image-loader-legacy/test/index.test.ts delete mode 100644 test/integration/export-image-loader/test/index.test.ts delete mode 100644 test/integration/export-index-not-found-gsp/test/index.test.ts delete mode 100644 test/integration/export-intent/test/index.test.ts delete mode 100644 test/integration/export-subfolders/test/index.test.ts delete mode 100644 test/integration/externals-esm-loose/node_modules/esm-package1/correct.mjs delete mode 100644 test/integration/externals-esm-loose/test/index.test.ts delete mode 100644 test/integration/externals-pages-bundle/test/externals.test.ts delete mode 100644 test/integration/externals-pages-bundle/test/index.test.ts delete mode 100644 test/integration/fallback-modules/test/index.test.ts delete mode 100644 test/integration/fallback-route-params/test/index.test.ts delete mode 100644 test/integration/fetch-polyfill-ky-universal/test/index.test.ts delete mode 100644 test/integration/fetch-polyfill/api-server.js delete mode 100644 test/integration/fetch-polyfill/test/index.test.ts delete mode 100644 test/integration/filesystempublicroutes/test/index.test.ts delete mode 100644 test/integration/firebase-grpc/test/index.test.ts delete mode 100644 test/integration/future/test/index.test.ts delete mode 100644 test/integration/getinitialprops/test/index.test.ts delete mode 100644 test/integration/getserversideprops-export-error/test/index.test.ts delete mode 100644 test/integration/getserversideprops-preview/test/index.test.ts delete mode 100644 test/integration/gip-identifier/test/index.test.ts delete mode 100644 test/integration/gsp-build-errors/test/index.test.ts delete mode 100644 test/integration/gsp-extension/test/index.test.ts delete mode 100644 test/integration/gssp-pageProps-merge/test/index.test.ts delete mode 100644 test/integration/gssp-redirect-with-rewrites/test/index.test.ts delete mode 100644 test/integration/handles-export-errors/test/index.test.ts delete mode 100644 test/integration/hashbang/test/index.test.ts delete mode 100644 test/integration/hydrate-then-render/test/index.test.ts delete mode 100644 test/integration/hydration/test/index.test.ts delete mode 100644 test/integration/i18n-support-base-path/test/index.test.ts delete mode 100644 test/integration/i18n-support-fallback-rewrite-legacy/test/index.test.ts delete mode 100644 test/integration/i18n-support-fallback-rewrite/test/index.test.ts delete mode 100644 test/integration/i18n-support-same-page-hash-change/test/index.test.ts delete mode 100644 test/integration/image-generation/test/index.test.ts delete mode 100644 test/integration/image-optimizer/app/next.config.js delete mode 100644 test/integration/image-optimizer/test/index.test.ts delete mode 100644 test/integration/image-optimizer/test/sharp.test.ts delete mode 100644 test/integration/import-assertion/next.config.js delete mode 100644 test/integration/import-assertion/test/index.test.ts delete mode 100644 test/integration/import-attributes/test/index.test.ts delete mode 100644 test/integration/import-attributes/tsconfig.json delete mode 100644 test/integration/index-index/test/index.test.ts delete mode 100644 test/integration/initial-ref/test/index.test.ts delete mode 100644 test/integration/invalid-config-values/test/index.test.ts delete mode 100644 test/integration/invalid-custom-routes/test/index.test.ts delete mode 100644 test/integration/invalid-document-image-import/test/index.test.ts delete mode 100644 test/integration/invalid-href/test/index.test.ts delete mode 100644 test/integration/invalid-middleware-matchers/test/index.test.ts delete mode 100644 test/integration/invalid-multi-match/test/index.test.ts delete mode 100644 test/integration/invalid-page-automatic-static-optimization/test/index.test.ts delete mode 100644 test/integration/invalid-revalidate-values/test/index.test.ts delete mode 100644 test/integration/jsconfig-baseurl/test/index.test.ts delete mode 100644 test/integration/jsconfig-empty/test/index.test.ts delete mode 100644 test/integration/jsconfig-paths-wildcard/test/index.test.ts delete mode 100644 test/integration/jsconfig-paths/test/index.test.ts delete mode 100644 test/integration/jsconfig/test/index.test.ts delete mode 100644 test/integration/json-serialize-original-error/test/index.test.ts delete mode 100644 test/integration/link-ref-app/test/index.test.ts delete mode 100644 test/integration/link-ref-pages/test/index.test.ts delete mode 100644 test/integration/link-with-encoding/test/index.test.ts delete mode 100644 test/integration/link-without-router/test/index.test.tsx delete mode 100644 test/integration/middleware-basic/test/index.test.ts delete mode 100644 test/integration/middleware-build-errors/test/index.test.ts delete mode 100644 test/integration/middleware-dev-update/test/index.test.ts delete mode 100644 test/integration/middleware-overrides-node.js-api/test/index.test.ts delete mode 100644 test/integration/middleware-prefetch/tests/index.test.ts delete mode 100644 test/integration/middleware-src-node/test/index.test.ts delete mode 100644 test/integration/middleware-src/test/index.test.ts delete mode 100644 test/integration/mixed-ssg-serverprops-error/test/index.test.ts delete mode 100644 test/integration/module-ids/test/index.test.ts delete mode 100644 test/integration/next-dynamic-css-asset-prefix/test/index.test.ts delete mode 100644 test/integration/next-dynamic-css-asset-prefix/tsconfig.json delete mode 100644 test/integration/next-dynamic-css/test/index.test.ts delete mode 100644 test/integration/next-dynamic-css/tsconfig.json delete mode 100644 test/integration/next-dynamic-lazy-compilation/test/index.test.ts delete mode 100644 test/integration/next-dynamic/test/index.test.ts delete mode 100644 test/integration/next-image-legacy/asset-prefix/test/index.test.ts delete mode 100644 test/integration/next-image-legacy/base-path/test/index.test.ts delete mode 100644 test/integration/next-image-legacy/basic/test/index.test.ts delete mode 100644 test/integration/next-image-legacy/custom-resolver/test/index.test.ts delete mode 100644 test/integration/next-image-legacy/default/test/index.test.ts delete mode 100644 test/integration/next-image-legacy/default/test/static.test.ts delete mode 100644 test/integration/next-image-legacy/image-from-node-modules/test/index.test.ts delete mode 100644 test/integration/next-image-legacy/no-intersection-observer-fallback/test/index.test.ts delete mode 100644 test/integration/next-image-legacy/noscript/test/index.test.ts delete mode 100644 test/integration/next-image-legacy/react-virtualized/test/index.test.ts delete mode 100644 test/integration/next-image-legacy/trailing-slash/test/index.test.ts delete mode 100644 test/integration/next-image-legacy/typescript/test/index.test.ts delete mode 100644 test/integration/next-image-legacy/typescript/tsconfig.json delete mode 100644 test/integration/next-image-new/app-dir-image-from-node-modules/test/index.test.ts delete mode 100644 test/integration/next-image-new/app-dir/test/static.test.ts delete mode 100644 test/integration/next-image-new/asset-prefix/test/index.test.ts delete mode 100644 test/integration/next-image-new/base-path/test/index.test.ts delete mode 100644 test/integration/next-image-new/both-basepath-trailingslash/test/index.test.ts delete mode 100644 test/integration/next-image-new/export-config/test/index.test.ts delete mode 100644 test/integration/next-image-new/image-from-node-modules/test/index.test.ts delete mode 100644 test/integration/next-image-new/invalid-image-import/test/index.test.ts delete mode 100644 test/integration/next-image-new/loader-config-default-loader-with-file/test/index.test.ts delete mode 100644 test/integration/next-image-new/loader-config-edge-runtime/test/index.test.ts delete mode 100644 test/integration/next-image-new/loader-config/test/index.test.ts delete mode 100644 test/integration/next-image-new/middleware/test/index.test.ts delete mode 100644 test/integration/next-image-new/middleware/test/middleware-intercept-next-image.test.ts delete mode 100644 test/integration/next-image-new/react-virtualized/test/index.test.ts delete mode 100644 test/integration/next-image-new/trailing-slash/test/index.test.ts delete mode 100644 test/integration/next-image-new/typescript/test/index.test.ts delete mode 100644 test/integration/next-image-new/typescript/tsconfig.json delete mode 100644 test/integration/next-image-new/unoptimized/test/index.test.ts delete mode 100644 test/integration/no-op-export/test/index.test.ts delete mode 100644 test/integration/no-override-next-props/test/index.test.ts delete mode 100644 test/integration/node-fetch-keep-alive/test/index.test.ts delete mode 100644 test/integration/non-next-dist-exclude/test/index.test.ts delete mode 100644 test/integration/non-standard-node-env-warning/test/index.test.ts delete mode 100644 test/integration/not-found-revalidate/test/index.test.ts delete mode 100644 test/integration/nullish-config/test/index.test.ts delete mode 100644 test/integration/numeric-sep/test/index.test.ts delete mode 100644 test/integration/ondemand/test/index.test.ts delete mode 100644 test/integration/optional-chaining-nullish-coalescing/test/index.test.ts delete mode 100644 test/integration/page-config/test/index.test.ts delete mode 100644 test/integration/page-extensions/test/index.test.ts delete mode 100644 test/integration/plugin-mdx-rs/test/index.test.ts delete mode 100644 test/integration/polyfilling-minimal/test/index.test.ts delete mode 100644 test/integration/polyfills/test/index.test.ts delete mode 100644 test/integration/port-env-var/test/index.test.ts delete mode 100644 test/integration/preload-viewport/test/index.test.ts delete mode 100644 test/integration/prerender-invalid-catchall-params/test/index.test.ts delete mode 100644 test/integration/prerender-invalid-paths/test/index.test.ts delete mode 100644 test/integration/prerender-no-revalidate/test/index.test.ts delete mode 100644 test/integration/prerender-preview/test/index.test.ts delete mode 100644 test/integration/prerender-revalidate/test/index.test.ts delete mode 100644 test/integration/prerender/pages/another/index.js delete mode 100644 test/integration/prerender/pages/api-docs/[...slug].js delete mode 100644 test/integration/prerender/pages/api/bad.js delete mode 100644 test/integration/prerender/pages/bad-gssp.js delete mode 100644 test/integration/prerender/pages/bad-ssr.js delete mode 100644 test/integration/prerender/pages/blocking-fallback-once/[slug].js delete mode 100644 test/integration/prerender/pages/blocking-fallback-some/[slug].js delete mode 100644 test/integration/prerender/pages/blocking-fallback/[slug].js delete mode 100644 test/integration/prerender/pages/blog/[post]/[comment].js delete mode 100644 test/integration/prerender/pages/blog/index.js delete mode 100644 test/integration/prerender/pages/catchall-explicit/[...slug].js delete mode 100644 test/integration/prerender/pages/catchall-optional/[[...slug]].js delete mode 100644 test/integration/prerender/pages/catchall/[...slug].js delete mode 100644 test/integration/prerender/pages/default-revalidate.js delete mode 100644 test/integration/prerender/pages/dynamic/[slug].js delete mode 100644 test/integration/prerender/pages/fallback-only/[slug].js delete mode 100644 test/integration/prerender/pages/index.js delete mode 100644 test/integration/prerender/pages/index/index.js delete mode 100644 test/integration/prerender/pages/lang/[lang]/about.js delete mode 100644 test/integration/prerender/pages/non-json-blocking/[p].js delete mode 100644 test/integration/prerender/pages/non-json/[p].js delete mode 100644 test/integration/prerender/pages/normal.js delete mode 100644 test/integration/prerender/pages/something.js delete mode 100644 test/integration/prerender/pages/user/[user]/profile.js delete mode 100644 test/integration/prerender/test/index.test.ts delete mode 100644 test/integration/production-build-dir/test/index.test.ts delete mode 100644 test/integration/production-config/test/index.test.ts delete mode 100644 test/integration/production-nav/test/index.test.ts delete mode 100644 test/integration/production-start-no-build/test/index.test.ts delete mode 100644 test/integration/query-with-encoding/test/index.test.ts delete mode 100644 test/integration/re-export-all-exports-from-page-disallowed/test/index.test.ts delete mode 100644 test/integration/re-export-all-exports-from-page-disallowed/world.txt delete mode 100644 test/integration/react-current-version/app/tsconfig.json delete mode 100644 test/integration/react-current-version/test/index.test.ts delete mode 100644 test/integration/react-current-version/tsconfig.json delete mode 100644 test/integration/relay-graphql-swc-multi-project/project-a/tsconfig.json delete mode 100644 test/integration/relay-graphql-swc-multi-project/project-b/tsconfig.json delete mode 100644 test/integration/relay-graphql-swc-multi-project/test/index.test.ts delete mode 100644 test/integration/relay-graphql-swc-single-project/test/index.test.ts delete mode 100644 test/integration/relay-graphql-swc-single-project/tsconfig.json delete mode 100644 test/integration/render-error-on-module-error/test/index.test.ts delete mode 100644 test/integration/render-error-on-top-level-error/with-get-initial-props/test/index.test.ts delete mode 100644 test/integration/render-error-on-top-level-error/without-get-initial-props/test/index.test.ts delete mode 100644 test/integration/revalidate-as-path/test/index.test.ts delete mode 100644 test/integration/rewrite-with-browser-history/test/index.test.ts delete mode 100644 test/integration/rewrites-destination-query-array/test/index.test.ts delete mode 100644 test/integration/root-catchall-cache/test/index.test.ts delete mode 100644 test/integration/root-optional-revalidate/test/index.test.ts delete mode 100644 test/integration/route-index/test/index.test.ts delete mode 100644 test/integration/route-load-cancel-css/test/index.test.ts delete mode 100644 test/integration/route-load-cancel/test/index.test.ts delete mode 100644 test/integration/router-hash-navigation/test/index.test.ts delete mode 100644 test/integration/router-is-ready-app-gip/test/index.test.ts delete mode 100644 test/integration/router-is-ready/test/index.test.ts delete mode 100644 test/integration/router-prefetch/test/index.test.ts delete mode 100644 test/integration/router-rerender/test/index.test.ts delete mode 100644 test/integration/script-loader/partytown/package.json delete mode 100644 test/integration/script-loader/partytown/pages/index.js delete mode 100644 test/integration/script-loader/test/index.test.ts delete mode 100644 test/integration/scroll-forward-restoration/test/index.test.ts delete mode 100644 test/integration/server-asset-modules/test/index.test.ts delete mode 100644 test/integration/server-side-dev-errors/test/index.test.ts delete mode 100644 test/integration/sharp-api/app/.gitignore delete mode 100644 test/integration/sharp-api/app/package-lock.json delete mode 100644 test/integration/sharp-api/app/package.json delete mode 100644 test/integration/sharp-api/test/sharp-api.test.ts delete mode 100644 test/integration/src-dir-support-double-dir/test/index.test.ts delete mode 100644 test/integration/src-dir-support/test/index.test.ts delete mode 100644 test/integration/ssg-dynamic-routes-404-page/test/index.test.ts delete mode 100644 test/integration/static-404/test/index.test.ts delete mode 100644 test/integration/static-page-name/test/index.test.ts delete mode 100644 test/integration/styled-jsx-plugin/app/package.json delete mode 100644 test/integration/styled-jsx-plugin/test/index.test.ts delete mode 100644 test/integration/telemetry/pages/index.js delete mode 100644 test/integration/telemetry/test/config.test.ts delete mode 100644 test/integration/telemetry/test/index.test.ts delete mode 100644 test/integration/telemetry/test/page-features.test.ts delete mode 100644 test/integration/test-file.txt delete mode 100644 test/integration/trailing-slash-dist/test/index.test.ts delete mode 100644 test/integration/trailing-slashes-rewrite/test/index.test.ts delete mode 100644 test/integration/turbopack-unsupported-log/index.test.ts delete mode 100644 test/integration/turborepo-access-trace/test/index.test.ts delete mode 100644 test/integration/turbotrace-with-webpack-worker/test/index.test.ts delete mode 100644 test/integration/typeof-window-replace/test/index.test.ts delete mode 100644 test/integration/typescript-app-type-declarations/next-env.d.ts delete mode 100644 test/integration/typescript-app-type-declarations/next-env.strictRouteTypes.d.ts delete mode 100644 test/integration/typescript-app-type-declarations/test/index.test.ts delete mode 100644 test/integration/typescript-app-type-declarations/tsconfig.json delete mode 100644 test/integration/typescript-custom-tsconfig/test/index.test.ts delete mode 100644 test/integration/typescript-external-dir/project/test/index.test.ts delete mode 100644 test/integration/typescript-filtered-files/test/index.test.ts delete mode 100644 test/integration/typescript-filtered-files/tsconfig.json delete mode 100644 test/integration/typescript-hmr/test/index.test.ts delete mode 100644 test/integration/typescript-ignore-errors/test/index.test.ts delete mode 100644 test/integration/typescript-ignore-errors/tsconfig.json delete mode 100644 test/integration/typescript-only-remove-type-imports/test/index.test.ts delete mode 100644 test/integration/typescript-only-remove-type-imports/tsconfig.json delete mode 100644 test/integration/typescript/test/index.test.ts delete mode 100644 test/integration/typescript/tsconfig.json delete mode 100644 test/integration/undefined-webpack-config/test/index.test.ts delete mode 100644 test/integration/webpack-bun-externals/test/index.test.ts delete mode 100644 test/integration/webpack-require-hook/test/index.test.ts delete mode 100644 test/integration/with-electron/app/.gitignore delete mode 100644 test/integration/with-electron/app/next.config.js delete mode 100644 test/integration/with-electron/app/package.json delete mode 100644 test/integration/with-electron/app/public/main.js delete mode 100644 test/integration/with-electron/next.config.js delete mode 100644 test/integration/with-electron/test/index.test.ts delete mode 100644 test/integration/worker-webpack5/test/index.test.ts create mode 100644 test/production/absolute-assetprefix/absolute-assetprefix.test.ts rename test/{integration/next-dynamic-css-asset-prefix => production/absolute-assetprefix}/next.config.js (100%) rename test/{integration => production}/absolute-assetprefix/pages/about.js (100%) rename test/{integration => production}/absolute-assetprefix/pages/gsp-fallback/[slug].js (100%) rename test/{integration => production}/absolute-assetprefix/pages/gssp.js (100%) rename test/{integration => production}/absolute-assetprefix/pages/index.js (100%) create mode 100644 test/production/app-document-style-fragment/app-document-style-fragment.test.ts rename test/{integration => production}/app-document-style-fragment/pages/_document.js (100%) rename test/{integration => production}/app-document-style-fragment/pages/index.js (100%) create mode 100644 test/production/app-dynamic-error/app-dynamic-error.test.ts rename test/{integration => production}/app-dynamic-error/app/dynamic-error/loading.js (100%) rename test/{integration => production}/app-dynamic-error/app/dynamic-error/page.js (100%) rename test/{integration => production}/app-dynamic-error/app/layout.js (100%) rename test/{integration/polyfilling-minimal => production/app-dynamic-error}/next.config.js (100%) create mode 100644 test/production/app-types/app-types.test.ts rename test/{integration => production}/app-types/next.config.js (100%) rename test/{integration => production}/app-types/package.json (100%) rename test/{integration => production}/app-types/src/app/(newroot)/dashboard/another/page.tsx (100%) rename test/{integration => production}/app-types/src/app/about/page.tsx (100%) rename test/{integration => production}/app-types/src/app/blog/[category]/[id]/page.tsx (100%) rename test/{integration => production}/app-types/src/app/dashboard/[...slug]/page.tsx (100%) rename test/{integration => production}/app-types/src/app/dashboard/user/[[...slug]]/page.tsx (100%) rename test/{integration => production}/app-types/src/app/layout.tsx (100%) rename test/{integration => production}/app-types/src/app/mdx-test/page.mdx (100%) rename test/{integration => production}/app-types/src/app/type-checks/config/page.tsx (100%) rename test/{integration => production}/app-types/src/app/type-checks/config/revalidate-with-seperators/page.tsx (100%) rename test/{integration => production}/app-types/src/app/type-checks/form/page.tsx (100%) rename test/{integration => production}/app-types/src/app/type-checks/layout/layout.tsx (100%) rename test/{integration => production}/app-types/src/app/type-checks/link/page.tsx (100%) rename test/{integration => production}/app-types/src/app/type-checks/redirect/page.tsx (100%) rename test/{integration => production}/app-types/src/app/type-checks/route-handlers/route.ts (100%) rename test/{integration => production}/app-types/src/app/type-checks/router/page.tsx (100%) rename test/{integration => production}/app-types/src/pages/aaa.js (100%) rename test/{integration/app-types/tsconfig.json => production/app-types/tsconfig.test.json} (100%) create mode 100644 test/production/auto-export-error-bail/auto-export-error-bail.test.ts rename test/{integration => production}/auto-export-error-bail/pages/app/_error.js (100%) create mode 100644 test/production/auto-export-query-error/auto-export-query-error.test.ts rename test/{integration => production}/auto-export-query-error/next.config.js (100%) rename test/{integration => production}/auto-export-query-error/pages/hello.js (100%) rename test/{integration => production}/auto-export-query-error/pages/ssg.js (100%) rename test/{integration => production}/auto-export-query-error/pages/ssr.js (100%) create mode 100644 test/production/babel-custom/babel-custom.test.ts rename test/{integration => production}/babel-custom/fixtures/babel-env/.babelrc (100%) rename test/{integration => production}/babel-custom/fixtures/babel-env/pages/index.js (100%) rename test/{integration => production}/babel-custom/fixtures/babel-json5/.babelrc (100%) rename test/{integration => production}/babel-custom/fixtures/babel-json5/pages/index.js (100%) rename test/{integration => production}/babel-custom/fixtures/targets-browsers/.babelrc (100%) rename test/{integration => production}/babel-custom/fixtures/targets-browsers/pages/index.js (100%) rename test/{integration => production}/babel-custom/fixtures/targets-string/.babelrc (100%) rename test/{integration => production}/babel-custom/fixtures/targets-string/pages/index.js (100%) create mode 100644 test/production/build-output/build-output.test.ts rename test/{integration => production}/build-output/fixtures/basic-app/pages/index.js (100%) rename test/{integration => production}/build-output/fixtures/basic-app/pages/slow-static/[propsDuration]/[renderDuration].js (100%) rename test/{integration => production}/build-output/fixtures/with-app/pages/_app.js (100%) rename test/{integration => production}/build-output/fixtures/with-app/pages/index.js (100%) rename test/{integration => production}/build-output/fixtures/with-error-static/pages/_error.js (100%) rename test/{integration => production}/build-output/fixtures/with-error-static/pages/index.js (100%) rename test/{integration => production}/build-output/fixtures/with-error/pages/_error.js (100%) rename test/{integration => production}/build-output/fixtures/with-error/pages/index.js (100%) rename test/{integration => production}/build-output/fixtures/with-parallel-routes/app/layout.js (100%) rename test/{integration => production}/build-output/fixtures/with-parallel-routes/app/page.js (100%) rename test/{integration => production}/build-output/fixtures/with-parallel-routes/app/root-page/@footer/default.js (100%) rename test/{integration => production}/build-output/fixtures/with-parallel-routes/app/root-page/@footer/page.js (100%) rename test/{integration => production}/build-output/fixtures/with-parallel-routes/app/root-page/@header/default.js (100%) rename test/{integration => production}/build-output/fixtures/with-parallel-routes/app/root-page/@header/page.js (100%) rename test/{integration => production}/build-output/fixtures/with-parallel-routes/app/root-page/layout.js (100%) rename test/{integration => production}/build-output/fixtures/with-parallel-routes/app/root-page/page.js (100%) rename test/{integration => production}/build-trace-extra-entries-monorepo/app/app/route1/route.js (100%) rename test/{integration => production}/build-trace-extra-entries-monorepo/app/next.config.js (100%) create mode 100644 test/production/build-trace-extra-entries-monorepo/build-trace-extra-entries-monorepo.test.ts rename test/{integration => production}/build-trace-extra-entries-monorepo/other/included.txt (100%) rename test/{integration => production}/build-trace-extra-entries-turbo/app/app/route1/route.js (100%) rename test/{integration => production}/build-trace-extra-entries-turbo/app/content/hello.json (100%) rename test/{integration => production}/build-trace-extra-entries-turbo/app/include-me/hello.txt (100%) rename test/{integration => production}/build-trace-extra-entries-turbo/app/include-me/second.txt (100%) rename test/{integration => production}/build-trace-extra-entries-turbo/app/lib/fetch-data.js (100%) rename test/{integration => production}/build-trace-extra-entries-turbo/app/lib/get-data.js (100%) rename test/{integration => production}/build-trace-extra-entries-turbo/app/next.config.js (86%) rename test/{integration => production}/build-trace-extra-entries-turbo/app/node_modules/nested-structure/constants/package.json (100%) rename test/{integration => production}/build-trace-extra-entries-turbo/app/node_modules/nested-structure/dist/constants.js (100%) rename test/{integration => production}/build-trace-extra-entries-turbo/app/node_modules/nested-structure/dist/index.js (100%) rename test/{integration => production}/build-trace-extra-entries-turbo/app/node_modules/nested-structure/package.json (100%) rename test/{integration => production}/build-trace-extra-entries-turbo/app/node_modules/some-cms/index.js (100%) rename test/{integration => production}/build-trace-extra-entries-turbo/app/node_modules/some-cms/package.json (100%) rename test/{integration => production}/build-trace-extra-entries-turbo/app/pages/another.js (100%) rename test/{integration => production}/build-trace-extra-entries-turbo/app/pages/image-import.js (100%) rename test/{integration => production}/build-trace-extra-entries-turbo/app/pages/index.js (100%) rename test/{integration => production}/build-trace-extra-entries-turbo/app/public/another.jpg (100%) rename test/{integration => production}/build-trace-extra-entries-turbo/app/public/exclude-me/another.txt (100%) rename test/{integration => production}/build-trace-extra-entries-turbo/app/public/exclude-me/hello.txt (100%) rename test/{integration/next-image-new/both-basepath-trailingslash => production/build-trace-extra-entries-turbo/app}/public/test.jpg (100%) create mode 100644 test/production/build-trace-extra-entries-turbo/build-trace-extra-entries-turbo.test.ts rename test/{integration => production}/build-trace-extra-entries/app/app/route1/route.js (100%) rename test/{integration => production}/build-trace-extra-entries/app/content/hello.json (100%) rename test/{integration => production}/build-trace-extra-entries/app/include-me-global.txt (100%) rename test/{integration => production}/build-trace-extra-entries/app/include-me/.dot-folder/another-file.txt (100%) rename test/{integration => production}/build-trace-extra-entries/app/include-me/hello.txt (100%) rename test/{integration => production}/build-trace-extra-entries/app/include-me/second.txt (100%) rename test/{integration => production}/build-trace-extra-entries/app/include-me/some-dir/file.txt (100%) rename test/{integration => production}/build-trace-extra-entries/app/lib/fetch-data.js (100%) rename test/{integration => production}/build-trace-extra-entries/app/lib/get-data.js (100%) rename test/{integration => production}/build-trace-extra-entries/app/lib/my-component.js (100%) rename test/{integration => production}/build-trace-extra-entries/app/next.config.js (100%) rename test/{integration => production}/build-trace-extra-entries/app/node_modules/nested-structure/constants/package.json (100%) rename test/{integration => production}/build-trace-extra-entries/app/node_modules/nested-structure/dist/constants.js (100%) rename test/{integration => production}/build-trace-extra-entries/app/node_modules/nested-structure/dist/index.js (100%) rename test/{integration => production}/build-trace-extra-entries/app/node_modules/nested-structure/package.json (100%) rename test/{integration => production}/build-trace-extra-entries/app/node_modules/pkg-behind-symlink (100%) rename test/{integration => production}/build-trace-extra-entries/app/node_modules/pkg/index.js (100%) rename test/{integration => production}/build-trace-extra-entries/app/node_modules/pkg/package.json (100%) rename test/{integration => production}/build-trace-extra-entries/app/node_modules/some-cms/index.js (100%) rename test/{integration => production}/build-trace-extra-entries/app/node_modules/some-cms/package.json (100%) rename test/{integration => production}/build-trace-extra-entries/app/pages/another.js (100%) rename test/{integration => production}/build-trace-extra-entries/app/pages/image-import.js (100%) rename test/{integration => production}/build-trace-extra-entries/app/pages/index.js (100%) rename test/{integration => production}/build-trace-extra-entries/app/public/another.jpg (100%) rename test/{integration => production}/build-trace-extra-entries/app/public/exclude-me/another.txt (100%) rename test/{integration => production}/build-trace-extra-entries/app/public/exclude-me/hello.txt (100%) rename test/{integration/next-image-new/default => production/build-trace-extra-entries/app}/public/test.jpg (100%) create mode 100644 test/production/build-trace-extra-entries/build-trace-extra-entries.test.ts create mode 100644 test/production/build-warnings/build-warnings.test.ts rename test/{integration => production}/build-warnings/next.config.js (100%) rename test/{integration => production}/build-warnings/pages/index.js (100%) create mode 100644 test/production/chunking/chunking.test.ts rename test/{integration => production}/chunking/components/one.js (100%) rename test/{integration => production}/chunking/next.config.js (100%) rename test/{integration => production}/chunking/pages/index.js (100%) rename test/{integration => production}/chunking/pages/page1.js (100%) rename test/{integration => production}/chunking/pages/page2.js (100%) rename test/{integration => production}/chunking/pages/page3.js (100%) create mode 100644 test/production/config-promise-error/config-promise-error.test.ts rename test/{integration => production}/config-promise-error/pages/index.js (100%) create mode 100644 test/production/config-resolve-alias/config-resolve-alias.test.ts rename test/{integration => production}/config-resolve-alias/next.config.js (100%) rename test/{integration => production}/config-resolve-alias/pages/index.js (100%) create mode 100644 test/production/config-syntax-error/config-syntax-error.test.ts rename test/{integration => production}/config-syntax-error/pages/index.js (100%) create mode 100644 test/production/config-validation/config-validation.test.ts rename test/{integration => production}/config-validation/pages/index.js (100%) create mode 100644 test/production/conflicting-ssg-paths/conflicting-ssg-paths.test.ts create mode 100644 test/production/cpu-profiling/cpu-profiling.test.ts rename test/{integration => production}/cpu-profiling/fixtures/basic-app/app/layout.tsx (100%) rename test/{integration => production}/cpu-profiling/fixtures/basic-app/app/page.tsx (100%) rename test/{integration => production}/create-next-app/__snapshots__/biome-config.test.ts.snap (100%) rename test/{integration => production}/create-next-app/biome-config.test.ts (94%) rename test/{integration => production}/create-next-app/eslint-config.test.ts (90%) rename test/{integration => production}/create-next-app/examples.test.ts (96%) rename test/{integration => production}/create-next-app/index.test.ts (96%) rename test/{integration => production}/create-next-app/lib/specification.ts (100%) create mode 100644 test/production/create-next-app/lib/test-pkg-paths.ts rename test/{integration => production}/create-next-app/lib/types.ts (100%) rename test/{integration => production}/create-next-app/lib/utils.ts (92%) rename test/{integration => production}/create-next-app/package-manager/bun.test.ts (91%) rename test/{integration => production}/create-next-app/package-manager/npm.test.ts (90%) rename test/{integration => production}/create-next-app/package-manager/pnpm.test.ts (94%) rename test/{integration => production}/create-next-app/package-manager/yarn.test.ts (91%) rename test/{integration => production}/create-next-app/prompts.test.ts (91%) rename test/{integration => production}/create-next-app/templates/app-api.test.ts (92%) rename test/{integration => production}/create-next-app/templates/app.test.ts (95%) create mode 100644 test/production/create-next-app/templates/matrix.test.ts rename test/{integration => production}/create-next-app/templates/pages.test.ts (95%) create mode 100644 test/production/create-next-app/utils.ts rename test/{integration => production}/critical-css/components/hello.js (100%) rename test/{integration => production}/critical-css/components/hello.module.css (100%) create mode 100644 test/production/critical-css/critical-css.test.ts create mode 100644 test/production/critical-css/next.config.js rename test/{integration => production}/critical-css/pages/_app.js (100%) rename test/{integration => production}/critical-css/pages/another.js (100%) rename test/{integration => production}/critical-css/pages/index.js (100%) rename test/{integration => production}/critical-css/styles/index.module.css (100%) rename test/{integration => production}/critical-css/styles/styles.css (100%) create mode 100644 test/production/css-customization/css-customization.test.ts rename test/{integration => production/css-customization}/css-fixtures/bad-custom-configuration-arr-1/.postcssrc.json (100%) rename test/{integration/css-fixtures/bad-custom-configuration-arr-3 => production/css-customization/css-fixtures/bad-custom-configuration-arr-1}/pages/_app.js (100%) rename test/{integration/css-fixtures/bad-custom-configuration-arr-2 => production/css-customization/css-fixtures/bad-custom-configuration-arr-1}/pages/index.js (100%) rename test/{integration => production/css-customization}/css-fixtures/bad-custom-configuration-arr-1/styles/global.css (100%) rename test/{integration => production/css-customization}/css-fixtures/bad-custom-configuration-arr-2/.postcssrc.json (100%) rename test/{integration/css-fixtures/bad-custom-configuration-arr-4 => production/css-customization/css-fixtures/bad-custom-configuration-arr-2}/pages/_app.js (100%) rename test/{integration/css-fixtures/bad-custom-configuration-arr-3 => production/css-customization/css-fixtures/bad-custom-configuration-arr-2}/pages/index.js (100%) rename test/{integration => production/css-customization}/css-fixtures/bad-custom-configuration-arr-2/styles/global.css (100%) rename test/{integration => production/css-customization}/css-fixtures/bad-custom-configuration-arr-3/.postcssrc.json (100%) rename test/{integration/css-fixtures/bad-custom-configuration-arr-5 => production/css-customization/css-fixtures/bad-custom-configuration-arr-3}/pages/_app.js (100%) rename test/{integration/css-fixtures/bad-custom-configuration-arr-4 => production/css-customization/css-fixtures/bad-custom-configuration-arr-3}/pages/index.js (100%) rename test/{integration => production/css-customization}/css-fixtures/bad-custom-configuration-arr-3/styles/global.css (100%) rename test/{integration => production/css-customization}/css-fixtures/bad-custom-configuration-arr-4/.postcssrc.json (100%) rename test/{integration/css-fixtures/bad-custom-configuration-arr-6 => production/css-customization/css-fixtures/bad-custom-configuration-arr-4}/pages/_app.js (100%) rename test/{integration/css-fixtures/bad-custom-configuration-arr-5 => production/css-customization/css-fixtures/bad-custom-configuration-arr-4}/pages/index.js (100%) rename test/{integration => production/css-customization}/css-fixtures/bad-custom-configuration-arr-4/styles/global.css (100%) rename test/{integration => production/css-customization}/css-fixtures/bad-custom-configuration-arr-5/.postcssrc.json (100%) rename test/{integration/css-fixtures/bad-custom-configuration-arr-7 => production/css-customization/css-fixtures/bad-custom-configuration-arr-5}/pages/_app.js (100%) rename test/{integration/css-fixtures/bad-custom-configuration-arr-6 => production/css-customization/css-fixtures/bad-custom-configuration-arr-5}/pages/index.js (100%) rename test/{integration => production/css-customization}/css-fixtures/bad-custom-configuration-arr-5/styles/global.css (100%) rename test/{integration => production/css-customization}/css-fixtures/bad-custom-configuration-arr-6/.postcssrc.json (100%) rename test/{integration/css-fixtures/bad-custom-configuration-arr-8 => production/css-customization/css-fixtures/bad-custom-configuration-arr-6}/pages/_app.js (100%) rename test/{integration/css-fixtures/bad-custom-configuration-arr-7 => production/css-customization/css-fixtures/bad-custom-configuration-arr-6}/pages/index.js (100%) rename test/{integration => production/css-customization}/css-fixtures/bad-custom-configuration-arr-6/styles/global.css (100%) rename test/{integration => production/css-customization}/css-fixtures/bad-custom-configuration-arr-7/.postcssrc.json (100%) rename test/{integration/css-fixtures/bad-custom-configuration-func => production/css-customization/css-fixtures/bad-custom-configuration-arr-7}/pages/_app.js (100%) rename test/{integration/css-fixtures/bad-custom-configuration-arr-8 => production/css-customization/css-fixtures/bad-custom-configuration-arr-7}/pages/index.js (100%) rename test/{integration => production/css-customization}/css-fixtures/bad-custom-configuration-arr-7/styles/global.css (100%) rename test/{integration/css-fixtures/bad-custom-configuration => production/css-customization/css-fixtures/bad-custom-configuration-arr-8}/pages/_app.js (100%) rename test/{integration/css-fixtures/bad-custom-configuration-func => production/css-customization/css-fixtures/bad-custom-configuration-arr-8}/pages/index.js (100%) rename test/{integration => production/css-customization}/css-fixtures/bad-custom-configuration-arr-8/postcss.config.js (100%) rename test/{integration => production/css-customization}/css-fixtures/bad-custom-configuration-arr-8/styles/global.css (100%) rename test/{integration/css-fixtures/compilation-and-prefixing => production/css-customization/css-fixtures/bad-custom-configuration-func}/pages/_app.js (100%) rename test/{integration/css-fixtures/bad-custom-configuration => production/css-customization/css-fixtures/bad-custom-configuration-func}/pages/index.js (100%) rename test/{integration => production/css-customization}/css-fixtures/bad-custom-configuration-func/postcss.config.js (100%) rename test/{integration => production/css-customization}/css-fixtures/bad-custom-configuration-func/styles/global.css (100%) rename test/{integration => production/css-customization}/css-fixtures/bad-custom-configuration/.postcssrc.json (100%) create mode 100644 test/production/css-customization/css-fixtures/bad-custom-configuration/next.config.js rename test/{integration/css-fixtures/custom-configuration-arr => production/css-customization/css-fixtures/bad-custom-configuration}/pages/_app.js (100%) rename test/{integration/css-fixtures/custom-configuration-arr => production/css-customization/css-fixtures/bad-custom-configuration}/pages/index.js (100%) rename test/{integration => production/css-customization}/css-fixtures/bad-custom-configuration/styles/global.css (100%) rename test/{integration => production/css-customization}/css-fixtures/custom-configuration-arr/.postcssrc.json (100%) create mode 100644 test/production/css-customization/css-fixtures/custom-configuration-arr/next.config.js rename test/{integration/css-fixtures/custom-configuration => production/css-customization/css-fixtures/custom-configuration-arr}/pages/_app.js (100%) rename test/{integration/css-fixtures/custom-configuration => production/css-customization/css-fixtures/custom-configuration-arr}/pages/index.js (100%) rename test/{integration => production/css-customization}/css-fixtures/custom-configuration-arr/styles/global.css (100%) rename test/{integration => production/css-customization}/css-fixtures/custom-configuration-loader/next.config.js (100%) rename test/{integration => production/css-customization}/css-fixtures/custom-configuration-loader/pages/index.js (100%) rename test/{integration => production/css-customization}/css-fixtures/custom-configuration-loader/styles/index.css (100%) rename test/{integration => production/css-customization}/css-fixtures/custom-configuration/.postcssrc.json (100%) create mode 100644 test/production/css-customization/css-fixtures/custom-configuration/next.config.js rename test/{integration/css-fixtures/global-and-module-ordering => production/css-customization/css-fixtures/custom-configuration}/pages/_app.js (100%) rename test/{integration/css-fixtures/unused => production/css-customization/css-fixtures/custom-configuration}/pages/index.js (100%) rename test/{integration => production/css-customization}/css-fixtures/custom-configuration/styles/global.css (100%) create mode 100644 test/production/css-features/basic-global-support.test.ts create mode 100644 test/production/css-features/browserslist.test.ts create mode 100644 test/production/css-features/css-compilation.test.ts create mode 100644 test/production/css-features/css-features.test.ts create mode 100644 test/production/css-features/css-modules-ordering.test.ts create mode 100644 test/production/css-features/css-modules-support.test.ts create mode 100644 test/production/css-features/css-modules.test.ts create mode 100644 test/production/css-features/css-rendering.test.ts rename test/{integration/css-fixtures => production/css-features/fixtures}/3rd-party-module/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/3rd-party-module/pages/index.module.css (100%) rename test/{integration/css-fixtures/prod-module => production/css-features/fixtures/basic-module}/pages/index.js (100%) rename test/{integration/css-fixtures/prod-module => production/css-features/fixtures/basic-module}/pages/index.module.css (100%) create mode 100644 test/production/css-features/fixtures/browsers-new/.browserslistrc rename test/{integration => production}/css-features/fixtures/browsers-new/package.json (100%) rename test/{integration => production}/css-features/fixtures/browsers-new/pages/_app.js (100%) rename test/{integration => production}/css-features/fixtures/browsers-new/pages/index.js (100%) rename test/{integration => production}/css-features/fixtures/browsers-new/pages/styles.css (100%) create mode 100644 test/production/css-features/fixtures/browsers-old/.browserslistrc rename test/{integration => production}/css-features/fixtures/browsers-old/package.json (100%) rename test/{integration => production}/css-features/fixtures/browsers-old/pages/_app.js (100%) rename test/{integration => production}/css-features/fixtures/browsers-old/pages/index.js (100%) rename test/{integration => production}/css-features/fixtures/browsers-old/pages/styles.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/catch-all-module/pages/[...post]/55css.module.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/catch-all-module/pages/[...post]/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/catch-all-module/pages/[...post]/index.module.css (100%) rename test/{integration/css-fixtures/npm-import-bad => production/css-features/fixtures/compilation-and-prefixing}/pages/_app.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/compilation-and-prefixing/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/compilation-and-prefixing/styles/global.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/composes-basic/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/composes-basic/pages/index.module.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/composes-external/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/composes-external/pages/index.module.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/composes-external/pages/other.css (100%) rename test/{integration => production}/css-features/fixtures/cp-el-modules/pages/index.js (100%) rename test/{integration => production}/css-features/fixtures/cp-el-modules/pages/styles.module.css (100%) rename test/{integration => production}/css-features/fixtures/cp-global-modules/pages/index.js (100%) rename test/{integration => production}/css-features/fixtures/cp-global-modules/pages/styles.module.css (100%) rename test/{integration => production}/css-features/fixtures/cp-ie-11/package.json (100%) rename test/{integration => production}/css-features/fixtures/cp-ie-11/pages/_app.js (100%) rename test/{integration => production}/css-features/fixtures/cp-ie-11/pages/index.js (100%) rename test/{integration => production}/css-features/fixtures/cp-ie-11/pages/styles.css (100%) rename test/{integration => production}/css-features/fixtures/cp-modern/package.json (100%) rename test/{integration => production}/css-features/fixtures/cp-modern/pages/_app.js (100%) rename test/{integration => production}/css-features/fixtures/cp-modern/pages/index.js (100%) rename test/{integration => production}/css-features/fixtures/cp-modern/pages/styles.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/csp-style-src-nonce/next.config.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/csp-style-src-nonce/pages/_document.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/csp-style-src-nonce/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/csp-style-src-nonce/pages/index.module.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/csp-style-src-nonce/pages/other.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/csp-style-src-nonce/pages/other.module.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/cssmodules-pure-no-check/pages/index.module.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/cssmodules-pure-no-check/pages/index.tsx (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/data-url/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/data-url/pages/index.module.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/dynamic-route-module/pages/[post]/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/dynamic-route-module/pages/[post]/index.module.css (100%) rename test/{integration/css-fixtures/next-issue-12343 => production/css-features/fixtures/hydrate-without-deps}/.gitignore (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/hydrate-without-deps/pages/client.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/hydrate-without-deps/pages/common.module.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/hydrate-without-deps/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/hydrate-without-deps/pages/index.module.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/import-global-from-module/node_modules/example/index.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/import-global-from-module/node_modules/example/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/import-global-from-module/node_modules/example/index.mjs (100%) rename test/{e2e/app-dir/scss/invalid-module => production/css-features/fixtures/import-global-from-module}/node_modules/example/package.json (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/import-global-from-module/pages/index.js (100%) rename test/{integration => production}/css-features/fixtures/inline-comments/package.json (100%) rename test/{integration/css-fixtures/multi-module => production/css-features/fixtures/inline-comments}/pages/_app.js (100%) rename test/{integration => production}/css-features/fixtures/inline-comments/pages/global.css (100%) rename test/{integration => production}/css-features/fixtures/inline-comments/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/invalid-global-with-app/pages/_app.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/invalid-global-with-app/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/invalid-global-with-app/styles/global.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/invalid-global/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/invalid-global/styles/global.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/invalid-module-document/pages/_document.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/invalid-module-document/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/invalid-module-document/styles.module.css (100%) rename test/{integration => production}/css-features/fixtures/module-import-exports/package.json (100%) rename test/{integration => production}/css-features/fixtures/module-import-exports/pages/colors.module.css (100%) rename test/{integration => production}/css-features/fixtures/module-import-exports/pages/index.js (100%) rename test/{integration => production}/css-features/fixtures/module-import-exports/pages/styles.module.css (100%) rename test/{integration => production}/css-features/fixtures/module-import-global-invalid/package.json (100%) rename test/{integration => production}/css-features/fixtures/module-import-global-invalid/pages/index.js (100%) rename test/{integration => production}/css-features/fixtures/module-import-global-invalid/pages/styles.css (100%) rename test/{integration => production}/css-features/fixtures/module-import-global-invalid/pages/styles.module.css (100%) rename test/{integration => production}/css-features/fixtures/module-import-global/package.json (100%) rename test/{integration => production}/css-features/fixtures/module-import-global/pages/index.js (100%) rename test/{integration => production}/css-features/fixtures/module-import-global/pages/styles.css (100%) rename test/{integration => production}/css-features/fixtures/module-import-global/pages/styles.module.css (100%) rename test/{integration/css-fixtures/next-issue-15468 => production/css-features/fixtures/multi-global-reversed}/.gitignore (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/multi-global-reversed/pages/_app.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/multi-global-reversed/pages/index.js (100%) rename test/{integration/css-fixtures/multi-page => production/css-features/fixtures/multi-global-reversed}/styles/global1.css (100%) rename test/{integration/css-fixtures/multi-page => production/css-features/fixtures/multi-global-reversed}/styles/global2.css (100%) rename test/{integration/css-fixtures/npm-import-bad => production/css-features/fixtures/multi-global}/.gitignore (100%) rename test/{integration/css-fixtures/nested-global => production/css-features/fixtures/multi-global}/pages/_app.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/multi-global/pages/index.js (100%) rename test/{integration/css-fixtures/single-global-special-characters/a+b/styles/global.css => production/css-features/fixtures/multi-global/styles/global1.css} (100%) create mode 100644 test/production/css-features/fixtures/multi-global/styles/global2.css rename test/{integration/css-fixtures/npm-import-nested => production/css-features/fixtures/multi-page}/.gitignore (100%) rename test/{integration/css-fixtures/url-global-asset-prefix-1 => production/css-features/fixtures/multi-page}/pages/_app.js (100%) create mode 100644 test/production/css-features/fixtures/multi-page/pages/page1.js create mode 100644 test/production/css-features/fixtures/multi-page/pages/page2.js rename test/{integration/css-fixtures/single-global-src/styles/global.css => production/css-features/fixtures/multi-page/styles/global1.css} (100%) create mode 100644 test/production/css-features/fixtures/multi-page/styles/global2.css rename test/{integration/css-fixtures/npm-import => production/css-features/fixtures/nested-global}/.gitignore (100%) rename test/{integration/css-fixtures/url-global-asset-prefix-2 => production/css-features/fixtures/nested-global}/pages/_app.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/nested-global/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/nested-global/styles/global1.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/nested-global/styles/global1b.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/nested-global/styles/global2.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/nested-global/styles/global2b.css (100%) rename test/{integration/css-fixtures/single-global-special-characters/a+b => production/css-features/fixtures/next-issue-15468}/.gitignore (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/next-issue-15468/pages/_app.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/next-issue-15468/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/next-issue-15468/styles/global.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/nm-module-nested/node_modules/example/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/nm-module-nested/node_modules/example/index.mjs (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/nm-module-nested/node_modules/example/index.module.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/nm-module-nested/node_modules/example/other.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/nm-module-nested/node_modules/example/other2.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/nm-module-nested/node_modules/example/other3.css (100%) rename test/{integration/css-fixtures/import-global-from-module => production/css-features/fixtures/nm-module-nested}/node_modules/example/package.json (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/nm-module-nested/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/nm-module/node_modules/example/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/nm-module/node_modules/example/index.mjs (100%) create mode 100644 test/production/css-features/fixtures/nm-module/node_modules/example/index.module.css rename test/{integration/css-fixtures/invalid-global-module => production/css-features/fixtures/nm-module}/node_modules/example/package.json (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/nm-module/pages/index.js (100%) rename test/{integration/css-fixtures/single-global-src => production/css-features/fixtures/npm-import-bad}/.gitignore (100%) rename test/{integration/css-fixtures/npm-import-nested => production/css-features/fixtures/npm-import-bad}/pages/_app.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/npm-import-bad/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/npm-import-bad/styles/global.css (100%) rename test/{integration/css-fixtures/single-global => production/css-features/fixtures/npm-import-nested}/.gitignore (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/npm-import-nested/node_modules/example/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/npm-import-nested/node_modules/example/index.mjs (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/npm-import-nested/node_modules/example/other.css (100%) rename test/{integration/css-fixtures/invalid-module => production/css-features/fixtures/npm-import-nested}/node_modules/example/package.json (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/npm-import-nested/node_modules/example/test.css (100%) rename test/{integration/css-fixtures/npm-import => production/css-features/fixtures/npm-import-nested}/pages/_app.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/npm-import-nested/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/npm-import-nested/styles/global.css (100%) rename test/{integration/css-fixtures/transition-cleanup => production/css-features/fixtures/npm-import}/.gitignore (100%) rename test/{integration/css-fixtures/single-global-special-characters/a+b => production/css-features/fixtures/npm-import}/pages/_app.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/npm-import/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/npm-import/styles/global.css (100%) create mode 100644 test/production/css-features/fixtures/prod-module/pages/index.js create mode 100644 test/production/css-features/fixtures/prod-module/pages/index.module.css rename test/{integration/css-fixtures/transition-react => production/css-features/fixtures/single-global-special-characters/a+b}/.gitignore (100%) rename test/{integration/css-fixtures/single-global => production/css-features/fixtures/single-global-special-characters/a+b}/pages/_app.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/single-global-special-characters/a+b/pages/index.js (100%) rename test/{integration/css-fixtures/single-global => production/css-features/fixtures/single-global-special-characters/a+b}/styles/global.css (100%) rename test/{integration/css-fixtures/transition-reload => production/css-features/fixtures/single-global-src}/.gitignore (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/single-global-src/src/pages/_app.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/single-global-src/src/pages/index.js (100%) rename test/{integration/css-fixtures/valid-and-invalid-global => production/css-features/fixtures/single-global-src}/styles/global.css (100%) rename test/{integration/css-fixtures/url-global => production/css-features/fixtures/single-global}/.gitignore (100%) rename test/{integration/css-fixtures/valid-and-invalid-global => production/css-features/fixtures/single-global}/pages/_app.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/single-global/pages/index.js (100%) create mode 100644 test/production/css-features/fixtures/single-global/styles/global.css create mode 100644 test/production/css-features/fixtures/transition-cleanup/.gitignore rename test/{integration/css-fixtures => production/css-features/fixtures}/transition-cleanup/pages/common.module.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/transition-cleanup/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/transition-cleanup/pages/index.module.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/transition-cleanup/pages/other.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/transition-cleanup/pages/other.module.css (100%) create mode 100644 test/production/css-features/fixtures/transition-react/.gitignore create mode 100644 test/production/css-features/fixtures/transition-react/pages/index.js create mode 100644 test/production/css-features/fixtures/transition-react/pages/other.js create mode 100644 test/production/css-features/fixtures/transition-react/pages/other.module.css create mode 100644 test/production/css-features/fixtures/transition-reload/.gitignore rename test/{integration/css-fixtures => production/css-features/fixtures}/transition-reload/pages/common.module.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/transition-reload/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/transition-reload/pages/index.module.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/transition-reload/pages/other.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/transition-reload/pages/other.module.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/unresolved-css-url/global.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/unresolved-css-url/global.scss (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/unresolved-css-url/pages/_app.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/unresolved-css-url/pages/another.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/unresolved-css-url/pages/another.module.scss (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/unresolved-css-url/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/unresolved-css-url/pages/index.module.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/unresolved-css-url/public/vercel.svg (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global-asset-prefix-1/assets/light.svg (100%) create mode 100644 test/production/css-features/fixtures/url-global-asset-prefix-1/next.config.js rename test/{integration/css-fixtures/url-global => production/css-features/fixtures/url-global-asset-prefix-1}/pages/_app.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global-asset-prefix-1/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global-asset-prefix-1/styles/dark.svg (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global-asset-prefix-1/styles/dark2.svg (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global-asset-prefix-1/styles/global1.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global-asset-prefix-1/styles/global2.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global-asset-prefix-1/styles/global2b.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global-asset-prefix-2/assets/light.svg (100%) create mode 100644 test/production/css-features/fixtures/url-global-asset-prefix-2/next.config.js create mode 100644 test/production/css-features/fixtures/url-global-asset-prefix-2/pages/_app.js rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global-asset-prefix-2/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global-asset-prefix-2/styles/dark.svg (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global-asset-prefix-2/styles/dark2.svg (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global-asset-prefix-2/styles/global1.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global-asset-prefix-2/styles/global2.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global-asset-prefix-2/styles/global2b.css (100%) create mode 100644 test/production/css-features/fixtures/url-global/.gitignore rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global/assets/light.svg (100%) create mode 100644 test/production/css-features/fixtures/url-global/pages/_app.js rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global/styles/dark.svg (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global/styles/dark2.svg (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global/styles/global1.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global/styles/global2.css (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/url-global/styles/global2b.css (100%) rename test/{integration/css-fixtures/with-styled-jsx => production/css-features/fixtures/valid-and-invalid-global}/pages/_app.js (100%) rename test/{integration/css-fixtures => production/css-features/fixtures}/valid-and-invalid-global/pages/index.js (100%) create mode 100644 test/production/css-features/fixtures/valid-and-invalid-global/styles/global.css create mode 100644 test/production/css-features/valid-invalid-css.test.ts create mode 100644 test/production/css-minify/css-minify.test.ts rename test/{integration => production}/css-minify/pages/_app.js (100%) rename test/{integration => production}/css-minify/pages/index.js (100%) rename test/{integration => production}/css-minify/styles/global.css (100%) create mode 100644 test/production/css-modules/css-modules.test.ts create mode 100644 test/production/css-modules/fixtures/3rd-party-module/pages/index.js create mode 100644 test/production/css-modules/fixtures/3rd-party-module/pages/index.module.css create mode 100644 test/production/css-modules/fixtures/basic-module/pages/index.js create mode 100644 test/production/css-modules/fixtures/basic-module/pages/index.module.css create mode 100644 test/production/css-modules/fixtures/catch-all-module/pages/[...post]/55css.module.css create mode 100644 test/production/css-modules/fixtures/catch-all-module/pages/[...post]/index.js create mode 100644 test/production/css-modules/fixtures/catch-all-module/pages/[...post]/index.module.css create mode 100644 test/production/css-modules/fixtures/composes-basic/pages/index.js create mode 100644 test/production/css-modules/fixtures/composes-basic/pages/index.module.css create mode 100644 test/production/css-modules/fixtures/composes-external/pages/index.js create mode 100644 test/production/css-modules/fixtures/composes-external/pages/index.module.css create mode 100644 test/production/css-modules/fixtures/composes-external/pages/other.css create mode 100644 test/production/css-modules/fixtures/cssmodules-pure-no-check/pages/index.module.css create mode 100644 test/production/css-modules/fixtures/cssmodules-pure-no-check/pages/index.tsx create mode 100644 test/production/css-modules/fixtures/dynamic-route-module/pages/[post]/index.js create mode 100644 test/production/css-modules/fixtures/dynamic-route-module/pages/[post]/index.module.css rename test/{integration/css-fixtures => production/css-modules/fixtures}/invalid-global-module/node_modules/example/index.css (100%) rename test/{integration/css-fixtures => production/css-modules/fixtures}/invalid-global-module/node_modules/example/index.js (100%) rename test/{integration/css-fixtures => production/css-modules/fixtures}/invalid-global-module/node_modules/example/index.mjs (100%) rename test/{integration/css-fixtures/nm-module-nested => production/css-modules/fixtures/invalid-global-module}/node_modules/example/package.json (100%) rename test/{integration/css-fixtures => production/css-modules/fixtures}/invalid-global-module/pages/index.js (100%) rename test/{integration/css-fixtures => production/css-modules/fixtures}/invalid-module/node_modules/example/index.js (100%) rename test/{integration/css-fixtures => production/css-modules/fixtures}/invalid-module/node_modules/example/index.mjs (100%) rename test/{integration/css-fixtures => production/css-modules/fixtures}/invalid-module/node_modules/example/index.module.css (100%) rename test/{integration/css-fixtures/nm-module => production/css-modules/fixtures/invalid-module}/node_modules/example/package.json (100%) rename test/{e2e/app-dir/scss => production/css-modules/fixtures}/invalid-module/pages/index.js (100%) create mode 100644 test/production/css-modules/fixtures/nm-module-nested/node_modules/example/index.js create mode 100644 test/production/css-modules/fixtures/nm-module-nested/node_modules/example/index.mjs create mode 100644 test/production/css-modules/fixtures/nm-module-nested/node_modules/example/index.module.css create mode 100644 test/production/css-modules/fixtures/nm-module-nested/node_modules/example/other.css create mode 100644 test/production/css-modules/fixtures/nm-module-nested/node_modules/example/other2.css create mode 100644 test/production/css-modules/fixtures/nm-module-nested/node_modules/example/other3.css rename test/{integration/css-fixtures/npm-import-nested => production/css-modules/fixtures/nm-module-nested}/node_modules/example/package.json (100%) create mode 100644 test/production/css-modules/fixtures/nm-module-nested/pages/index.js create mode 100644 test/production/css-modules/fixtures/nm-module/node_modules/example/index.js create mode 100644 test/production/css-modules/fixtures/nm-module/node_modules/example/index.mjs create mode 100644 test/production/css-modules/fixtures/nm-module/node_modules/example/index.module.css create mode 100644 test/production/css-modules/fixtures/nm-module/node_modules/example/package.json create mode 100644 test/production/css-modules/fixtures/nm-module/pages/index.js create mode 100644 test/production/css-modules/fixtures/prod-module/pages/index.js create mode 100644 test/production/css-modules/fixtures/prod-module/pages/index.module.css rename test/{integration => production}/custom-server-types/.gitignore (100%) create mode 100644 test/production/custom-server-types/custom-server-types.test.ts rename test/{integration => production}/custom-server-types/pages/index.tsx (100%) rename test/{integration => production}/custom-server-types/server.ts (100%) rename test/{integration/import-assertion/tsconfig.json => production/custom-server-types/tsconfig.test.json} (83%) rename test/{integration => production}/dedupes-scripts/components/hello.js (100%) create mode 100644 test/production/dedupes-scripts/dedupes-scripts.test.ts rename test/{integration => production}/dedupes-scripts/pages/index.js (100%) rename test/{integration => production}/document-file-dependencies/css/404.module.css (100%) rename test/{integration => production}/document-file-dependencies/css/error.module.css (100%) rename test/{integration => production}/document-file-dependencies/css/global.css (100%) rename test/{integration => production}/document-file-dependencies/css/index.module.css (100%) create mode 100644 test/production/document-file-dependencies/document-file-dependencies.test.ts rename test/{integration => production}/document-file-dependencies/pages/404.js (100%) rename test/{integration => production}/document-file-dependencies/pages/_app.js (100%) rename test/{integration => production}/document-file-dependencies/pages/_error.js (100%) rename test/{integration => production}/document-file-dependencies/pages/error-trigger.js (100%) rename test/{integration => production}/document-file-dependencies/pages/index.js (100%) create mode 100644 test/production/error-in-error/error-in-error.test.ts rename test/{integration => production}/error-in-error/pages/_error.js (100%) rename test/{integration => production}/error-in-error/pages/index.js (100%) create mode 100644 test/production/error-load-fail/error-load-fail.test.ts rename test/{integration => production}/error-load-fail/pages/broken.js (100%) rename test/{integration => production}/error-load-fail/pages/index.js (100%) create mode 100644 test/production/error-plugin-stack-overflow/error-plugin-stack-overflow.test.ts rename test/{integration => production}/error-plugin-stack-overflow/next.config.js (100%) rename test/{integration => production}/error-plugin-stack-overflow/pages/index.js (100%) create mode 100644 test/production/errors-on-output-to-public/errors-on-output-to-public.test.ts rename test/{integration/invalid-custom-routes => production/errors-on-output-to-public}/pages/index.js (100%) create mode 100644 test/production/errors-on-output-to-static/errors-on-output-to-static.test.ts rename test/{integration => production}/errors-on-output-to-static/next.config.js (100%) rename test/{integration/invalid-middleware-matchers => production/errors-on-output-to-static}/pages/index.js (100%) create mode 100644 test/production/export-404/export-404.test.ts rename test/{integration => production}/export-404/next.config.js (100%) rename test/{integration => production}/export-404/pages/404.js (100%) create mode 100644 test/production/export-dynamic-pages/export-dynamic-pages.test.ts rename test/{integration => production}/export-dynamic-pages/next.config.js (100%) rename test/{integration => production}/export-dynamic-pages/pages/regression/[slug].js (100%) create mode 100644 test/production/export-fallback-true-error/export-fallback-true-error.test.ts rename test/{integration => production}/export-fallback-true-error/next.config.js (100%) rename test/{integration => production}/export-fallback-true-error/pages/[slug].js (100%) create mode 100644 test/production/export-getInitialProps-warn/export-getInitialProps-warn.test.ts rename test/{integration => production}/export-getInitialProps-warn/next.config.js (100%) rename test/{integration => production}/export-getInitialProps-warn/pages/index.js (100%) create mode 100644 test/production/export-image-default/export-image-default.test.ts rename test/{integration/export-index-not-found-gsp => production/export-image-default}/next.config.js (100%) rename test/{integration => production}/export-image-default/pages/index.js (100%) create mode 100644 test/production/export-image-loader-legacy/export-image-loader-legacy.test.ts rename test/{integration => production}/export-image-loader-legacy/next.config.js (100%) rename test/{integration => production}/export-image-loader-legacy/pages/index.js (100%) rename test/{integration/next-image-new/loader-config => production/export-image-loader}/dummy-loader.js (100%) create mode 100644 test/production/export-image-loader/export-image-loader.test.ts rename test/{integration => production}/export-image-loader/next.config.js (100%) rename test/{integration => production}/export-image-loader/pages/index.js (100%) create mode 100644 test/production/export-index-not-found-gsp/export-index-not-found-gsp.test.ts rename test/{integration/export-intent/fixtures/bad-export => production/export-index-not-found-gsp}/next.config.js (100%) rename test/{integration => production}/export-index-not-found-gsp/pages/index.js (100%) create mode 100644 test/production/export-intent/export-intent.test.ts rename test/{integration => production}/export-intent/fixtures/bad-export/.gitignore (100%) rename test/{integration/export-intent/fixtures/default-export => production/export-intent/fixtures/bad-export}/next.config.js (100%) rename test/{integration => production}/export-intent/fixtures/bad-export/pages/index.js (100%) rename test/{integration => production}/export-intent/fixtures/custom-export/.gitignore (100%) rename test/{integration => production}/export-intent/fixtures/custom-export/next.config.js (100%) rename test/{integration => production}/export-intent/fixtures/custom-export/pages/index.js (100%) rename test/{integration => production}/export-intent/fixtures/custom-out/.gitignore (100%) rename test/{integration => production}/export-intent/fixtures/custom-out/next.config.js (100%) rename test/{integration => production}/export-intent/fixtures/custom-out/pages/index.js (100%) rename test/{integration => production}/export-intent/fixtures/default-export/.gitignore (100%) rename test/{integration/export-subfolders => production/export-intent/fixtures/default-export}/next.config.js (100%) rename test/{integration => production}/export-intent/fixtures/default-export/pages/index.js (100%) rename test/{integration => production}/export-intent/fixtures/no-export/.gitignore (100%) rename test/{integration => production}/export-intent/fixtures/no-export/pages/index.js (100%) create mode 100644 test/production/export-subfolders/export-subfolders.test.ts rename test/{integration/getserversideprops-export-error => production/export-subfolders}/next.config.js (100%) rename test/{integration => production}/export-subfolders/pages/about.js (100%) rename test/{integration => production}/export-subfolders/pages/index.js (100%) rename test/{integration => production}/export-subfolders/pages/posts/index.js (100%) rename test/{integration => production}/export-subfolders/pages/posts/single.js (100%) create mode 100644 test/production/externals-esm-loose/externals-esm-loose.test.ts rename test/{integration => production}/externals-esm-loose/next.config.js (100%) rename test/{integration/externals-esm-loose/node_modules/esm-package2/correct.js => production/externals-esm-loose/node_modules/esm-package1/correct.mjs} (100%) rename test/{integration => production}/externals-esm-loose/node_modules/esm-package1/package.json (100%) rename test/{integration => production}/externals-esm-loose/node_modules/esm-package1/wrong.js (100%) create mode 100644 test/production/externals-esm-loose/node_modules/esm-package2/correct.js rename test/{integration => production}/externals-esm-loose/node_modules/esm-package2/package.json (100%) rename test/{integration => production}/externals-esm-loose/node_modules/esm-package2/wrong.cjs (100%) rename test/{integration => production}/externals-esm-loose/node_modules/esm-package3/correct.cjs (100%) rename test/{integration => production}/externals-esm-loose/node_modules/esm-package3/package.json (100%) rename test/{integration => production}/externals-esm-loose/node_modules/esm-package3/wrong.js (100%) rename test/{integration => production}/externals-esm-loose/node_modules/preact/compat.js (100%) rename test/{integration => production}/externals-esm-loose/pages/ssg.js (100%) rename test/{integration => production}/externals-esm-loose/pages/ssr.js (100%) rename test/{integration => production}/externals-esm-loose/pages/static.js (100%) create mode 100644 test/production/fallback-modules/fallback-modules.test.ts rename test/{integration/fallback-modules/fixtures/with-crypto => production/fallback-modules}/pages/index.js (100%) create mode 100644 test/production/firebase-grpc/firebase-grpc.test.ts rename test/{integration => production}/firebase-grpc/pages/page-1.js (100%) rename test/{integration => production}/firebase-grpc/pages/page-2.js (100%) create mode 100644 test/production/future/future.test.ts rename test/{integration/port-env-var => production/future}/next.config.js (100%) rename test/{integration => production}/future/pages/index.js (100%) create mode 100644 test/production/getserversideprops-export-error/getserversideprops-export-error.test.ts rename test/{integration/next-image-new/export-config => production/getserversideprops-export-error}/next.config.js (100%) rename test/{integration => production}/getserversideprops-export-error/pages/index.js (100%) create mode 100644 test/production/gsp-build-errors/gsp-build-errors.test.ts rename test/{integration/production-build-dir/build => production/gsp-build-errors}/next.config.js (100%) create mode 100644 test/production/gsp-extension/gsp-extension.test.ts rename test/{integration => production}/gsp-extension/pages/[slug].js (100%) create mode 100644 test/production/handles-export-errors/handles-export-errors.test.ts rename test/{integration => production}/handles-export-errors/next.config.mjs (100%) rename test/{integration => production}/handles-export-errors/pages/blog/[slug].js (100%) rename test/{integration => production}/handles-export-errors/pages/custom-error.js (100%) rename test/{integration => production}/handles-export-errors/pages/page-1.js (100%) rename test/{integration => production}/handles-export-errors/pages/page-10.js (100%) rename test/{integration => production}/handles-export-errors/pages/page-11.js (100%) rename test/{integration => production}/handles-export-errors/pages/page-12.js (100%) rename test/{integration => production}/handles-export-errors/pages/page-13.js (100%) rename test/{integration => production}/handles-export-errors/pages/page-2.js (100%) rename test/{integration => production}/handles-export-errors/pages/page-3.js (100%) rename test/{integration => production}/handles-export-errors/pages/page-4.js (100%) rename test/{integration => production}/handles-export-errors/pages/page-5.js (100%) rename test/{integration => production}/handles-export-errors/pages/page-6.js (100%) rename test/{integration => production}/handles-export-errors/pages/page-7.js (100%) rename test/{integration => production}/handles-export-errors/pages/page-8.js (100%) rename test/{integration => production}/handles-export-errors/pages/page-9.js (100%) rename test/{integration => production}/handles-export-errors/pages/page.js (100%) create mode 100644 test/production/hydrate-then-render/hydrate-then-render.test.ts rename test/{integration => production}/hydrate-then-render/pages/_app.js (92%) rename test/{integration => production}/hydrate-then-render/pages/index.js (100%) rename test/{integration => production}/hydrate-then-render/pages/other.js (100%) create mode 100644 test/production/image-generation/image-generation.test.ts rename test/{integration/image-generation/app => production/image-generation}/pages/api/image.jsx (100%) create mode 100644 test/production/invalid-config-values/invalid-config-values.test.ts rename test/{integration/link-ref-pages => production/invalid-config-values}/pages/index.js (100%) create mode 100644 test/production/invalid-document-image-import/invalid-document-image-import.test.ts rename test/{integration => production}/invalid-document-image-import/next.config.js (100%) rename test/{integration => production}/invalid-document-image-import/pages/_document.js (100%) rename test/{integration => production}/invalid-document-image-import/pages/index.js (100%) rename test/{integration/next-image-new/react-virtualized => production/invalid-document-image-import}/public/test.jpg (100%) create mode 100644 test/production/invalid-page-automatic-static-optimization/invalid-page-automatic-static-optimization.test.ts rename test/{integration => production}/invalid-page-automatic-static-optimization/pages/also-invalid.js (100%) rename test/{integration => production}/invalid-page-automatic-static-optimization/pages/also-valid.js (100%) rename test/{integration => production}/invalid-page-automatic-static-optimization/pages/invalid.js (100%) rename test/{integration => production}/invalid-page-automatic-static-optimization/pages/valid.js (100%) create mode 100644 test/production/jsconfig-empty/jsconfig-empty.test.ts rename test/{integration => production}/jsconfig-empty/jsconfig.json (100%) rename test/{integration/typescript => production/jsconfig-empty}/next.config.js (100%) rename test/{integration => production}/jsconfig-empty/pages/index.js (100%) rename test/{integration => production}/jsconfig/jsconfig.json (100%) create mode 100644 test/production/jsconfig/jsconfig.test.ts rename test/{integration => production}/jsconfig/pages/hello.js (100%) create mode 100644 test/production/json-serialize-original-error/json-serialize-original-error.test.ts rename test/{integration => production}/json-serialize-original-error/pages/bigint.js (100%) create mode 100644 test/production/middleware-build-errors/middleware-build-errors.test.ts rename test/{integration => production}/middleware-build-errors/middleware.js (100%) rename test/{integration/middleware-overrides-node.js-api => production/middleware-build-errors}/pages/index.js (100%) create mode 100644 test/production/middleware-prefetch/middleware-prefetch.test.ts rename test/{integration => production}/middleware-prefetch/middleware.js (100%) rename test/{integration => production}/middleware-prefetch/pages/index.js (100%) rename test/{integration => production}/middleware-prefetch/pages/ssg-page-2.js (100%) rename test/{integration => production}/middleware-prefetch/pages/ssg-page.js (100%) create mode 100644 test/production/mixed-ssg-serverprops-error/mixed-ssg-serverprops-error.test.ts rename test/{integration => production}/mixed-ssg-serverprops-error/pages/index.js (100%) rename test/{integration => production}/mixed-ssg-serverprops-error/pages/index.js.alt (100%) create mode 100644 test/production/next-image-legacy/basic/basic.test.ts rename test/{integration => production}/next-image-legacy/basic/next.config.js (100%) rename test/{integration => production}/next-image-legacy/basic/pages/client-side.js (100%) rename test/{integration => production}/next-image-legacy/basic/pages/errors.js (100%) rename test/{integration => production}/next-image-legacy/basic/pages/index.js (100%) rename test/{integration => production}/next-image-legacy/basic/pages/lazy.js (100%) rename test/{integration => production}/next-image-legacy/basic/pages/loader-prop.js (100%) rename test/{integration => production}/next-image-legacy/basic/public/styles.css (100%) create mode 100644 test/production/next-image-legacy/custom-resolver/custom-resolver.test.ts rename test/{integration => production}/next-image-legacy/custom-resolver/next.config.js (100%) rename test/{integration => production}/next-image-legacy/custom-resolver/pages/client-side.js (100%) rename test/{integration => production}/next-image-legacy/custom-resolver/pages/index.js (100%) rename test/{integration => production}/next-image-legacy/no-intersection-observer-fallback/next.config.js (100%) create mode 100644 test/production/next-image-legacy/no-intersection-observer-fallback/no-intersection-observer-fallback.test.ts rename test/{integration => production}/next-image-legacy/no-intersection-observer-fallback/pages/_document.js (100%) rename test/{integration => production}/next-image-legacy/no-intersection-observer-fallback/pages/index.js (100%) rename test/{integration => production}/next-image-legacy/no-intersection-observer-fallback/pages/no-observer.js (100%) create mode 100644 test/production/next-image-legacy/noscript/noscript.test.ts rename test/{integration => production}/next-image-legacy/noscript/pages/index.js (100%) rename test/{integration => production}/next-image-legacy/react-virtualized/pages/index.js (100%) rename test/{integration/next-image-new/trailing-slash => production/next-image-legacy/react-virtualized}/public/test.jpg (100%) create mode 100644 test/production/next-image-legacy/react-virtualized/react-virtualized.test.ts create mode 100644 test/production/next-image-legacy/react-virtualized/server.js create mode 100644 test/production/next-image-new/invalid-image-import/invalid-image-import.test.ts create mode 100644 test/production/next-image-new/invalid-image-import/pages/index.js create mode 100644 test/production/next-image-new/invalid-image-import/public/invalid.svg rename test/{integration => production}/next-image-new/react-virtualized/pages/index.js (100%) rename test/{integration/next-image-new/unoptimized => production/next-image-new/react-virtualized}/public/test.jpg (100%) create mode 100644 test/production/next-image-new/react-virtualized/react-virtualized.test.ts create mode 100644 test/production/next-image-new/react-virtualized/server.js create mode 100644 test/production/no-op-export/no-op-export.test.ts rename test/{integration => production}/non-next-dist-exclude/app/node_modules/notnext/.gitignore (100%) rename test/{integration => production}/non-next-dist-exclude/app/node_modules/notnext/dist/index.js (100%) rename test/{integration => production}/non-next-dist-exclude/app/node_modules/notnext/package.json (100%) rename test/{integration/typeof-window-replace => production/non-next-dist-exclude}/app/package.json (62%) rename test/{integration => production}/non-next-dist-exclude/app/pages/index.js (100%) create mode 100644 test/production/non-next-dist-exclude/non-next-dist-exclude.test.ts rename test/{integration => production}/not-found-revalidate/data.txt (100%) create mode 100644 test/production/not-found-revalidate/not-found-revalidate.test.ts rename test/{integration => production}/not-found-revalidate/pages/404.js (100%) rename test/{integration => production}/not-found-revalidate/pages/fallback-blocking/[slug].js (100%) rename test/{integration => production}/not-found-revalidate/pages/fallback-true/[slug].js (100%) rename test/{integration => production}/not-found-revalidate/pages/initial-not-found/[slug].js (100%) rename test/{integration => production}/not-found-revalidate/pages/initial-not-found/index.js (100%) create mode 100644 test/production/numeric-sep/numeric-sep.test.ts rename test/{integration => production}/numeric-sep/pages/index.js (100%) rename test/{integration => production}/page-config/config/index.js (100%) rename test/{integration => production}/page-config/lib/data.js (100%) rename test/{integration/worker-webpack5 => production/page-config}/next.config.js (100%) create mode 100644 test/production/page-config/page-config.test.ts rename test/{integration => production}/page-config/pages/blog/index.js (100%) rename test/{integration => production}/page-config/pages/blog/post.js (100%) rename test/{integration => production}/page-config/pages/index.js (100%) rename test/{integration => production}/page-config/pages/invalid/export-from.js (100%) rename test/{integration => production}/page-config/pages/invalid/import-export.js (100%) rename test/{integration => production}/page-config/pages/invalid/no-init.js (100%) rename test/{integration => production}/page-config/pages/invalid/spread-config.js (100%) rename test/{integration => production}/page-config/pages/invalid/string-config.js (100%) rename test/{integration => production}/page-config/pages/valid/config-import.js (100%) rename test/{integration => production}/page-config/pages/valid/not-config-export.js (100%) rename test/{integration => production}/page-config/pages/valid/not-config-import-export.js (100%) rename test/{integration => production}/page-config/something.js (100%) create mode 100644 test/production/page-extensions/page-extensions.test.ts rename test/{integration => production}/page-extensions/pages/index.js (100%) rename test/{integration => production}/page-extensions/pages/invalidExtension.d.ts (100%) rename test/{integration/cli/duplicate-sass/node_modules/node-sass/index.js => production/polyfilling-minimal/next.config.js} (100%) rename test/{integration => production}/polyfilling-minimal/pages/index.js (100%) create mode 100644 test/production/polyfilling-minimal/polyfilling-minimal.test.ts rename test/{integration => production}/polyfills/pages/fetch.js (100%) rename test/{integration => production}/polyfills/pages/index.js (100%) rename test/{integration => production}/polyfills/pages/process.js (100%) create mode 100644 test/production/polyfills/polyfills.test.ts rename test/{integration => production}/preload-viewport/next.config.js (100%) rename test/{integration => production}/preload-viewport/pages/[...rest].js (100%) rename test/{integration => production}/preload-viewport/pages/another.js (100%) rename test/{integration => production}/preload-viewport/pages/bot-user-agent.js (100%) rename test/{integration => production}/preload-viewport/pages/de-duped.js (100%) rename test/{integration => production}/preload-viewport/pages/dynamic/[hello].js (100%) rename test/{integration => production}/preload-viewport/pages/first.js (100%) rename test/{integration => production}/preload-viewport/pages/index.js (100%) rename test/{integration => production}/preload-viewport/pages/invalid-prefetch.js (100%) rename test/{integration => production}/preload-viewport/pages/invalid-ref.js (100%) rename test/{integration => production}/preload-viewport/pages/multi-prefetch.js (100%) rename test/{integration => production}/preload-viewport/pages/not-de-duped.js (100%) rename test/{integration => production}/preload-viewport/pages/opt-out.js (100%) rename test/{integration => production}/preload-viewport/pages/prefetch-disabled-ssg.js (100%) rename test/{integration => production}/preload-viewport/pages/prefetch-disabled.js (100%) rename test/{integration => production}/preload-viewport/pages/rewrite-prefetch.js (100%) rename test/{integration => production}/preload-viewport/pages/ssg/basic.js (100%) rename test/{integration => production}/preload-viewport/pages/ssg/catch-all/[...slug].js (100%) rename test/{integration => production}/preload-viewport/pages/ssg/dynamic-nested/[slug1]/[slug2].js (100%) rename test/{integration => production}/preload-viewport/pages/ssg/dynamic/[slug].js (100%) rename test/{integration => production}/preload-viewport/pages/ssg/fixture/index.js (100%) rename test/{integration => production}/preload-viewport/pages/ssg/fixture/mismatch.js (100%) rename test/{integration => production}/preload-viewport/pages/ssg/slow.js (100%) create mode 100644 test/production/preload-viewport/preload-viewport.test.ts create mode 100644 test/production/preload-viewport/server.js rename test/{integration => production}/prerender-export/next.config.js (100%) rename test/{integration => production}/prerender-export/pages/another/index.js (100%) rename test/{integration => production}/prerender-export/pages/api-docs/[...slug].js (100%) rename test/{integration => production}/prerender-export/pages/api/bad.js (100%) rename test/{integration => production}/prerender-export/pages/blocking-fallback-once/[slug].js (100%) rename test/{integration => production}/prerender-export/pages/blocking-fallback-some/[slug].js (100%) rename test/{integration => production}/prerender-export/pages/blocking-fallback/[slug].js (100%) rename test/{integration => production}/prerender-export/pages/blog/[post]/[comment].js (100%) rename test/{integration => production}/prerender-export/pages/blog/[post]/index.js (100%) rename test/{integration => production}/prerender-export/pages/blog/index.js (100%) rename test/{integration => production}/prerender-export/pages/catchall-explicit/[...slug].js (100%) rename test/{integration => production}/prerender-export/pages/catchall-optional/[[...slug]].js (100%) rename test/{integration => production}/prerender-export/pages/catchall/[...slug].js (100%) rename test/{integration => production}/prerender-export/pages/default-revalidate.js (100%) rename test/{integration => production}/prerender-export/pages/dynamic/[slug].js (100%) rename test/{integration => production}/prerender-export/pages/fallback-only/[slug].js (100%) rename test/{integration => production}/prerender-export/pages/index.js (100%) rename test/{integration => production}/prerender-export/pages/index/index.js (100%) rename test/{integration => production}/prerender-export/pages/lang/[lang]/about.js (100%) rename test/{integration => production}/prerender-export/pages/non-json-blocking/[p].js (100%) rename test/{integration => production}/prerender-export/pages/non-json/[p].js (100%) rename test/{integration => production}/prerender-export/pages/normal.js (100%) rename test/{integration => production}/prerender-export/pages/something.js (100%) rename test/{integration => production}/prerender-export/pages/user/[user]/profile.js (100%) rename test/{integration/prerender-export/test/index.test.ts => production/prerender-export/prerender-export.test.ts} (75%) rename test/{integration => production}/prerender-export/world.txt (100%) create mode 100644 test/production/prerender-invalid-catchall-params/index.test.ts rename test/{integration => production}/prerender-invalid-catchall-params/pages/[...slug].js (100%) rename test/{integration => production}/prerender-invalid-paths/pages/[foo]/[post].js (100%) create mode 100644 test/production/prerender-invalid-paths/prerender-invalid-paths.test.ts rename test/{integration => production}/prerender-no-revalidate/pages/index.js (100%) rename test/{integration => production}/prerender-no-revalidate/pages/named.js (100%) rename test/{integration => production}/prerender-no-revalidate/pages/nested/index.js (100%) rename test/{integration => production}/prerender-no-revalidate/pages/nested/named.js (100%) create mode 100644 test/production/prerender-no-revalidate/prerender-no-revalidate.test.ts rename test/{integration => production}/prerender-revalidate/pages/index.js (100%) rename test/{integration => production}/prerender-revalidate/pages/named.js (100%) rename test/{integration => production}/prerender-revalidate/pages/nested/index.js (100%) rename test/{integration => production}/prerender-revalidate/pages/nested/named.js (100%) rename test/{integration => production}/prerender-revalidate/pages/static.js (100%) create mode 100644 test/production/prerender-revalidate/prerender-revalidate.test.ts rename test/{integration/cli/duplicate-sass/node_modules/sass/index.js => production/production-build-dir/next.config.js} (100%) rename test/{integration/production-build-dir/build => production/production-build-dir}/pages/index.js (100%) create mode 100644 test/production/production-build-dir/production-build-dir.test.ts rename test/{integration/scss/scss-fixtures => production/production-config/fixture-generateBuildId}/next.config.js (68%) rename test/{integration/production-config => production/production-config/fixture-generateBuildId}/pages/_app.js (100%) rename test/{integration/production-config => production/production-config/fixture-generateBuildId}/pages/index.js (100%) rename test/{integration/production-config => production/production-config/fixture-generateBuildId}/styles.css (100%) rename test/{integration => production}/production-config/next.config.js (100%) create mode 100644 test/production/production-config/pages/_app.js create mode 100644 test/production/production-config/pages/index.js create mode 100644 test/production/production-config/production-config.test.ts create mode 100644 test/production/production-config/styles.css rename test/{integration => production}/production-nav/next.config.js (100%) rename test/{integration => production}/production-nav/pages/another.js (100%) rename test/{integration => production}/production-nav/pages/index.js (100%) create mode 100644 test/production/production-nav/production-nav.test.ts rename test/{integration => production}/production-start-no-build/next.config.js (100%) create mode 100644 test/production/production-start-no-build/production-start-no-build.test.ts rename test/{integration => production}/query-with-encoding/pages/index.js (100%) rename test/{integration => production}/query-with-encoding/pages/newline.js (100%) rename test/{integration => production}/query-with-encoding/pages/percent.js (100%) rename test/{integration => production}/query-with-encoding/pages/plus.js (100%) rename test/{integration => production}/query-with-encoding/pages/space.js (100%) create mode 100644 test/production/query-with-encoding/query-with-encoding.test.ts rename test/{integration => production}/re-export-all-exports-from-page-disallowed/component/child.js (100%) rename test/{integration => production}/re-export-all-exports-from-page-disallowed/component/test.js (100%) rename test/{integration => production}/re-export-all-exports-from-page-disallowed/pages/about.js (100%) rename test/{integration => production}/re-export-all-exports-from-page-disallowed/pages/contact.js (100%) rename test/{integration => production}/re-export-all-exports-from-page-disallowed/pages/index.js (100%) create mode 100644 test/production/re-export-all-exports-from-page-disallowed/re-export-all-exports-from-page-disallowed.test.ts rename test/{integration/prerender => production/re-export-all-exports-from-page-disallowed}/world.txt (100%) rename test/{integration => production}/render-error-on-module-error/pages/_error.js (100%) rename test/{integration => production}/render-error-on-module-error/pages/index.js (100%) create mode 100644 test/production/render-error-on-module-error/render-error-on-module-error.test.ts create mode 100644 test/production/render-error-on-top-level-error/render-error-on-top-level-error.test.ts rename test/{integration => production}/render-error-on-top-level-error/with-get-initial-props/pages/_error.js (100%) rename test/{integration => production}/render-error-on-top-level-error/with-get-initial-props/pages/index.js (100%) rename test/{integration => production}/render-error-on-top-level-error/without-get-initial-props/pages/_error.js (100%) rename test/{integration => production}/render-error-on-top-level-error/without-get-initial-props/pages/index.js (100%) rename test/{integration => production}/revalidate-as-path/pages/_app.js (100%) rename test/{integration => production}/revalidate-as-path/pages/another/index/index.js (100%) rename test/{integration => production}/revalidate-as-path/pages/index.js (100%) create mode 100644 test/production/revalidate-as-path/revalidate-as-path.test.ts rename test/{integration => production}/root-catchall-cache/app/[[...slug]]/page.js (100%) rename test/{integration => production}/root-catchall-cache/app/layout.js (100%) rename test/{integration => production}/root-catchall-cache/next.config.js (100%) create mode 100644 test/production/root-catchall-cache/root-catchall-cache.test.ts rename test/{integration => production}/root-optional-revalidate/pages/[[...slug]].js (100%) create mode 100644 test/production/root-optional-revalidate/root-optional-revalidate.test.ts rename test/{integration/route-load-cancel => production/route-load-cancel-css}/pages/index.js (100%) rename test/{integration => production}/route-load-cancel-css/pages/page1.js (100%) rename test/{integration => production}/route-load-cancel-css/pages/page1.module.css (100%) rename test/{integration/route-load-cancel => production/route-load-cancel-css}/pages/page2.js (100%) create mode 100644 test/production/route-load-cancel-css/route-load-cancel-css.test.ts create mode 100644 test/production/route-load-cancel-css/route-load-cancel-css/pages/index.js create mode 100644 test/production/route-load-cancel-css/route-load-cancel-css/pages/page1.js create mode 100644 test/production/route-load-cancel-css/route-load-cancel-css/pages/page1.module.css create mode 100644 test/production/route-load-cancel-css/route-load-cancel-css/pages/page2.js create mode 100644 test/production/route-load-cancel-css/route-load-cancel-css/route-load-cancel-css.test.ts create mode 100644 test/production/scss-invalid-module/invalid-module.test.ts rename test/{e2e/app-dir/scss/invalid-module => production/scss-invalid-module}/node_modules/example/index.js (100%) rename test/{e2e/app-dir/scss/invalid-module => production/scss-invalid-module}/node_modules/example/index.mjs (100%) rename test/{e2e/app-dir/scss/invalid-module => production/scss-invalid-module}/node_modules/example/index.module.scss (100%) create mode 100644 test/production/scss-invalid-module/node_modules/example/package.json rename test/{integration/css-fixtures/invalid-module => production/scss-invalid-module}/pages/index.js (100%) rename test/{integration/sharp-api/app => production/sharp-api}/pages/api/custom-sharp.js (100%) create mode 100644 test/production/sharp-api/sharp-api.test.ts rename test/{integration => production}/static-404/pages/index.js (100%) create mode 100644 test/production/static-404/static-404.test.ts rename test/{integration/styled-jsx-plugin/app => production/styled-jsx-plugin}/.babelrc.js (100%) rename test/{integration/styled-jsx-plugin/app => production/styled-jsx-plugin}/pages/index.js (100%) create mode 100644 test/production/styled-jsx-plugin/postcss.config.js create mode 100644 test/production/styled-jsx-plugin/styled-jsx-plugin.test.ts rename test/production/tsconfig-verifier/{test/index.test.ts => tsconfig-verifier.test.ts} (85%) rename test/{integration => production}/turborepo-access-trace/app/app/route1/route.js (100%) rename test/{integration => production}/turborepo-access-trace/app/lib/fetch-data.js (100%) rename test/{integration => production}/turborepo-access-trace/app/lib/get-data.js (100%) rename test/{integration => production}/turborepo-access-trace/app/lib/my-component.js (100%) rename test/{integration => production}/turborepo-access-trace/app/next.config.js (100%) rename test/{integration => production}/turborepo-access-trace/app/node_modules/some-cms/index.js (100%) rename test/{integration => production}/turborepo-access-trace/app/node_modules/some-cms/package.json (100%) rename test/{integration => production}/turborepo-access-trace/app/pages/image-import.js (100%) rename test/{integration => production}/turborepo-access-trace/app/pages/index.js (100%) rename test/{integration => production}/turborepo-access-trace/app/public/another.jpg (100%) rename test/{integration => production}/turborepo-access-trace/app/public/exclude-me/another.txt (100%) rename test/{integration => production}/turborepo-access-trace/app/public/exclude-me/hello.txt (100%) rename test/{integration => production}/turborepo-access-trace/app/public/test.jpg (100%) create mode 100644 test/production/turborepo-access-trace/turborepo-access-trace.test.ts rename test/{integration => production}/turbotrace-with-webpack-worker/app/content/hello.json (100%) rename test/{integration => production}/turbotrace-with-webpack-worker/app/include-me/hello.txt (100%) rename test/{integration => production}/turbotrace-with-webpack-worker/app/include-me/second.txt (100%) rename test/{integration => production}/turbotrace-with-webpack-worker/app/lib/fetch-data.js (100%) rename test/{integration => production}/turbotrace-with-webpack-worker/app/lib/get-data.js (100%) rename test/{integration => production}/turbotrace-with-webpack-worker/app/next.config.js (100%) rename test/{integration => production}/turbotrace-with-webpack-worker/app/node_modules/nested-structure/constants/package.json (100%) rename test/{integration => production}/turbotrace-with-webpack-worker/app/node_modules/nested-structure/lib/constants.js (100%) rename test/{integration => production}/turbotrace-with-webpack-worker/app/node_modules/nested-structure/lib/index.js (100%) rename test/{integration => production}/turbotrace-with-webpack-worker/app/node_modules/nested-structure/package.json (100%) rename test/{integration => production}/turbotrace-with-webpack-worker/app/node_modules/some-cms/index.js (100%) rename test/{integration => production}/turbotrace-with-webpack-worker/app/node_modules/some-cms/package.json (100%) rename test/{integration => production}/turbotrace-with-webpack-worker/app/pages/another.js (100%) rename test/{integration => production}/turbotrace-with-webpack-worker/app/pages/image-import.js (100%) rename test/{integration => production}/turbotrace-with-webpack-worker/app/pages/index.js (100%) rename test/{integration => production}/turbotrace-with-webpack-worker/app/public/another.jpg (100%) rename test/{integration => production}/turbotrace-with-webpack-worker/app/public/exclude-me/another.txt (100%) rename test/{integration => production}/turbotrace-with-webpack-worker/app/public/exclude-me/hello.txt (100%) rename test/{integration => production}/turbotrace-with-webpack-worker/app/public/test.jpg (100%) create mode 100644 test/production/turbotrace-with-webpack-worker/turbotrace-with-webpack-worker.test.ts rename test/{integration => production}/typeof-window-replace/app/node_modules/comps/index.js (76%) rename test/{integration => production}/typeof-window-replace/app/node_modules/comps/package.json (100%) rename test/{integration/non-next-dist-exclude => production/typeof-window-replace}/app/package.json (62%) rename test/{integration => production}/typeof-window-replace/app/pages/index.js (100%) create mode 100644 test/production/typeof-window-replace/typeof-window-replace.test.ts rename test/{integration => production}/typescript-custom-tsconfig/next.config.js (100%) rename test/{integration => production}/typescript-custom-tsconfig/pages/index.tsx (100%) create mode 100644 test/production/typescript-custom-tsconfig/typescript-custom-tsconfig.test.ts rename test/{integration => production}/typescript-custom-tsconfig/web.tsconfig.json (100%) rename test/{integration => production}/typescript-filtered-files/pages/contest.tsx (100%) create mode 100644 test/production/typescript-filtered-files/typescript-filtered-files.test.ts rename test/{integration => production}/typescript-ignore-errors/pages/index.tsx (100%) rename test/{integration/typescript-hmr/tsconfig.json => production/typescript-ignore-errors/tsconfig.test.json} (100%) create mode 100644 test/production/typescript-ignore-errors/typescript-ignore-errors.test.ts rename test/{integration => production}/webpack-bun-externals/pages/index.js (100%) create mode 100644 test/production/webpack-bun-externals/webpack-bun-externals.test.ts rename test/{integration => production}/webpack-config-extensionalias/components/TsxComponent.tsx (100%) rename test/{integration => production}/webpack-config-extensionalias/next.config.js (100%) rename test/{integration => production}/webpack-config-extensionalias/pages/pagewithimport.js (100%) rename test/{integration/webpack-config-extensionalias/test/index.test.ts => production/webpack-config-extensionalias/webpack-config-extensionalias.test.ts} (55%) rename test/{integration => production}/webpack-config-mainjs/client/polyfills.js (100%) rename test/{integration => production}/webpack-config-mainjs/next.config.js (100%) rename test/{integration => production}/webpack-config-mainjs/pages/static.js (100%) rename test/{integration/webpack-config-mainjs/test/index.test.ts => production/webpack-config-mainjs/webpack-config-mainjs.test.ts} (52%) rename test/{integration/with-electron/app => production/with-electron}/pages/about.js (100%) rename test/{integration/with-electron/app => production/with-electron}/pages/index.js (100%) create mode 100644 test/production/with-electron/with-electron.test.ts rename test/{integration => unit}/link-without-router/components/hello.js (100%) create mode 100644 test/unit/link-without-router/link-without-router.test.tsx diff --git a/.agents/skills/pr-status-triage/SKILL.md b/.agents/skills/pr-status-triage/SKILL.md index 6a06077142e1..46839db45d50 100644 --- a/.agents/skills/pr-status-triage/SKILL.md +++ b/.agents/skills/pr-status-triage/SKILL.md @@ -14,12 +14,12 @@ Use this skill when the user asks about PR status, CI failures, or review commen ## Workflow -1. Run `node scripts/pr-status.js --wait` in the background (timeout 1 min), then read `scripts/pr-status/index.md`. -2. Analyze each `job-{id}.md` and `thread-{N}.md` file for failures and review feedback. +1. Run `node scripts/pr-status.js --wait` in the background (timeout 1 min), then read `scripts/pr-status/results/index.md`. +2. Analyze each `job-{id}.md` and `thread-{N}.md` file in `scripts/pr-status/results/` for failures and review feedback. 3. Prioritize blocking jobs first: build, lint, types, then test jobs. 4. Treat failures as real until disproven; check the "Known Flaky Tests" section before calling anything flaky. 5. Reproduce locally with the same mode and env vars as CI. -6. After addressing review comments, reply to the thread describing what was done, then resolve it. Use `reply-and-resolve-thread` to do both in one step, or use `reply-thread` + `resolve-thread` separately. See `thread-N.md` files for ready-to-use commands. +6. After addressing review comments, reply to the thread describing what was done, then resolve it. Use `reply-and-resolve-thread` to do both in one step, or use `reply-thread` + `resolve-thread` separately. See `scripts/pr-status/results/thread-N.md` files for ready-to-use commands. 7. When the only remaining failures are known flaky tests and no code changes are needed, retrigger the failing CI jobs with `gh run rerun --failed`. Then wait 5 minutes and go back to step 1. Repeat this loop up to 5 times. ## Quick Commands diff --git a/.agents/skills/pr-status-triage/workflow.md b/.agents/skills/pr-status-triage/workflow.md index a47c1ce04a68..6228352be2d7 100644 --- a/.agents/skills/pr-status-triage/workflow.md +++ b/.agents/skills/pr-status-triage/workflow.md @@ -45,6 +45,6 @@ Or do both in one step: node scripts/pr-status.js reply-and-resolve-thread "Done -- " ``` -The ready-to-use commands with the correct thread IDs are at the bottom of each `thread-N.md` file in `scripts/pr-status/`. +The ready-to-use commands with the correct thread IDs are at the bottom of each `thread-N.md` file in `scripts/pr-status/results/`. **Important:** Always reply with a description of the actions taken before resolving. This gives the reviewer context about what changed. diff --git a/.config/eslintignore.mjs b/.config/eslintignore.mjs index 984e0506e44a..62407c08a66e 100644 --- a/.config/eslintignore.mjs +++ b/.config/eslintignore.mjs @@ -32,8 +32,6 @@ export default globalIgnores([ 'packages/next-codemod/**/*.d.ts', 'packages/next-env/**/*.d.ts', 'packages/create-next-app/templates/**/*', - 'test/integration/eslint/**/*.js', - 'test/integration/script-loader/**/*.js', 'test/development/basic/legacy-decorators/**/*.js', 'test/production/emit-decorator-metadata/**/*.js', '!test/**/*.test.*', diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 2377a49cdb56..28fbebf14c4f 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -388,59 +388,6 @@ jobs: stepName: 'test-turbopack-dev-react-${{ matrix.react }}-${{ matrix.group }}' secrets: inherit - test-turbopack-integration: - name: test turbopack integration - needs: - [ - 'optimize-ci', - 'changes', - 'build-native', - 'build-next', - 'fetch-test-timings', - ] - if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }} - - strategy: - fail-fast: false - matrix: - group: - - 1/13 - - 2/13 - - 3/13 - - 4/13 - - 5/13 - - 6/13 - - 7/13 - - 8/13 - - 9/13 - - 10/13 - - 11/13 - - 12/13 - - 13/13 - # Empty value uses default - # TODO: Run with React 18. - # Integration tests use the installed React version in next/package.json. - # We can't easily switch like we do for e2e tests. - # Skipping this dimension until we can figure out a way to test multiple React versions. - react: [''] - uses: ./.github/workflows/build_reusable.yml - with: - nodeVersion: 20.9.0 - afterBuild: | - export IS_TURBOPACK_TEST=1 - export NEXT_TEST_REACT_VERSION="${{ matrix.react }}" - export __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true - export RUST_BACKTRACE=1 - - node run-tests.js \ - --timings \ - --require-timings \ - -g ${{ matrix.group }} \ - --type integration - testTimingsArtifact: 'test-timings' - stepName: 'test-turbopack-integration-react-${{ matrix.react }}-${{ matrix.group }}' - secrets: inherit - test-turbopack-production: name: test turbopack production needs: @@ -523,52 +470,6 @@ jobs: stepName: 'test-rspack-dev-react-${{ matrix.react }}-${{ matrix.group }}' secrets: inherit - test-rspack-integration: - name: test rspack development integration - needs: - [ - 'optimize-ci', - 'changes', - 'build-next', - 'build-native', - 'fetch-test-timings', - ] - if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' && needs.changes.outputs.rspack == 'true' }} - strategy: - fail-fast: false - matrix: - group: [1/6, 2/6, 3/6, 4/6, 5/6, 6/6] - # Empty value uses default - # TODO: Run with React 18. - # Integration tests use the installed React version in next/package.json. - # We can't easily switch like we do for e2e tests. - # Skipping this dimension until we can figure out a way to test multiple React versions. - react: [''] - uses: ./.github/workflows/build_reusable.yml - with: - nodeVersion: 20.9.0 - afterBuild: | - export NEXT_EXTERNAL_TESTS_FILTERS="$(pwd)/test/rspack-dev-tests-manifest.json" - export NEXT_TEST_REACT_VERSION="${{ matrix.react }}" - export __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true - - # rspack flags - export NEXT_RSPACK=1 - export NEXT_TEST_USE_RSPACK=1 - - # HACK: Despite the name, this environment variable is only used to gate - # tests, so it's applicable to rspack - export TURBOPACK_DEV=1 - - node run-tests.js \ - --timings \ - --require-timings \ - -g ${{ matrix.group }} \ - --type integration - testTimingsArtifact: 'test-timings' - stepName: 'test-rspack-integration-react-${{ matrix.react }}-${{ matrix.group }}' - secrets: inherit - test-rspack-production: name: test rspack production needs: @@ -611,52 +512,6 @@ jobs: stepName: 'test-rspack-production-react-${{ matrix.react }}-${{ matrix.group }}' secrets: inherit - test-rspack-production-integration: - name: test rspack production integration - needs: - [ - 'optimize-ci', - 'changes', - 'build-next', - 'build-native', - 'fetch-test-timings', - ] - if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' && needs.changes.outputs.rspack == 'true' }} - strategy: - fail-fast: false - matrix: - group: [1/7, 2/7, 3/7, 4/7, 5/7, 6/7, 7/7] - # Empty value uses default - # TODO: Run with React 18. - # Integration tests use the installed React version in next/package.json. - # We can't easily switch like we do for e2e tests. - # Skipping this dimension until we can figure out a way to test multiple React versions. - react: [''] - uses: ./.github/workflows/build_reusable.yml - with: - nodeVersion: 20.9.0 - afterBuild: | - export NEXT_EXTERNAL_TESTS_FILTERS="$(pwd)/test/rspack-build-tests-manifest.json" - export NEXT_TEST_REACT_VERSION="${{ matrix.react }}" - export __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true - - # rspack flags - export NEXT_RSPACK=1 - export NEXT_TEST_USE_RSPACK=1 - - # HACK: Despite the name, this environment variable is only used to gate - # tests, so it's applicable to rspack - export TURBOPACK_BUILD=1 - - node run-tests.js \ - --timings \ - --require-timings \ - -g ${{ matrix.group }} \ - --type integration - testTimingsArtifact: 'test-timings' - stepName: 'test-rspack-production-integration-react-${{ matrix.react }}-${{ matrix.group }}' - secrets: inherit - test-next-swc-wasm: name: test next-swc wasm needs: ['optimize-ci', 'changes', 'build-next'] @@ -795,7 +650,7 @@ jobs: strategy: fail-fast: false matrix: - group: [1/5, 2/5, 3/5, 4/5, 5/5] + group: [1/10, 2/10, 3/10, 4/10, 5/10, 6/10, 7/10, 8/10, 9/10, 10/10] uses: ./.github/workflows/build_reusable.yml with: @@ -807,7 +662,7 @@ jobs: --group ${{ matrix.group }} \ --preview-builds-base-url "${{ vars.PREVIEW_BUILDS_BASE_URL }}" stepName: 'test-new-tests-dev-${{matrix.group}}' - timeout_minutes: 60 # Increase the default timeout as tests are intentionally run multiple times to detect flakes + timeout_minutes: 120 # Increase the default timeout as tests are intentionally run multiple times to detect flakes secrets: inherit @@ -821,7 +676,7 @@ jobs: strategy: fail-fast: false matrix: - group: [1/5, 2/5, 3/5, 4/5, 5/5] + group: [1/10, 2/10, 3/10, 4/10, 5/10, 6/10, 7/10, 8/10, 9/10, 10/10] uses: ./.github/workflows/build_reusable.yml with: @@ -833,7 +688,7 @@ jobs: --group ${{ matrix.group }} \ --preview-builds-base-url "${{ vars.PREVIEW_BUILDS_BASE_URL }}" stepName: 'test-new-tests-start-${{matrix.group}}' - timeout_minutes: 60 # Increase the default timeout as tests are intentionally run multiple times to detect flakes + timeout_minutes: 120 # Increase the default timeout as tests are intentionally run multiple times to detect flakes secrets: inherit test-new-tests-deploy: @@ -969,15 +824,16 @@ jobs: with: nodeVersion: 20.9.0 afterBuild: | + export NEXT_TEST_MODE=start export IS_WEBPACK_TEST=1 export __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true - node run-tests.js \ + node run-tests.js --type production \ --concurrency 4 \ test/production/pages-dir/production/test/index.test.ts \ - test/integration/css-client-nav/test/index.test.ts \ - test/integration/rewrites-has-condition/test/index.test.ts \ - test/integration/create-next-app/index.test.ts \ - test/integration/create-next-app/package-manager/pnpm.test.ts + test/e2e/css-client-nav/css-client-nav.test.ts \ + test/e2e/rewrites-has-condition/rewrites-has-condition.test.ts \ + test/production/create-next-app/index.test.ts \ + test/production/create-next-app/package-manager/pnpm.test.ts stepName: 'test-integration-windows' runs_on_labels: '["windows-latest-8-core-oss"]' buildNativeTarget: 'x86_64-pc-windows-msvc' @@ -1040,58 +896,6 @@ jobs: stepName: 'test-prod-react-${{ matrix.react }}-${{ matrix.group }}' secrets: inherit - test-integration: - name: test integration - needs: - [ - 'optimize-ci', - 'changes', - 'build-native', - 'build-next', - 'fetch-test-timings', - ] - if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }} - - strategy: - fail-fast: false - matrix: - group: - - 1/13 - - 2/13 - - 3/13 - - 4/13 - - 5/13 - - 6/13 - - 7/13 - - 8/13 - - 9/13 - - 10/13 - - 11/13 - - 12/13 - - 13/13 - # Empty value uses default - # TODO: Run with React 18. - # Integration tests use the installed React version in next/package.json. - # We can't easily switch like we do for e2e tests. - # Skipping this dimension until we can figure out a way to test multiple React versions. - react: [''] - uses: ./.github/workflows/build_reusable.yml - with: - nodeVersion: 20.9.0 - afterBuild: | - export IS_WEBPACK_TEST=1 - export NEXT_TEST_REACT_VERSION="${{ matrix.react }}" - export __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true - - node run-tests.js \ - --timings \ - --require-timings \ - -g ${{ matrix.group }} \ - --type integration - testTimingsArtifact: 'test-timings' - stepName: 'test-integration-${{ matrix.group }}-react-${{ matrix.react }}' - secrets: inherit - test-firefox-safari: name: test firefox and safari needs: ['optimize-ci', 'changes', 'build-native', 'build-next'] @@ -1125,28 +929,6 @@ jobs: stepName: 'test-firefox-safari' secrets: inherit - # Manifest generated via: https://gist.github.com/wyattjoh/2ceaebd82a5bcff4819600fd60126431 - test-cache-components-integration: - name: test cache components integration - needs: ['optimize-ci', 'changes', 'build-native', 'build-next'] - if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }} - - uses: ./.github/workflows/build_reusable.yml - with: - nodeVersion: 20.9.0 - afterBuild: | - export __NEXT_CACHE_COMPONENTS=true - export __NEXT_EXPERIMENTAL_CACHED_NAVIGATIONS=true - export __NEXT_EXPERIMENTAL_APP_NEW_SCROLL_HANDLER=true - export NEXT_EXTERNAL_TESTS_FILTERS="test/cache-components-tests-manifest.json" - export IS_WEBPACK_TEST=1 - - node run-tests.js \ - --timings \ - --type integration - stepName: 'test-cache-components-integration' - secrets: inherit - test-cache-components-dev: name: test cache components dev needs: @@ -1356,11 +1138,9 @@ jobs: 'test-next-config-ts-native-ts-prod', 'test-dev', 'test-prod', - 'test-integration', 'test-firefox-safari', 'test-cache-components-dev', 'test-cache-components-prod', - 'test-cache-components-integration', 'test-node-streams-cache-components-dev', 'test-node-streams-cache-components-prod', 'test-node-streams-dev', @@ -1370,7 +1150,6 @@ jobs: 'rustdoc-check', 'test-next-swc-wasm', 'test-turbopack-dev', - 'test-turbopack-integration', 'test-new-tests-dev', 'test-new-tests-start', 'test-new-tests-deploy', diff --git a/.prettierignore b/.prettierignore index 73e48a51389e..1395aad47f8e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -60,8 +60,6 @@ test/e2e/app-dir/server-source-maps/fixtures/default/internal-pkg/sourcemapped.j test/e2e/app-dir/server-source-maps/fixtures/default/external-pkg/sourcemapped.js test/development/mcp-server/fixtures/default-template/app/build-error/page.tsx test/development/mcp-server/fixtures/compilation-errors-app/app/syntax-error/page.tsx -# Tested against auto-generated content that isn't formatted -test/integration/typescript-app-type-declarations/next-env.strictRouteTypes.d.ts # turbopack crates, disable for some tests and precompiled dependencies. /turbopack/crates/*/js/src/compiled @@ -91,8 +89,6 @@ readme.md test/development/app-dir/hmr-symlink/app/symlink-chain/page.tsx test/development/app-dir/hmr-symlink/app/symlink-link/page.tsx test/development/app-dir/ssr-in-rsc/node_modules/random-react-library -test/integration/build-trace-extra-entries/app/node_modules/pkg-behind-symlink -test/integration/edge-runtime-configurable-guards/node_modules/lib test/production/app-dir/symbolic-file-links/src/i18n.ts test/production/app-dir/symbolic-file-links/src/app/layout.tsx test/production/app-dir/symbolic-file-links/src/app/page.tsx diff --git a/crates/next-core/src/url_node.rs b/crates/next-core/src/url_node.rs index cdd88ba44350..efaa22aad117 100644 --- a/crates/next-core/src/url_node.rs +++ b/crates/next-core/src/url_node.rs @@ -338,7 +338,7 @@ pub fn get_sorted_routes(normalized_pages: &[String]) -> Result, Url // segment Eg you can't have pages/[post]/abc.js and // pages/[hello]/something-else.js Only 1 dynamic segment per nesting level - // So in the case that is test/integration/dynamic-routing it'll be this: + // So in the case that is test/e2e/dynamic-routing it'll be this: // pages/[post]/comments.js // pages/blog/[post]/comment/[id].js // Both are fine because `pages/[post]` and `pages/blog` are on the same level diff --git a/package.json b/package.json index 7211d444923c..d202dcd6c45c 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,6 @@ "next-with-deps": "./scripts/next-with-deps.sh", "next": "cross-env NEXT_PRIVATE_LOCAL_DEV=1 NEXT_TELEMETRY_DISABLED=1 NODE_OPTIONS=\"--trace-deprecation --enable-source-maps\" next", "next-no-sourcemaps": "echo 'No longer supported. Use `pnpm next --disable-source-maps` instead'; exit 1;", - "clean-trace-jaeger": "node scripts/rm.mjs test/integration/basic/.next && TRACE_TARGET=JAEGER pnpm next build test/integration/basic", "debug": "cross-env NEXT_PRIVATE_LOCAL_DEV=1 NEXT_TELEMETRY_DISABLED=1 node --inspect --trace-deprecation --enable-source-maps packages/next/dist/bin/next", "debug-brk": "cross-env NEXT_PRIVATE_LOCAL_DEV=1 NEXT_TELEMETRY_DISABLED=1 node --inspect-brk --trace-deprecation --enable-source-maps packages/next/dist/bin/next", "pnpm:devPreinstall": "node scripts/create-next-bin-placeholder.mjs", diff --git a/packages/create-next-app/helpers/get-pkg-manager.ts b/packages/create-next-app/helpers/get-pkg-manager.ts index 2d9f850949de..5bd9cb955cb6 100644 --- a/packages/create-next-app/helpers/get-pkg-manager.ts +++ b/packages/create-next-app/helpers/get-pkg-manager.ts @@ -21,33 +21,45 @@ export function getPkgManager(): PackageManager { } /** - * Get the major version of pnpm being used. + * Get the full version string for the given package manager. * Returns null if unable to determine the version. * * First tries to parse from npm_config_user_agent (e.g., "pnpm/9.13.2 npm/? ..."), - * then falls back to spawning `pnpm --version --silent`. + * then falls back to spawning ` --version`. */ -export function getPnpmMajorVersion(): number | null { - // Try to get version from user agent first (e.g., "pnpm/9.13.2 npm/? node/v20.x linux x64") +export function getPackageManagerVersion( + packageManager: PackageManager +): string | null { const userAgent = process.env.npm_config_user_agent || '' - const pnpmVersionMatch = userAgent.match(/pnpm\/(\d+)/) - if (pnpmVersionMatch) { - return parseInt(pnpmVersionMatch[1], 10) + const userAgentMatch = userAgent.match( + new RegExp(`${packageManager}/([\\d.]+[\\w.-]*)`) + ) + if (userAgentMatch) { + return userAgentMatch[1] } - // Fall back to spawning pnpm --version try { - const version = execSync('pnpm --version --silent', { + const version = execSync(`${packageManager} --version`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'], }).trim() - const majorVersion = parseInt(version.split('.')[0], 10) - if (!Number.isNaN(majorVersion)) { - return majorVersion + if (/^\d+\.\d+\.\d+/.test(version)) { + return version } } catch { - // pnpm not available or failed to run + // package manager not available or failed to run } return null } + +/** + * Get the major version of pnpm being used. + * Returns null if unable to determine the version. + */ +export function getPnpmMajorVersion(): number | null { + const version = getPackageManagerVersion('pnpm') + if (!version) return null + const major = parseInt(version.split('.')[0], 10) + return Number.isNaN(major) ? null : major +} diff --git a/packages/create-next-app/templates/index.ts b/packages/create-next-app/templates/index.ts index c27cee21bcb5..ff1891556f14 100644 --- a/packages/create-next-app/templates/index.ts +++ b/packages/create-next-app/templates/index.ts @@ -1,7 +1,10 @@ import { install } from "../helpers/install"; import { runTypegen } from "../helpers/typegen"; import { copy } from "../helpers/copy"; -import { getPnpmMajorVersion } from "../helpers/get-pkg-manager"; +import { + getPackageManagerVersion, + getPnpmMajorVersion, +} from "../helpers/get-pkg-manager"; import { async as glob } from "fast-glob"; import os from "os"; @@ -211,6 +214,24 @@ export const installTemplate = async ({ const version = process.env.NEXT_PRIVATE_TEST_VERSION ?? pkg.version; const bundlerFlags = bundler === Bundler.Webpack ? " --webpack" : ""; + // When isolated tests pack workspace packages and pass their tarball + // paths via `NEXT_TEST_PKG_PATHS` (set by `run-tests.js`), prefer those + // paths over `version` so siblings like `next-rspack` and + // `eslint-config-next` install from their own tarball — not from the + // `next` tarball, which would cause npm to extract `next`'s contents + // into the sibling's `node_modules/` entry. + const testPkgPaths: Map | null = (() => { + const env = process.env.NEXT_TEST_PKG_PATHS; + if (!env) return null; + try { + return new Map(JSON.parse(env)); + } catch { + return null; + } + })(); + const resolvePkgVersion = (name: string): string => + testPkgPaths?.get(name) ?? version; + /** Create a package.json for the new project and write it to disk. */ const packageJson: any = { name: appName, @@ -229,24 +250,13 @@ export const installTemplate = async ({ dependencies: { react: nextjsReactPeerVersion, "react-dom": nextjsReactPeerVersion, - next: version, + next: resolvePkgVersion("next"), }, devDependencies: {}, }; if (bundler === Bundler.Rspack) { - const NEXT_PRIVATE_TEST_VERSION = process.env.NEXT_PRIVATE_TEST_VERSION; - if ( - NEXT_PRIVATE_TEST_VERSION && - path.isAbsolute(NEXT_PRIVATE_TEST_VERSION) - ) { - packageJson.dependencies["next-rspack"] = path.resolve( - path.dirname(NEXT_PRIVATE_TEST_VERSION), - "../next-rspack/packed.tgz", - ); - } else { - packageJson.dependencies["next-rspack"] = version; - } + packageJson.dependencies["next-rspack"] = resolvePkgVersion("next-rspack"); } if (reactCompiler) { @@ -280,7 +290,7 @@ export const installTemplate = async ({ packageJson.devDependencies = { ...packageJson.devDependencies, eslint: "^9", - "eslint-config-next": version, + "eslint-config-next": resolvePkgVersion("eslint-config-next"), }; } @@ -346,6 +356,19 @@ export const installTemplate = async ({ } } + // Pin the package manager version via corepack so the project always uses the + // same version the project was created with. This avoids subtle differences + // between the pnpm/yarn/bun version on a contributor's PATH and the one used + // to scaffold the project (e.g. a pnpm-workspace.yaml written for pnpm v10+ + // but installed with pnpm v9 on PATH). + // See: https://nodejs.org/api/packages.html#packagemanager + if (packageManager !== "npm") { + const packageManagerVersion = getPackageManagerVersion(packageManager); + if (packageManagerVersion) { + packageJson.packageManager = `${packageManager}@${packageManagerVersion}`; + } + } + if (packageManager === "bun") { // Equivalent to pnpm's `ignoredBuiltDependencies`, added in bun 1.3.2. // - https://bun.com/blog/bun-v1.3.2#faster-bun-install diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index e30ce1c0afa8..68e696f09969 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -40,6 +40,7 @@ "dev": "pnpm build", "build": "swc -d dist src && pnpm types", "types": "tsc --skipLibCheck --declaration --emitDeclarationOnly --esModuleInterop --declarationDir dist --rootDir src", + "pack-for-isolated-tests": "pnpm pack --out ./packed.tgz", "prepublishOnly": "cd ../../ && turbo run build" }, "exports": { diff --git a/packages/next/src/server/dev/hot-reloader-rspack.ts b/packages/next/src/server/dev/hot-reloader-rspack.ts index 8ffbbc2bb0ab..babe31e16a92 100644 --- a/packages/next/src/server/dev/hot-reloader-rspack.ts +++ b/packages/next/src/server/dev/hot-reloader-rspack.ts @@ -181,7 +181,7 @@ export default class HotReloaderRspack extends HotReloaderWebpack { if (key === 'edge-server' && !this.isEdgeServerCacheEnabled) return // TODO: Rspack does not store middleware entries in persistent cache, causing - // test/integration/middleware-src/test/index.test.ts to fail. This is a temporary + // test/e2e/middleware-src/middleware-src.test.ts to fail. This is a temporary // workaround to skip middleware entry caching until Rspack properly supports it. if (page === '/middleware') { return diff --git a/packages/next/src/shared/lib/router/utils/sorted-routes.ts b/packages/next/src/shared/lib/router/utils/sorted-routes.ts index c83e8cf10353..5078920cef4c 100644 --- a/packages/next/src/shared/lib/router/utils/sorted-routes.ts +++ b/packages/next/src/shared/lib/router/utils/sorted-routes.ts @@ -211,7 +211,7 @@ export function getSortedRoutes( // Eg you can't have pages/[post]/abc.js and pages/[hello]/something-else.js // Only 1 dynamic segment per nesting level - // So in the case that is test/integration/dynamic-routing it'll be this: + // So in the case that is test/e2e/dynamic-routing it'll be this: // pages/[post]/comments.js // pages/blog/[post]/comment/[id].js // Both are fine because `pages/[post]` and `pages/blog` are on the same level diff --git a/packages/next/taskfile-swc.js b/packages/next/taskfile-swc.js index 50322b8c060b..833f1aa99d59 100644 --- a/packages/next/taskfile-swc.js +++ b/packages/next/taskfile-swc.js @@ -86,7 +86,7 @@ module.exports = function (task) { targets: { // Ideally, should be same version defined in packages/next/package.json#engines // Currently a few minors behind due to babel class transpiling - // which fails "test/integration/mixed-ssg-serverprops-error/test/index.test.js" + // which fails "test/production/mixed-ssg-serverprops-error/test/index.test.js" node: '16.8.0', }, }, diff --git a/run-tests.js b/run-tests.js index d46ee7f967ea..bee42baf2a65 100644 --- a/run-tests.js +++ b/run-tests.js @@ -263,7 +263,6 @@ const testFilters = { production: new RegExp('^(test/(production|e2e))'), unit: new RegExp('^(test/unit|packages/.*/src|packages/next-codemod)'), examples: 'examples/', - integration: 'test/integration/', e2e: 'test/e2e/', } @@ -631,9 +630,9 @@ ${ENDGROUP}`) ((options.type && options.type !== 'unit') || tests.some((test) => !testFilters.unit.test(test.file))) ) { - // For isolated next tests (e2e, dev, prod) and integration tests we create - // a starter Next.js install to re-use to speed up tests to avoid having to - // run `pnpm install` each time. + // For isolated next tests (e2e, dev, prod) we create a starter Next.js + // install to re-use to speed up tests to avoid having to run `pnpm install` + // each time. console.log(`${GROUP}Creating shared Next.js install`) const reactVersion = process.env.NEXT_TEST_REACT_VERSION || nextjsReactPeerVersion @@ -969,28 +968,16 @@ ${ENDGROUP}`) } } - const directorySemas = new Map() - const results = await Promise.allSettled( tests.map(async (test) => { - const dirName = path.dirname(test.file) - let dirSema = directorySemas.get(dirName) - - // we only restrict 1 test per directory for - // legacy integration tests - if (/^test[/\\]integration/.test(test.file) && dirSema === undefined) { - directorySemas.set(dirName, (dirSema = new Sema(1))) - } - // TODO: Use explicit resource managment instead of this acquire/release pattern - // once CI runs with Node.js 24+. - if (dirSema) await dirSema.acquire() + // TODO: Use explicit resource management instead of this acquire/release + // pattern once CI runs with Node.js 24+. await sema.acquire() try { await runTest(test) } finally { sema.release() - if (dirSema) dirSema.release() } }) ) diff --git a/scripts/get-changed-tests.mjs b/scripts/get-changed-tests.mjs index cb2f5ce2cbbc..8ef7cd7d82a3 100644 --- a/scripts/get-changed-tests.mjs +++ b/scripts/get-changed-tests.mjs @@ -164,10 +164,7 @@ export default async function getChangedTests() { devTests.add(file) prodTests.add(file) deployTests.add(file) - } else if (file.startsWith('test/integration/')) { - devTests.add(file) - prodTests.add(file) - } else if (file.startsWith('test/prod')) { + } else if (file.startsWith('test/production')) { prodTests.add(file) } else if (file.startsWith('test/development')) { devTests.add(file) diff --git a/scripts/pr-logs.js b/scripts/pr-logs.js new file mode 100644 index 000000000000..df2dfa7b26cc --- /dev/null +++ b/scripts/pr-logs.js @@ -0,0 +1,426 @@ +#!/usr/bin/env node + +const { execFileSync, spawn } = require('child_process') +const fs = require('fs/promises') +const path = require('path') + +const OWNER = 'vercel' +const REPO = 'next.js' +const WORKFLOW_NAME = 'build-and-test' +const OUTPUT_ROOT = path.join(__dirname, 'pr-logs') +const FAILED_CONCLUSIONS = new Set(['failure', 'timed_out', 'startup_failure']) +const LOG_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\s?/ + +function execGh(args) { + try { + return execFileSync('gh', args, { + encoding: 'utf8', + maxBuffer: 50 * 1024 * 1024, + }).trim() + } catch (error) { + const command = `gh ${args.join(' ')}` + console.error(`Command failed: ${command}`) + console.error(error.stderr || error.message) + throw error + } +} + +function execGhJson(args) { + const output = execGh(args) + return JSON.parse(output) +} + +function execGhLines(args) { + const output = execGh(args) + if (!output) return [] + + return output + .split('\n') + .filter((line) => line.trim()) + .map((line) => JSON.parse(line)) +} + +function execGhTextAsync(args) { + return new Promise((resolve, reject) => { + const child = spawn('gh', args, { + stdio: ['ignore', 'pipe', 'pipe'], + }) + const chunks = [] + let stderr = '' + + child.stdout.on('data', (chunk) => chunks.push(chunk)) + child.stderr.on('data', (chunk) => { + stderr += chunk + }) + child.on('close', (code) => { + if (code !== 0) { + const error = new Error(`Command failed: gh ${args.join(' ')}`) + error.stderr = stderr.trim() + reject(error) + return + } + + resolve(Buffer.concat(chunks).toString('utf8')) + }) + child.on('error', reject) + }) +} + +function stripLogTimestamps(logs) { + return logs + .split('\n') + .map((line) => line.replace(LOG_TIMESTAMP_RE, '')) + .join('\n') +} + +function sanitizeFilename(name) { + return String(name) + .replace(/[^a-zA-Z0-9._-]/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') + .slice(0, 100) +} + +function formatDuration(startedAt, completedAt) { + if (!startedAt || !completedAt) return 'N/A' + + const start = new Date(startedAt) + const end = new Date(completedAt) + if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) return 'N/A' + + const totalSeconds = Math.floor((end - start) / 1000) + if (totalSeconds < 0) return 'N/A' + if (totalSeconds < 60) return `${totalSeconds}s` + + const minutes = Math.floor(totalSeconds / 60) + const seconds = totalSeconds % 60 + return `${minutes}m ${seconds}s` +} + +function formatStatus(job) { + return job.conclusion ? `${job.status}/${job.conclusion}` : job.status +} + +function usage() { + console.log(`Usage: node scripts/pr-logs.js [PR_NUMBER] [--wait] [--failed-only] + +Downloads CI job logs for the latest "${WORKFLOW_NAME}" run on the current PR. + +Options: + --wait Wait for the current run to finish before downloading logs + --failed-only Only download logs for failed jobs + --help Show this help text +`) +} + +function getBranchInfo(prNumberArg) { + if (prNumberArg) { + try { + const data = execGhJson([ + 'pr', + 'view', + String(prNumberArg), + '--json', + 'number,headRefName', + ]) + + return { + prNumber: String(data.number), + branchName: data.headRefName, + } + } catch { + console.error(`Failed to fetch PR #${prNumberArg}`) + process.exit(1) + } + } + + try { + const data = execGhJson(['pr', 'view', '--json', 'number,headRefName']) + return { + prNumber: String(data.number), + branchName: data.headRefName, + } + } catch { + console.error( + 'Could not detect a PR from the current branch. Pass a PR number explicitly.' + ) + process.exit(1) + } +} + +function getLatestWorkflowRun(branchName) { + const route = `repos/${OWNER}/${REPO}/actions/runs?branch=${encodeURIComponent(branchName)}&per_page=20` + const runs = execGhLines([ + 'api', + route, + '--jq', + `.workflow_runs[] | select(.name == "${WORKFLOW_NAME}") | {id, name, status, conclusion, run_attempt, html_url, created_at, updated_at}`, + ]) + + return runs[0] || null +} + +function getRunMetadata(runId) { + return execGhJson([ + 'api', + `repos/${OWNER}/${REPO}/actions/runs/${runId}`, + '--jq', + '{id, name, status, conclusion, run_attempt, html_url, created_at, updated_at}', + ]) +} + +function getJobsForRunAttempt(runId, runAttempt) { + const jobs = [] + let page = 1 + + while (true) { + const pageJobs = execGhLines([ + 'api', + `repos/${OWNER}/${REPO}/actions/runs/${runId}/attempts/${runAttempt}/jobs?per_page=100&page=${page}`, + '--jq', + '.jobs[] | {id, name, status, conclusion, started_at, completed_at, html_url}', + ]) + + if (pageJobs.length === 0) { + break + } + + jobs.push(...pageJobs) + + if (pageJobs.length < 100) { + break + } + + page += 1 + } + + return jobs +} + +async function downloadJobLog(jobId) { + const logs = await execGhTextAsync([ + 'api', + `repos/${OWNER}/${REPO}/actions/jobs/${jobId}/logs`, + ]) + + return stripLogTimestamps(logs) +} + +async function mapLimit(items, limit, mapper) { + const queue = [...items] + const workers = Array.from( + { length: Math.min(limit, queue.length) }, + async () => { + while (queue.length > 0) { + const item = queue.shift() + await mapper(item) + } + } + ) + + await Promise.all(workers) +} + +function buildOutputDir(prNumber, runId, runAttempt) { + return path.join( + OUTPUT_ROOT, + `pr-${prNumber}`, + `run-${runId}-attempt-${runAttempt}` + ) +} + +function buildIndexLog(branchInfo, runMetadata, jobs, results, outputDir) { + const lines = [ + `PR: #${branchInfo.prNumber}`, + `Branch: ${branchInfo.branchName}`, + `Workflow: ${runMetadata.name}`, + `Run: ${runMetadata.id} (attempt ${runMetadata.run_attempt})`, + `Status: ${ + runMetadata.conclusion + ? `${runMetadata.status}/${runMetadata.conclusion}` + : runMetadata.status + }`, + `Created: ${runMetadata.created_at}`, + `Updated: ${runMetadata.updated_at || 'N/A'}`, + `URL: ${runMetadata.html_url}`, + `Output: ${outputDir}`, + '', + `Jobs considered: ${jobs.length}`, + `Logs downloaded: ${results.filter((result) => result.downloaded).length}`, + `Logs skipped: ${results.filter((result) => result.skippedReason).length}`, + `Log download errors: ${results.filter((result) => result.error).length}`, + '', + 'Jobs:', + '', + ] + + for (const result of results) { + const duration = formatDuration( + result.job.started_at, + result.job.completed_at + ) + lines.push( + `- ${result.job.id} | ${formatStatus(result.job)} | ${duration} | ${result.job.name}` + ) + + if (result.fileName) { + lines.push(` file: ${result.fileName}`) + } + + if (result.skippedReason) { + lines.push(` skipped: ${result.skippedReason}`) + } + + if (result.error) { + lines.push(` error: ${result.error}`) + } + } + + return `${lines.join('\n')}\n` +} + +async function main() { + const args = process.argv.slice(2) + if (args.includes('--help')) { + usage() + return + } + + const waitFlag = args.includes('--wait') + const failedOnly = args.includes('--failed-only') + const prNumberArg = args.find((arg) => !arg.startsWith('--')) + + console.log('Resolving PR...') + const branchInfo = getBranchInfo(prNumberArg) + console.log(`Using PR #${branchInfo.prNumber} (${branchInfo.branchName})`) + + console.log('Finding latest workflow run...') + let runMetadata = getLatestWorkflowRun(branchInfo.branchName) + if (!runMetadata) { + console.error( + `No "${WORKFLOW_NAME}" workflow runs found for branch ${branchInfo.branchName}.` + ) + process.exit(1) + } + + if ( + waitFlag && + (runMetadata.status === 'queued' || runMetadata.status === 'in_progress') + ) { + console.log(`Waiting for run ${runMetadata.id} to complete...`) + try { + execFileSync( + 'gh', + [ + 'run', + 'watch', + String(runMetadata.id), + '--compact', + '-R', + `${OWNER}/${REPO}`, + ], + { stdio: 'inherit' } + ) + } catch { + // gh run watch exits non-zero when the run fails, which is expected here. + } + + runMetadata = getRunMetadata(runMetadata.id) + } + + console.log( + `Fetching jobs for run ${runMetadata.id} (attempt ${runMetadata.run_attempt})...` + ) + const allJobs = getJobsForRunAttempt(runMetadata.id, runMetadata.run_attempt) + const jobs = failedOnly + ? allJobs.filter((job) => FAILED_CONCLUSIONS.has(job.conclusion)) + : allJobs + + const outputDir = buildOutputDir( + branchInfo.prNumber, + runMetadata.id, + runMetadata.run_attempt + ) + await fs.rm(outputDir, { recursive: true, force: true }) + await fs.mkdir(outputDir, { recursive: true }) + + if (jobs.length === 0) { + const indexLog = buildIndexLog(branchInfo, runMetadata, jobs, [], outputDir) + await fs.writeFile(path.join(outputDir, 'index.log'), indexLog) + console.log( + `No matching jobs found. Wrote summary to ${outputDir}/index.log` + ) + return + } + + const results = new Array(jobs.length) + console.log(`Downloading logs for ${jobs.length} job(s)...`) + + await mapLimit( + jobs.map((job, index) => ({ job, index })), + 4, + async (entry) => { + const { job, index } = entry + const fileName = `job-${job.id}-${sanitizeFilename(job.name || 'job')}.log` + const filePath = path.join(outputDir, fileName) + + if (job.status === 'queued') { + results[index] = { + job, + downloaded: false, + skippedReason: 'job is still queued', + } + return + } + + if (job.conclusion === 'skipped') { + results[index] = { + job, + downloaded: false, + skippedReason: 'job was skipped', + } + return + } + + try { + const logs = await downloadJobLog(job.id) + await fs.writeFile(filePath, logs) + results[index] = { + job, + downloaded: true, + fileName, + } + } catch (error) { + const message = + error.stderr || error.message || 'Unknown error while fetching logs' + await fs.writeFile( + filePath, + `Failed to download logs for job ${job.id} (${job.name})\n\n${message}\n` + ) + results[index] = { + job, + downloaded: false, + fileName, + error: message.replace(/\s+/g, ' ').trim(), + } + } + } + ) + + const indexLog = buildIndexLog( + branchInfo, + runMetadata, + jobs, + results, + outputDir + ) + await fs.writeFile(path.join(outputDir, 'index.log'), indexLog) + + console.log(`Done. Logs written to ${outputDir}`) + console.log(`Summary: ${outputDir}/index.log`) +} + +main().catch((error) => { + console.error(error) + process.exit(1) +}) diff --git a/scripts/pr-status.js b/scripts/pr-status.js index 6575e3c3d931..47a0dbb8cff8 100644 --- a/scripts/pr-status.js +++ b/scripts/pr-status.js @@ -2,7 +2,10 @@ const { execSync, execFileSync, spawn } = require('child_process') const fs = require('fs/promises') const path = require('path') -const OUTPUT_DIR = path.join(__dirname, 'pr-status') +const OUTPUT_ROOT = path.join(__dirname, 'pr-status') +const RESULTS_DIR = path.join(OUTPUT_ROOT, 'results') +const INTERMEDIATE_DIR = path.join(OUTPUT_ROOT, 'intermediate') +const ANSI_RE = new RegExp(String.raw`\u001B\[[0-9;]*[A-Za-z]`, 'g') // ============================================================================ // Helper Functions @@ -88,6 +91,14 @@ function sanitizeFilename(name) { .substring(0, 100) } +function resultPath(...segments) { + return path.join(RESULTS_DIR, ...segments) +} + +function intermediatePath(...segments) { + return path.join(INTERMEDIATE_DIR, ...segments) +} + function escapeMarkdownTableCell(text) { if (!text) return '' // Escape pipe characters and newlines for markdown table cells @@ -102,6 +113,34 @@ function stripTimestamps(logContent) { return logContent.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s/gm, '') } +function stripAnsi(text) { + return String(text || '').replace(ANSI_RE, '') +} + +function truncate(text, maxLength) { + const value = String(text || '') + if (value.length <= maxLength) return value + return `${value.substring(0, maxLength)}...` +} + +function normalizeTestPath(testName) { + const normalized = String(testName || '').replace(/\\/g, '/') + const match = normalized.match(/(?:^|\/)(test\/.*)$/) + return match ? match[1] : normalized +} + +function formatFailedAssertionName(assertion) { + const ancestors = assertion?.ancestorTitles || [] + const title = assertion?.title || assertion?.fullName || 'Unknown test' + return [...ancestors, title].filter(Boolean).join(' > ') +} + +function collectFailureMessages(testResult) { + return (testResult?.assertionResults || []) + .flatMap((assertion) => assertion.failureMessages || []) + .join('\n\n') +} + function isBot(username) { if (!username) return false return username.endsWith('-bot') || username.endsWith('[bot]') @@ -131,11 +170,18 @@ function getJobEnvVarsFromWorkflow() { const afterBuild = match[3] const exports = [] for (const line of afterBuild.split('\n')) { - const exportMatch = line.match( - /^\s*export\s+([\w]+)=["']?([^"'\s]+)["']?/ - ) + const exportMatch = line.match(/^\s*export\s+([\w]+)=(.*)$/) if (exportMatch) { - exports.push(`${exportMatch[1]}=${exportMatch[2]}`) + let value = exportMatch[2].trim() + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.substring(1, value.length - 1) + } + if (value) { + exports.push(`${exportMatch[1]}=${value}`) + } } } if (exports.length > 0) { @@ -539,6 +585,82 @@ function extractTestCaseGroups(logContent) { return groups } +function extractStructuredTestFiles(testResults) { + const testsByPath = new Map() + + for (const result of testResults) { + const resultTestFiles = result.testResults || [] + + for (const testResult of resultTestFiles) { + const failedAssertions = (testResult.assertionResults || []) + .filter((assertion) => assertion.status === 'failed') + .map((assertion) => ({ + name: formatFailedAssertionName(assertion), + message: stripAnsi( + (assertion.failureMessages || []).join('\n\n') || testResult.message + ).trim(), + })) + + const failed = + testResult.status === 'failed' || + failedAssertions.length > 0 || + (resultTestFiles.length === 1 && (result.numFailedTests || 0) > 0) + + if (!failed) continue + + const testPath = normalizeTestPath(testResult.name) + if (!testsByPath.has(testPath)) { + testsByPath.set(testPath, { + testPath, + attempts: [], + rawOutput: null, + }) + } + + testsByPath.get(testPath).attempts.push({ + status: testResult.status || 'unknown', + processEnv: result.processEnv || {}, + summary: { + failed: result.numFailedTests || failedAssertions.length, + passed: result.numPassedTests || 0, + total: result.numTotalTests || 0, + }, + message: stripAnsi( + testResult.message || collectFailureMessages(testResult) + ).trim(), + failedAssertions, + }) + } + } + + return [...testsByPath.values()].sort((a, b) => + a.testPath.localeCompare(b.testPath) + ) +} + +function mergeRawTestOutputs(structuredTestFiles, rawTestGroups) { + const testsByPath = new Map( + structuredTestFiles.map((testFile) => [testFile.testPath, testFile]) + ) + + for (const rawGroup of rawTestGroups) { + const existing = testsByPath.get(rawGroup.testPath) + if (existing) { + existing.rawOutput = rawGroup.content + } else { + testsByPath.set(rawGroup.testPath, { + testPath: rawGroup.testPath, + attempts: [], + rawOutput: rawGroup.content, + }) + } + } + + return [...testsByPath.values()].sort((a, b) => + a.testPath.localeCompare(b.testPath) + ) +} + function extractSections(logContent) { // Split the log into sections at ##[group] and ##[endgroup] boundaries const sections = [] @@ -871,7 +993,7 @@ function generateIndexMd( return lines.join('\n') } -function generateJobMd(jobMetadata, testResults, testGroups, sections) { +function generateJobMd(jobMetadata, testResults, testFiles, sections) { const duration = formatDuration( jobMetadata.started_at, jobMetadata.completed_at @@ -898,13 +1020,14 @@ function generateJobMd(jobMetadata, testResults, testGroups, sections) { const sectionNum = i + 1 const filename = `job-${jobMetadata.id}-section-${sectionNum}.txt` const errorPrefix = section.hasError ? '[error] ' : '' + const linkPath = `../intermediate/${filename}` if (section.name) { lines.push( - `- ${errorPrefix}[${section.name} (${section.lineCount} lines)](${filename})` + `- ${errorPrefix}[${section.name} (${section.lineCount} lines)](${linkPath})` ) } else { - lines.push(`- ${errorPrefix}[${section.lineCount} lines](${filename})`) + lines.push(`- ${errorPrefix}[${section.lineCount} lines](${linkPath})`) } } lines.push('') @@ -927,10 +1050,10 @@ function generateJobMd(jobMetadata, testResults, testGroups, sections) { for (const assertion of testResult.assertionResults) { if (assertion.status === 'failed') { allFailedTests.push({ - testFile: testResult.name, - testName: assertion.fullName || assertion.title, + testFile: normalizeTestPath(testResult.name), + testName: formatFailedAssertionName(assertion), error: - assertion.failureMessages?.[0]?.substring(0, 100) || + stripAnsi(assertion.failureMessages?.[0] || '').trim() || 'Unknown', }) } @@ -959,28 +1082,24 @@ function generateJobMd(jobMetadata, testResults, testGroups, sections) { ) for (const test of allFailedTests) { - const shortFile = test.testFile.replace(/.*\/next\.js\/next\.js\//, '') - const shortError = test.error - .replace(/\n/g, ' ') - .substring(0, 60) - .replace(/\|/g, '\\|') + const shortError = truncate( + test.error.replace(/\n/g, ' ').replace(/\|/g, '\\|'), + 60 + ) lines.push( - `| ${escapeMarkdownTableCell(shortFile)} | ${escapeMarkdownTableCell(test.testName)} | ${shortError}... |` + `| ${escapeMarkdownTableCell(test.testFile)} | ${escapeMarkdownTableCell(test.testName)} | ${shortError} |` ) } lines.push('') } } - if (testGroups.length > 0) { + if (testFiles.length > 0) { lines.push('## Individual Test Files', '') - const seenPaths = new Set() - for (const group of testGroups) { - if (seenPaths.has(group.testPath)) continue - seenPaths.add(group.testPath) - const sanitizedName = sanitizeFilename(group.testPath) + for (const testFile of testFiles) { + const sanitizedName = sanitizeFilename(testFile.testPath) lines.push( - `- [${group.testPath}](job-${jobMetadata.id}-test-${sanitizedName}.md)` + `- [${testFile.testPath}](job-${jobMetadata.id}-test-${sanitizedName}.md)` ) } } @@ -988,28 +1107,52 @@ function generateJobMd(jobMetadata, testResults, testGroups, sections) { return lines.join('\n') } -function generateTestMd(jobMetadata, testPath, content, testResultJson) { +function generateTestMd(jobMetadata, testFile) { const lines = [ - `# Test: ${testPath}`, + `# Test: ${testFile.testPath}`, '', `Job: [${jobMetadata.name}](job-${jobMetadata.id}.md)`, '', - '## Output', - '', - '```', - content, - '```', ] - if (testResultJson) { + if (testFile.attempts.length > 0) { lines.push( + '## Structured Results', '', - '## Test Results JSON', - '', - '```json', - JSON.stringify(testResultJson, null, 2), - '```' + `Attempts: ${testFile.attempts.length}`, + '' ) + + for (let i = 0; i < testFile.attempts.length; i++) { + const attempt = testFile.attempts[i] + lines.push(`### Attempt ${i + 1}`, '', `Status: ${attempt.status}`) + + if (attempt.processEnv?.NEXT_TEST_MODE) { + lines.push(`Mode: ${attempt.processEnv.NEXT_TEST_MODE}`) + } + + lines.push( + `Failed: ${attempt.summary.failed}`, + `Passed: ${attempt.summary.passed}`, + `Total: ${attempt.summary.total}`, + '' + ) + + if (attempt.failedAssertions.length > 0) { + for (const assertion of attempt.failedAssertions) { + lines.push(`#### ${assertion.name}`, '') + lines.push('```', assertion.message || 'Unknown failure', '```', '') + } + } else if (attempt.message) { + lines.push('```', attempt.message, '```', '') + } else { + lines.push('_No structured failure details captured._', '') + } + } + } + + if (testFile.rawOutput && testFile.attempts.length === 0) { + lines.push('## Raw Job Output', '', '```', testFile.rawOutput, '```') } return lines.join('\n') @@ -1266,8 +1409,9 @@ async function getFlakyTests(currentBranch, runsToCheck = 5) { async function runAnalysis(prNumberArg, skipFlakyCheck) { // Step 1: Delete and recreate output directory console.log('Cleaning output directory...') - await fs.rm(OUTPUT_DIR, { recursive: true, force: true }) - await fs.mkdir(OUTPUT_DIR, { recursive: true }) + await fs.rm(OUTPUT_ROOT, { recursive: true, force: true }) + await fs.mkdir(RESULTS_DIR, { recursive: true }) + await fs.mkdir(INTERMEDIATE_DIR, { recursive: true }) // Step 2: Get branch info console.log('Getting branch info...') @@ -1353,7 +1497,7 @@ async function runAnalysis(prNumberArg, skipFlakyCheck) { for (let i = 0; i < reviewData.reviewThreads.length; i++) { const thread = reviewData.reviewThreads[i] await fs.writeFile( - path.join(OUTPUT_DIR, `thread-${i + 1}.md`), + resultPath(`thread-${i + 1}.md`), generateThreadMd(thread, i) ) } @@ -1361,7 +1505,7 @@ async function runAnalysis(prNumberArg, skipFlakyCheck) { for (const review of reviewData.reviews) { if (review.body && review.body.trim()) { await fs.writeFile( - path.join(OUTPUT_DIR, `review-${review.id}.md`), + resultPath(`review-${review.id}.md`), generateReviewMd(review) ) } @@ -1369,7 +1513,7 @@ async function runAnalysis(prNumberArg, skipFlakyCheck) { // Write individual comment files for (const comment of reviewData.prComments) { await fs.writeFile( - path.join(OUTPUT_DIR, `comment-${comment.id}.md`), + resultPath(`comment-${comment.id}.md`), generateCommentMd(comment) ) } @@ -1384,7 +1528,7 @@ async function runAnalysis(prNumberArg, skipFlakyCheck) { skipped: [], } await fs.writeFile( - path.join(OUTPUT_DIR, 'index.md'), + resultPath('index.md'), generateIndexMd( branchInfo, runMetadata, @@ -1440,36 +1584,30 @@ async function runAnalysis(prNumberArg, skipFlakyCheck) { const section = sections[i] const sectionNum = i + 1 await fs.writeFile( - path.join(OUTPUT_DIR, `job-${id}-section-${sectionNum}.txt`), + intermediatePath(`job-${id}-section-${sectionNum}.txt`), section.content ) } // Extract test case groups - const testGroups = extractTestCaseGroups(logs) + const testFiles = mergeRawTestOutputs( + extractStructuredTestFiles(testResults), + extractTestCaseGroups(logs) + ) // Write individual test files - for (const group of testGroups) { - const sanitizedName = sanitizeFilename(group.testPath) - // Find matching test result JSON for this test - const matchingResult = testResults.find((r) => - r.testResults?.some((tr) => tr.name?.includes(group.testPath)) - ) - const testMd = generateTestMd( - jobMetadata, - group.testPath, - group.content, - matchingResult - ) + for (const testFile of testFiles) { + const sanitizedName = sanitizeFilename(testFile.testPath) + const testMd = generateTestMd(jobMetadata, testFile) await fs.writeFile( - path.join(OUTPUT_DIR, `job-${id}-test-${sanitizedName}.md`), + resultPath(`job-${id}-test-${sanitizedName}.md`), testMd ) } // Generate job markdown - const jobMd = generateJobMd(jobMetadata, testResults, testGroups, sections) - await fs.writeFile(path.join(OUTPUT_DIR, `job-${id}.md`), jobMd) + const jobMd = generateJobMd(jobMetadata, testResults, testFiles, sections) + await fs.writeFile(resultPath(`job-${id}.md`), jobMd) } // Step 7: Write PR review files if we have PR data @@ -1479,7 +1617,7 @@ async function runAnalysis(prNumberArg, skipFlakyCheck) { for (let i = 0; i < reviewData.reviewThreads.length; i++) { const thread = reviewData.reviewThreads[i] await fs.writeFile( - path.join(OUTPUT_DIR, `thread-${i + 1}.md`), + resultPath(`thread-${i + 1}.md`), generateThreadMd(thread, i) ) } @@ -1487,7 +1625,7 @@ async function runAnalysis(prNumberArg, skipFlakyCheck) { for (const review of reviewData.reviews) { if (review.body?.trim()) { await fs.writeFile( - path.join(OUTPUT_DIR, `review-${review.id}.md`), + resultPath(`review-${review.id}.md`), generateReviewMd(review) ) } @@ -1495,7 +1633,7 @@ async function runAnalysis(prNumberArg, skipFlakyCheck) { // Write individual comment files for (const comment of reviewData.prComments) { await fs.writeFile( - path.join(OUTPUT_DIR, `comment-${comment.id}.md`), + resultPath(`comment-${comment.id}.md`), generateCommentMd(comment) ) } @@ -1507,7 +1645,7 @@ async function runAnalysis(prNumberArg, skipFlakyCheck) { flakyTests = await getFlakyTests(branchInfo.branchName, 5) if (flakyTests.size > 0) { await fs.writeFile( - path.join(OUTPUT_DIR, 'flaky-tests.json'), + resultPath('flaky-tests.json'), JSON.stringify([...flakyTests].sort(), null, 2) ) } @@ -1530,9 +1668,9 @@ async function runAnalysis(prNumberArg, skipFlakyCheck) { jobEnvMap, flakyTests ) - await fs.writeFile(path.join(OUTPUT_DIR, 'index.md'), indexMd) + await fs.writeFile(resultPath('index.md'), indexMd) - console.log(`\nDone! Output written to ${OUTPUT_DIR}/index.md`) + console.log(`\nDone! Output written to ${RESULTS_DIR}/index.md`) return { runId: latestRun.id, isRunInProgress } } diff --git a/scripts/run-for-change.mjs b/scripts/run-for-change.mjs index c0b6daa726fd..f5cd05acac34 100644 --- a/scripts/run-for-change.mjs +++ b/scripts/run-for-change.mjs @@ -42,7 +42,7 @@ const CHANGE_ITEM_GROUPS = { ], cna: [ 'packages/create-next-app', - 'test/integration/create-next-app', + 'test/production/create-next-app', 'examples/basic-css', 'examples/mdx-pages', 'examples/with-sass', @@ -52,7 +52,7 @@ const CHANGE_ITEM_GROUPS = { 'next-swc': [ 'packages/next-swc', 'scripts/normalize-version-bump.js', - 'test/integration/create-next-app', + 'test/production/create-next-app', 'scripts/send-trace-to-jaeger', ], } diff --git a/test/cache-components-tests-manifest.json b/test/cache-components-tests-manifest.json index 42895abcba17..e2375821ad08 100644 --- a/test/cache-components-tests-manifest.json +++ b/test/cache-components-tests-manifest.json @@ -320,6 +320,8 @@ "test/e2e/new-link-behavior/material-ui.test.ts", "test/e2e/next-form/default/app-dir.test.ts", "test/e2e/next-form/default/pages-dir.test.ts", + "test/e2e/next-image-new/app-dir/app-dir-static.test.ts", + "test/e2e/next-image-new/app-dir/app-dir.test.ts", "test/e2e/next-link-errors/next-link-errors.test.ts", "test/e2e/nonce-head-manager/index.test.ts", "test/e2e/og-api/index.test.ts", @@ -337,6 +339,7 @@ "test/e2e/react-version/react-version.test.ts", "test/e2e/streaming-ssr-edge/streaming-ssr-edge.test.ts", "test/e2e/switchable-runtime/index.test.ts", + "test/e2e/telemetry/page-features.test.ts", "test/e2e/testmode/testmode.test.ts", "test/e2e/use-link-status/index.test.ts", "test/e2e/url/url.test.ts", @@ -345,6 +348,7 @@ "test/integration/app-types/app-types.test.ts", "test/production/app-dir-edge-runtime-with-wasm/index.test.ts", "test/production/app-dir-prevent-304-caching/index.test.ts", + "test/production/app-dynamic-error/app-dynamic-error.test.ts", "test/production/app-dir/actions-tree-shaking/basic/basic-edge.test.ts", "test/production/app-dir/actions-tree-shaking/mixed-module-actions/mixed-module-actions-edge.test.ts", "test/production/app-dir/actions-tree-shaking/reexport/reexport-edge.test.ts", diff --git a/test/development/app-aspath/app-aspath.test.ts b/test/development/app-aspath/app-aspath.test.ts new file mode 100644 index 000000000000..443b902c9b21 --- /dev/null +++ b/test/development/app-aspath/app-aspath.test.ts @@ -0,0 +1,32 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('App asPath', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should not have any changes in asPath after a bundle rebuild', async () => { + const browser = await next.browser('/') + + const text = await browser.elementByCss('body').text() + expect(text).toBe( + '{ "url": { "query": {}, "pathname": "/", "asPath": "/" } }' + ) + + const originalContent = await next.readFile('pages/_app.js') + const editedContent = originalContent.replace( + 'find this', + 'replace with this' + ) + + await next.patchFile('pages/_app.js', editedContent) + + await retry(async () => { + const newContent = await browser.elementByCss('body').text() + expect(newContent).toBe( + '{ "url": { "query": {}, "pathname": "/", "asPath": "/" } }' + ) + }) + }) +}) diff --git a/test/integration/app-aspath/pages/_app.js b/test/development/app-aspath/pages/_app.js similarity index 100% rename from test/integration/app-aspath/pages/_app.js rename to test/development/app-aspath/pages/_app.js diff --git a/test/integration/app-aspath/pages/index.js b/test/development/app-aspath/pages/index.js similarity index 100% rename from test/integration/app-aspath/pages/index.js rename to test/development/app-aspath/pages/index.js diff --git a/test/development/app-config-asset-prefix/app-config-asset-prefix.test.ts b/test/development/app-config-asset-prefix/app-config-asset-prefix.test.ts new file mode 100644 index 000000000000..5260907cf8e3 --- /dev/null +++ b/test/development/app-config-asset-prefix/app-config-asset-prefix.test.ts @@ -0,0 +1,15 @@ +import { nextTestSetup } from 'e2e-utils' +import { waitForNoRedbox } from 'next-test-utils' + +describe('App assetPrefix config', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should render correctly with assetPrefix: "/"', async () => { + const browser = await next.browser('/') + await waitForNoRedbox(browser) + const title = await browser.elementById('title').text() + expect(title).toBe('IndexPage') + }) +}) diff --git a/test/integration/app-config-asset-prefix/app/layout.js b/test/development/app-config-asset-prefix/app/layout.js similarity index 100% rename from test/integration/app-config-asset-prefix/app/layout.js rename to test/development/app-config-asset-prefix/app/layout.js diff --git a/test/integration/app-config-asset-prefix/app/page.js b/test/development/app-config-asset-prefix/app/page.js similarity index 100% rename from test/integration/app-config-asset-prefix/app/page.js rename to test/development/app-config-asset-prefix/app/page.js diff --git a/test/integration/app-config-asset-prefix/next.config.js b/test/development/app-config-asset-prefix/next.config.js similarity index 100% rename from test/integration/app-config-asset-prefix/next.config.js rename to test/development/app-config-asset-prefix/next.config.js diff --git a/test/integration/app-document-add-hmr/test/index.test.ts b/test/development/app-document-add-hmr/app-document-add-hmr.test.ts similarity index 56% rename from test/integration/app-document-add-hmr/test/index.test.ts rename to test/development/app-document-add-hmr/app-document-add-hmr.test.ts index b82515ffff70..df90388941d7 100644 --- a/test/integration/app-document-add-hmr/test/index.test.ts +++ b/test/development/app-document-add-hmr/app-document-add-hmr.test.ts @@ -1,34 +1,21 @@ -/* eslint-env jest */ - -import fs from 'fs-extra' -import { join } from 'path' -import webdriver from 'next-webdriver' -import { killApp, findPort, launchApp, check } from 'next-test-utils' - -const appDir = join(__dirname, '../') -const appPage = join(appDir, 'pages/_app.js') -const documentPage = join(appDir, 'pages/_document.js') - -let appPort -let app +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' describe('_app/_document add HMR', () => { - beforeAll(async () => { - appPort = await findPort() - app = await launchApp(appDir, appPort) + const { next } = nextTestSetup({ + files: __dirname, }) - afterAll(() => killApp(app)) // TODO: figure out why test fails. it.skip('should HMR when _app is added', async () => { - const browser = await webdriver(appPort, '/') + const browser = await next.browser('/') try { const html = await browser.eval('document.documentElement.innerHTML') expect(html).not.toContain('custom _app') expect(html).toContain('index page') - await fs.writeFile( - appPage, + await next.patchFile( + 'pages/_app.js', ` export default function MyApp({ Component, pageProps }) { return ( @@ -41,33 +28,31 @@ describe('_app/_document add HMR', () => { ` ) - await check(async () => { + await retry(async () => { const html = await browser.eval('document.documentElement.innerHTML') - return html.includes('custom _app') && html.includes('index page') - ? 'success' - : html - }, 'success') + expect(html).toContain('custom _app') + expect(html).toContain('index page') + }) } finally { - await fs.remove(appPage) - await check(async () => { + await next.deleteFile('pages/_app.js') + await retry(async () => { const html = await browser.eval('document.documentElement.innerHTML') - return !html.includes('custom _app') && html.includes('index page') - ? 'restored' - : html - }, 'restored') + expect(html).not.toContain('custom _app') + expect(html).toContain('index page') + }) } }) // TODO: Figure out why test fails. it.skip('should HMR when _document is added', async () => { - const browser = await webdriver(appPort, '/') + const browser = await next.browser('/') try { const html = await browser.eval('document.documentElement.innerHTML') expect(html).not.toContain('custom _document') expect(html).toContain('index page') - await fs.writeFile( - documentPage, + await next.patchFile( + 'pages/_document.js', ` import Document, { Html, Head, Main, NextScript } from 'next/document' @@ -92,24 +77,21 @@ describe('_app/_document add HMR', () => { } export default MyDocument - ` ) - await check(async () => { + await retry(async () => { const html = await browser.eval('document.documentElement.innerHTML') - return html.includes('custom _document') && html.includes('index page') - ? 'success' - : html - }, 'success') + expect(html).toContain('custom _document') + expect(html).toContain('index page') + }) } finally { - await fs.remove(documentPage) - await check(async () => { + await next.deleteFile('pages/_document.js') + await retry(async () => { const html = await browser.eval('document.documentElement.innerHTML') - return !html.includes('custom _document') && html.includes('index page') - ? 'restored' - : html - }, 'restored') + expect(html).not.toContain('custom _document') + expect(html).toContain('index page') + }) } }) }) diff --git a/test/integration/app-document-add-hmr/pages/index.js b/test/development/app-document-add-hmr/pages/index.js similarity index 100% rename from test/integration/app-document-add-hmr/pages/index.js rename to test/development/app-document-add-hmr/pages/index.js diff --git a/test/development/app-document-remove-hmr/app-document-remove-hmr.test.ts b/test/development/app-document-remove-hmr/app-document-remove-hmr.test.ts new file mode 100644 index 000000000000..8a03dc6a4322 --- /dev/null +++ b/test/development/app-document-remove-hmr/app-document-remove-hmr.test.ts @@ -0,0 +1,96 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('_app/_document removal HMR', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should HMR when _app is removed', async () => { + const indexContent = await next.readFile('pages/index.js') + try { + const browser = await next.browser('/') + + const html = await browser.eval('document.documentElement.innerHTML') + expect(html).toContain('custom _app') + + const appContent = await next.readFile('pages/_app.js') + await next.deleteFile('pages/_app.js') + + await retry(async () => { + const html = await browser.eval('document.documentElement.innerHTML') + expect(html).toContain('index page') + expect(html).not.toContain('custom _app') + }) + + await next.patchFile( + 'pages/index.js', + ` + export default function Page() { + return

index page updated

+ } + ` + ) + + await retry(async () => { + const html = await browser.eval('document.documentElement.innerHTML') + expect(html).toContain('index page updated') + expect(html).not.toContain('custom _app') + }) + + await next.patchFile('pages/_app.js', appContent) + + await retry(async () => { + const html = await browser.eval('document.documentElement.innerHTML') + expect(html).toContain('index page updated') + expect(html).toContain('custom _app') + }) + } finally { + await next.patchFile('pages/index.js', indexContent) + } + }) + + it('should HMR when _document is removed', async () => { + const indexContent = await next.readFile('pages/index.js') + try { + const browser = await next.browser('/') + + const html = await browser.eval('document.documentElement.innerHTML') + expect(html).toContain('custom _document') + + const documentContent = await next.readFile('pages/_document.js') + await next.deleteFile('pages/_document.js') + + await retry(async () => { + const html = await browser.eval('document.documentElement.innerHTML') + expect(html).toContain('index page') + expect(html).not.toContain('custom _document') + }) + + await next.patchFile( + 'pages/index.js', + ` + export default function Page() { + return

index page updated

+ } + ` + ) + + await retry(async () => { + const html = await browser.eval('document.documentElement.innerHTML') + expect(html).toContain('index page updated') + expect(html).not.toContain('custom _document') + }) + + await next.patchFile('pages/_document.js', documentContent) + + await retry(async () => { + const html = await browser.eval('document.documentElement.innerHTML') + expect(html).toContain('index page updated') + expect(html).toContain('custom _document') + }) + } finally { + await next.patchFile('pages/index.js', indexContent) + } + }) +}) diff --git a/test/integration/app-document-remove-hmr/pages/_app.js b/test/development/app-document-remove-hmr/pages/_app.js similarity index 100% rename from test/integration/app-document-remove-hmr/pages/_app.js rename to test/development/app-document-remove-hmr/pages/_app.js diff --git a/test/integration/app-document-remove-hmr/pages/_document.js b/test/development/app-document-remove-hmr/pages/_document.js similarity index 100% rename from test/integration/app-document-remove-hmr/pages/_document.js rename to test/development/app-document-remove-hmr/pages/_document.js diff --git a/test/integration/app-document-remove-hmr/pages/index.js b/test/development/app-document-remove-hmr/pages/index.js similarity index 100% rename from test/integration/app-document-remove-hmr/pages/index.js rename to test/development/app-document-remove-hmr/pages/index.js diff --git a/test/development/app-functional/app-functional.test.ts b/test/development/app-functional/app-functional.test.ts new file mode 100644 index 000000000000..f989a14a74ed --- /dev/null +++ b/test/development/app-functional/app-functional.test.ts @@ -0,0 +1,12 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('Document and App', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should not have any missing key warnings', async () => { + const html = await next.render('/') + expect(html).toMatch(/
Hello World!!!<\/div>/) + }) +}) diff --git a/test/integration/app-functional/next.config.js b/test/development/app-functional/next.config.js similarity index 100% rename from test/integration/app-functional/next.config.js rename to test/development/app-functional/next.config.js diff --git a/test/integration/app-functional/pages/_app.js b/test/development/app-functional/pages/_app.js similarity index 100% rename from test/integration/app-functional/pages/_app.js rename to test/development/app-functional/pages/_app.js diff --git a/test/integration/app-functional/pages/index.js b/test/development/app-functional/pages/index.js similarity index 100% rename from test/integration/app-functional/pages/index.js rename to test/development/app-functional/pages/index.js diff --git a/test/integration/app-functional/shared-module.js b/test/development/app-functional/shared-module.js similarity index 100% rename from test/integration/app-functional/shared-module.js rename to test/development/app-functional/shared-module.js diff --git a/test/integration/babel-next-image/.babelrc b/test/development/babel-next-image/.babelrc similarity index 100% rename from test/integration/babel-next-image/.babelrc rename to test/development/babel-next-image/.babelrc diff --git a/test/integration/babel-next-image/app/layout.js b/test/development/babel-next-image/app/layout.js similarity index 100% rename from test/integration/babel-next-image/app/layout.js rename to test/development/babel-next-image/app/layout.js diff --git a/test/integration/babel-next-image/app/page.js b/test/development/babel-next-image/app/page.js similarity index 100% rename from test/integration/babel-next-image/app/page.js rename to test/development/babel-next-image/app/page.js diff --git a/test/development/babel-next-image/babel-next-image.test.ts b/test/development/babel-next-image/babel-next-image.test.ts new file mode 100644 index 000000000000..3b5de9c59f00 --- /dev/null +++ b/test/development/babel-next-image/babel-next-image.test.ts @@ -0,0 +1,12 @@ +import { nextTestSetup } from 'e2e-utils' +;(process.env.IS_TURBOPACK_TEST ? describe.skip : describe)( + 'babel-next-image', + () => { + const { next } = nextTestSetup({ files: __dirname }) + + it('should work with babel and next/image', async () => { + const res = await next.fetch('/') + expect(res.status).toBe(200) + }) + } +) diff --git a/test/development/broken-webpack-plugin/broken-webpack-plugin.test.ts b/test/development/broken-webpack-plugin/broken-webpack-plugin.test.ts new file mode 100644 index 000000000000..4cbba1465b03 --- /dev/null +++ b/test/development/broken-webpack-plugin/broken-webpack-plugin.test.ts @@ -0,0 +1,22 @@ +import { nextTestSetup } from 'e2e-utils' + +// The isolated test install for this suite (fresh `pnpm install` of a packed +// Next.js tarball + starting `next dev`) can exceed the default 120s jest +// beforeAll timeout on cold caches. +jest.setTimeout(5 * 60 * 1000) + +// Skipped for Turbopack as this test is webpack-specific +;(process.env.IS_TURBOPACK_TEST ? describe.skip : describe)( + 'Handles a broken webpack plugin (precompile)', + () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should render error correctly', async () => { + const text = await next.render('/') + expect(text).toContain('Internal Server Error') + expect(next.cliOutput).toMatch('Error: oops') + }) + } +) diff --git a/test/integration/broken-webpack-plugin/next.config.js b/test/development/broken-webpack-plugin/next.config.js similarity index 100% rename from test/integration/broken-webpack-plugin/next.config.js rename to test/development/broken-webpack-plugin/next.config.js diff --git a/test/integration/broken-webpack-plugin/pages/index.js b/test/development/broken-webpack-plugin/pages/index.js similarity index 100% rename from test/integration/broken-webpack-plugin/pages/index.js rename to test/development/broken-webpack-plugin/pages/index.js diff --git a/test/integration/client-navigation-a11y/test/index.test.ts b/test/development/client-navigation-a11y/client-navigation-a11y.test.ts similarity index 57% rename from test/integration/client-navigation-a11y/test/index.test.ts rename to test/development/client-navigation-a11y/client-navigation-a11y.test.ts index b4d7d0ddfdae..6ed79a2046f0 100644 --- a/test/integration/client-navigation-a11y/test/index.test.ts +++ b/test/development/client-navigation-a11y/client-navigation-a11y.test.ts @@ -1,56 +1,34 @@ -/* eslint-env jest */ - -import { - findPort, - killApp, - launchApp, - renderViaHTTP, -} from '../../../lib/next-test-utils' -import webdriver from 'next-webdriver' -import { join } from 'path' - -const context: Record = {} -const appDir = join(__dirname, '../') - -const navigateTo = async (browser, selector) => - await browser - .waitForElementByCss('#' + selector + '-link') - .click() - .waitForElementByCss('#' + selector) - -const getAnnouncedTitle = async (browser) => - await browser.waitForElementByCss('#__next-route-announcer__').text() - -const getDocumentTitle = async (browser) => await browser.eval('document.title') - -const getMainHeadingTitle = async (browser) => - await browser.elementByCss('h1').text() +import { nextTestSetup } from 'e2e-utils' describe('Client Navigation accessibility', () => { - beforeAll(async () => { - context.appPort = await findPort() - context.server = await launchApp(appDir, context.appPort) + const { next } = nextTestSetup({ + files: __dirname, + }) - const prerender = [ - '/page-with-h1-and-title, /page-with-h1, /page-with-title, /page-without-h1-or-title', - ] + const navigateTo = async (browser: any, selector: string) => + await browser + .waitForElementByCss('#' + selector + '-link') + .click() + .waitForElementByCss('#' + selector) - await Promise.all( - prerender.map((route) => renderViaHTTP(context.appPort, route)) - ) - }) + const getAnnouncedTitle = async (browser: any) => + await browser.waitForElementByCss('#__next-route-announcer__').text() + + const getDocumentTitle = async (browser: any) => + await browser.eval('document.title') - afterAll(() => killApp(context.server)) + const getMainHeadingTitle = async (browser: any) => + await browser.elementByCss('h1').text() describe('', () => { it('should not have the initial route announced', async () => { - const browser = await webdriver(context.appPort, '/') + const browser = await next.browser('/') const title = await getAnnouncedTitle(browser) expect(title).toBe('') }) it('has aria-live="assertive" and role="alert"', async () => { - const browser = await webdriver(context.appPort, '/') + const browser = await next.browser('/') const routeAnnouncer = await browser.waitForElementByCss( '#__next-route-announcer__' ) @@ -59,58 +37,53 @@ describe('Client Navigation accessibility', () => { expect(ariaLiveValue).toBe('assertive') expect(roleValue).toBe('alert') - await browser.close() }) describe('There is a title but no h1 tag', () => { it('has the innerText equal to the value of document.title', async () => { - const browser = await webdriver(context.appPort, '/') + const browser = await next.browser('/') await navigateTo(browser, 'page-with-title') const routeAnnouncerValue = await getAnnouncedTitle(browser) const title = await getDocumentTitle(browser) expect(routeAnnouncerValue).toBe(title) - await browser.close() }) }) describe('There is no title but a h1 tag', () => { it('has the innerText equal to the value of h1', async () => { - const browser = await webdriver(context.appPort, '/') + const browser = await next.browser('/') await navigateTo(browser, 'page-with-h1') const routeAnnouncerValue = await getAnnouncedTitle(browser) const h1Value = await getMainHeadingTitle(browser) expect(routeAnnouncerValue).toBe(h1Value) - await browser.close() }) }) describe('There is a title and a h1 tag', () => { it('has the innerText equal to the value of h1', async () => { - const browser = await webdriver(context.appPort, '/') + const browser = await next.browser('/') await navigateTo(browser, 'page-with-h1-and-title') const routeAnnouncerValue = await getAnnouncedTitle(browser) const title = await getDocumentTitle(browser) expect(routeAnnouncerValue).toBe(title) - await browser.close() }) }) describe('There is no title and no h1 tag', () => { it('has the innerText equal to the value of the pathname', async () => { - const browser = await webdriver(context.appPort, '/') + const browser = await next.browser('/') await navigateTo(browser, 'page-without-h1-or-title') const routeAnnouncerValue = await getAnnouncedTitle(browser) const pathname = '/page-without-h1-or-title' expect(routeAnnouncerValue).toBe(pathname) - await browser.close() }) }) }) diff --git a/test/integration/client-navigation-a11y/pages/index.js b/test/development/client-navigation-a11y/pages/index.js similarity index 100% rename from test/integration/client-navigation-a11y/pages/index.js rename to test/development/client-navigation-a11y/pages/index.js diff --git a/test/integration/client-navigation-a11y/pages/page-with-h1-and-title.js b/test/development/client-navigation-a11y/pages/page-with-h1-and-title.js similarity index 100% rename from test/integration/client-navigation-a11y/pages/page-with-h1-and-title.js rename to test/development/client-navigation-a11y/pages/page-with-h1-and-title.js diff --git a/test/integration/client-navigation-a11y/pages/page-with-h1.js b/test/development/client-navigation-a11y/pages/page-with-h1.js similarity index 100% rename from test/integration/client-navigation-a11y/pages/page-with-h1.js rename to test/development/client-navigation-a11y/pages/page-with-h1.js diff --git a/test/integration/client-navigation-a11y/pages/page-with-title.js b/test/development/client-navigation-a11y/pages/page-with-title.js similarity index 100% rename from test/integration/client-navigation-a11y/pages/page-with-title.js rename to test/development/client-navigation-a11y/pages/page-with-title.js diff --git a/test/integration/client-navigation-a11y/pages/page-without-h1-or-title.js b/test/development/client-navigation-a11y/pages/page-without-h1-or-title.js similarity index 100% rename from test/integration/client-navigation-a11y/pages/page-without-h1-or-title.js rename to test/development/client-navigation-a11y/pages/page-without-h1-or-title.js diff --git a/test/development/compression/compression.test.ts b/test/development/compression/compression.test.ts new file mode 100644 index 000000000000..9459e197bbf1 --- /dev/null +++ b/test/development/compression/compression.test.ts @@ -0,0 +1,13 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('Compression', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should compress responses by default', async () => { + const res = await next.fetch('/') + + expect(res.headers.get('content-encoding')).toMatch(/gzip/) + }) +}) diff --git a/test/integration/compression/pages/index.js b/test/development/compression/pages/index.js similarity index 100% rename from test/integration/compression/pages/index.js rename to test/development/compression/pages/index.js diff --git a/test/integration/config-devtool-dev/test/index.test.ts b/test/development/config-devtool-dev/config-devtool-dev.test.ts similarity index 51% rename from test/integration/config-devtool-dev/test/index.test.ts rename to test/development/config-devtool-dev/config-devtool-dev.test.ts index fd39d0893c25..0d03ea9ed51a 100644 --- a/test/integration/config-devtool-dev/test/index.test.ts +++ b/test/development/config-devtool-dev/config-devtool-dev.test.ts @@ -1,41 +1,22 @@ -/* eslint-env jest */ +import { nextTestSetup } from 'e2e-utils' +import { retry, waitForRedbox, getRedboxSource } from 'next-test-utils' -import { - waitForRedbox, - findPort, - getRedboxSource, - killApp, - launchApp, - retry, -} from 'next-test-utils' -import webdriver from 'next-webdriver' -import { join } from 'path' - -const appDir = join(__dirname, '../') - -// Webpack specific config tests. +// Webpack specific config test. ;(process.env.IS_TURBOPACK_TEST ? describe.skip : describe)( 'devtool set in development mode in next config', () => { - it('should warn and revert when a devtool is set in development mode', async () => { - let stderr = '' - - const appPort = await findPort() - const app = await launchApp(appDir, appPort, { - onStderr(msg) { - stderr += msg || '' - }, - }) + const { next } = nextTestSetup({ + files: __dirname, + }) + it('should warn and revert when a devtool is set in development mode', async () => { await retry(async () => { - expect(stderr).toMatch(/Reverting webpack devtool to /) + expect(next.cliOutput).toMatch(/Reverting webpack devtool to /) }) - const browser = await webdriver(appPort, '/') + const browser = await next.browser('/') await waitForRedbox(browser) - if (process.platform === 'win32') { - // TODO: add win32 snapshot - } else { + if (process.platform !== 'win32') { expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` "pages/index.js (5:11) @ Index.useEffect @@ -49,8 +30,6 @@ const appDir = join(__dirname, '../') `) } await browser.close() - - await killApp(app) }) } ) diff --git a/test/integration/config-devtool-dev/next.config.js b/test/development/config-devtool-dev/next.config.js similarity index 100% rename from test/integration/config-devtool-dev/next.config.js rename to test/development/config-devtool-dev/next.config.js diff --git a/test/integration/config-devtool-dev/pages/index.js b/test/development/config-devtool-dev/pages/index.js similarity index 100% rename from test/integration/config-devtool-dev/pages/index.js rename to test/development/config-devtool-dev/pages/index.js diff --git a/test/integration/config-mjs/.gitignore b/test/development/config-mjs/.gitignore similarity index 100% rename from test/integration/config-mjs/.gitignore rename to test/development/config-mjs/.gitignore diff --git a/test/integration/config-mjs/components/hello-webpack-css.css b/test/development/config-mjs/components/hello-webpack-css.css similarity index 100% rename from test/integration/config-mjs/components/hello-webpack-css.css rename to test/development/config-mjs/components/hello-webpack-css.css diff --git a/test/integration/config-mjs/components/hello-webpack-css.js b/test/development/config-mjs/components/hello-webpack-css.js similarity index 100% rename from test/integration/config-mjs/components/hello-webpack-css.js rename to test/development/config-mjs/components/hello-webpack-css.js diff --git a/test/integration/config-mjs/components/hello-webpack-sass.scss b/test/development/config-mjs/components/hello-webpack-sass.scss similarity index 100% rename from test/integration/config-mjs/components/hello-webpack-sass.scss rename to test/development/config-mjs/components/hello-webpack-sass.scss diff --git a/test/development/config-mjs/config-mjs.test.ts b/test/development/config-mjs/config-mjs.test.ts new file mode 100644 index 000000000000..b5ee420b4d0e --- /dev/null +++ b/test/development/config-mjs/config-mjs.test.ts @@ -0,0 +1,23 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('Configuration with next.config.mjs', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should disable X-Powered-By header support', async () => { + const res = await next.fetch('/') + expect(res.headers.get('X-Powered-By')).not.toBe('Next.js') + }) + + it('correctly imports a package that defines `module` but no `main` in package.json', async () => { + const $ = await next.render$('/module-only-content') + expect($('#messageInAPackage').text()).toBe('OK') + }) + + it('should have env variables available on the client', async () => { + const browser = await next.browser('/next-config') + const envValue = await browser.elementByCss('#env').text() + expect(envValue).toBe('hello') + }) +}) diff --git a/test/integration/config-mjs/next.config.mjs b/test/development/config-mjs/next.config.mjs similarity index 100% rename from test/integration/config-mjs/next.config.mjs rename to test/development/config-mjs/next.config.mjs diff --git a/test/integration/config-mjs/node_modules/css-framework/framework.css b/test/development/config-mjs/node_modules/css-framework/framework.css similarity index 100% rename from test/integration/config-mjs/node_modules/css-framework/framework.css rename to test/development/config-mjs/node_modules/css-framework/framework.css diff --git a/test/integration/config-mjs/node_modules/module-only-package/modern.js b/test/development/config-mjs/node_modules/module-only-package/modern.js similarity index 100% rename from test/integration/config-mjs/node_modules/module-only-package/modern.js rename to test/development/config-mjs/node_modules/module-only-package/modern.js diff --git a/test/integration/config-mjs/node_modules/module-only-package/package.json b/test/development/config-mjs/node_modules/module-only-package/package.json similarity index 100% rename from test/integration/config-mjs/node_modules/module-only-package/package.json rename to test/development/config-mjs/node_modules/module-only-package/package.json diff --git a/test/integration/config-mjs/pages/module-only-content.js b/test/development/config-mjs/pages/module-only-content.js similarity index 100% rename from test/integration/config-mjs/pages/module-only-content.js rename to test/development/config-mjs/pages/module-only-content.js diff --git a/test/integration/config-mjs/pages/next-config.js b/test/development/config-mjs/pages/next-config.js similarity index 100% rename from test/integration/config-mjs/pages/next-config.js rename to test/development/config-mjs/pages/next-config.js diff --git a/test/development/config-output-export/config-output-export.test.ts b/test/development/config-output-export/config-output-export.test.ts new file mode 100644 index 000000000000..e83e388240a5 --- /dev/null +++ b/test/development/config-output-export/config-output-export.test.ts @@ -0,0 +1,424 @@ +import { nextTestSetup } from 'e2e-utils' +import { + waitForRedbox, + waitForNoRedbox, + getRedboxHeader, + retry, +} from 'next-test-utils' + +const reactDependencies = { + react: '19.3.0-canary-fef12a01-20260413', + 'react-dom': '19.3.0-canary-fef12a01-20260413', +} + +const blogIsr = `export default function Blog({ posts }) { + return posts.map(p => (
{p}
)) + } + + export async function getStaticProps() { + return { + props: { posts: ["my isr post"] }, + revalidate: 10, + } + }` + +const blogGspRevalidateFalse = `export default function Blog({ posts }) { + return posts.map(p => (
{p}
)) + } + + export async function getStaticProps() { + return { + props: { posts: ["my gsp post"] }, + revalidate: false, + } + }` + +const blogGspNoRevalidate = `export default function Blog({ posts }) { + return posts.map(p => (
{p}
)) + } + + export async function getStaticProps() { + return { + props: { posts: ["my gsp post"] }, + } + }` + +const blogGssp = `export default function Blog({ posts }) { + return posts.map(p => (
{p}
)) + } + + export async function getServerSideProps() { + return { + props: { posts: ["my ssr post"] }, + } + }` + +const postsSlug = (fallback: string) => + `export default function Post(props) { + return

Hello from {props.slug}

+ } + + export async function getStaticPaths({ params }) { + return { + paths: [ + { params: { slug: 'one' } }, + ], + fallback: ${fallback}, + } + } + + export async function getStaticProps({ params }) { + return { + props: { slug: params.slug }, + } + }` + +describe('config-output-export', () => { + describe('static homepage', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: reactDependencies, + }) + + it('should work with static homepage', async () => { + const response = await next.fetch('/') + expect(response.status).toBe(200) + expect(await response.text()).toContain( + '
Hello World
' + ) + }) + }) + + describe('"i18n" config', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: reactDependencies, + overrideFiles: { + 'next.config.js': `module.exports = ${JSON.stringify({ + output: 'export', + i18n: { locales: ['en'], defaultLocale: 'en' }, + })}`, + }, + }) + + it('should error with "i18n" config', async () => { + await retry(async () => { + expect(next.cliOutput).toContain( + 'Specified "i18n" cannot be used with "output: export".' + ) + }) + }) + }) + + describe('when hasNextSupport = false', () => { + describe('"rewrites" config', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: reactDependencies, + overrideFiles: { + 'next.config.js': `module.exports = ${JSON.stringify({ + output: 'export', + rewrites: [{ source: '/from', destination: '/to' }], + })}`, + }, + }) + + it('should error with "rewrites" config', async () => { + await retry(async () => { + expect(next.cliOutput).toContain( + 'Specified "rewrites" will not automatically work with "output: export".' + ) + }) + }) + }) + + describe('"redirects" config', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: reactDependencies, + overrideFiles: { + 'next.config.js': `module.exports = ${JSON.stringify({ + output: 'export', + redirects: [ + { source: '/from', destination: '/to', permanent: true }, + ], + })}`, + }, + }) + + it('should error with "redirects" config', async () => { + await retry(async () => { + expect(next.cliOutput).toContain( + 'Specified "redirects" will not automatically work with "output: export".' + ) + }) + }) + }) + + describe('"headers" config', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: reactDependencies, + overrideFiles: { + 'next.config.js': `module.exports = ${JSON.stringify({ + output: 'export', + headers: [ + { + source: '/foo', + headers: [{ key: 'x-foo', value: 'val' }], + }, + ], + })}`, + }, + }) + + it('should error with "headers" config', async () => { + await retry(async () => { + expect(next.cliOutput).toContain( + 'Specified "headers" will not automatically work with "output: export".' + ) + }) + }) + }) + }) + + describe('api routes function', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: reactDependencies, + overrideFiles: { + 'pages/api/wow.js': 'export default (_, res) => res.end("wow")', + }, + }) + + it('should error with api routes function', async () => { + const response = await next.fetch('/api/wow') + expect(response.status).toBe(404) + await retry(async () => { + expect(next.cliOutput).toContain( + 'API Routes cannot be used with "output: export".' + ) + }) + }) + }) + + describe('middleware function', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: reactDependencies, + overrideFiles: { + 'middleware.js': + 'export function middleware(req) { console.log("[mw]",request.url) }', + }, + }) + + it('should error with middleware function', async () => { + const response = await next.fetch('/api/mw') + expect(response.status).toBe(404) + expect(next.cliOutput).not.toContain('[mw]') + await retry(async () => { + expect(next.cliOutput).toContain( + 'Middleware cannot be used with "output: export".' + ) + }) + }) + }) + + describe('getStaticProps with revalidate 10 (ISR)', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: reactDependencies, + overrideFiles: { + 'pages/blog.js': blogIsr, + }, + }) + + it('should error with getStaticProps and revalidate 10 seconds (ISR)', async () => { + const browser = await next.browser('/blog') + await waitForRedbox(browser) + expect(await getRedboxHeader(browser)).toContain( + 'ISR cannot be used with "output: export".' + ) + expect(next.cliOutput).toContain( + 'ISR cannot be used with "output: export".' + ) + }) + }) + + describe('getStaticProps with revalidate false', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: reactDependencies, + overrideFiles: { + 'pages/blog.js': blogGspRevalidateFalse, + }, + }) + + it('should work with getStaticProps and revalidate false', async () => { + const browser = await next.browser('/blog') + await waitForNoRedbox(browser) + }) + }) + + describe('getStaticProps without revalidate', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: reactDependencies, + overrideFiles: { + 'pages/blog.js': blogGspNoRevalidate, + }, + }) + + it('should work with getStaticProps and without revalidate', async () => { + const browser = await next.browser('/blog') + await waitForNoRedbox(browser) + }) + }) + + describe('getServerSideProps', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: reactDependencies, + overrideFiles: { + 'pages/blog.js': blogGssp, + }, + }) + + it('should error with getServerSideProps without fallback', async () => { + const browser = await next.browser('/blog') + await waitForRedbox(browser) + expect(await getRedboxHeader(browser)).toContain( + 'getServerSideProps cannot be used with "output: export".' + ) + expect(next.cliOutput).toContain( + 'getServerSideProps cannot be used with "output: export".' + ) + }) + }) + + describe('getStaticPaths with fallback true', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: reactDependencies, + overrideFiles: { + 'pages/posts/[slug].js': postsSlug('true'), + }, + }) + + it('should error with getStaticPaths and fallback true', async () => { + const browser = await next.browser('/posts/one') + await waitForRedbox(browser) + expect(await getRedboxHeader(browser)).toContain( + 'getStaticPaths with "fallback: true" cannot be used with "output: export".' + ) + expect(next.cliOutput).toContain( + 'getStaticPaths with "fallback: true" cannot be used with "output: export".' + ) + }) + }) + + describe('getStaticPaths with fallback blocking', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: reactDependencies, + overrideFiles: { + 'pages/posts/[slug].js': postsSlug("'blocking'"), + }, + }) + + it('should error with getStaticPaths and fallback blocking', async () => { + const browser = await next.browser('/posts/one') + await waitForRedbox(browser) + expect(await getRedboxHeader(browser)).toContain( + 'getStaticPaths with "fallback: blocking" cannot be used with "output: export".' + ) + expect(next.cliOutput).toContain( + 'getStaticPaths with "fallback: blocking" cannot be used with "output: export".' + ) + }) + }) + + describe('getStaticPaths with fallback false', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: reactDependencies, + overrideFiles: { + 'pages/posts/[slug].js': postsSlug('false'), + }, + }) + + it('should work with getStaticPaths and fallback false', async () => { + const browser = await next.browser('/posts/one') + const h1 = await browser.elementByCss('h1') + expect(await h1.text()).toContain('Hello from one') + await waitForNoRedbox(browser) + }) + }) +}) + +describe('config-output-export with hasNextSupport', () => { + describe('"rewrites" config', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: reactDependencies, + env: { NOW_BUILDER: '1' }, + overrideFiles: { + 'next.config.js': `module.exports = ${JSON.stringify({ + output: 'export', + rewrites: [{ source: '/from', destination: '/to' }], + })}`, + }, + }) + + it('should not error with "rewrites" config', async () => { + expect(next.cliOutput).not.toContain( + 'Specified "rewrites" will not automatically work with "output: export".' + ) + }) + }) + + describe('"redirects" config', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: reactDependencies, + env: { NOW_BUILDER: '1' }, + overrideFiles: { + 'next.config.js': `module.exports = ${JSON.stringify({ + output: 'export', + redirects: [{ source: '/from', destination: '/to', permanent: true }], + })}`, + }, + }) + + it('should not error with "redirects" config', async () => { + expect(next.cliOutput).not.toContain( + 'Specified "redirects" will not automatically work with "output: export".' + ) + }) + }) + + describe('"headers" config', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: reactDependencies, + env: { NOW_BUILDER: '1' }, + overrideFiles: { + 'next.config.js': `module.exports = ${JSON.stringify({ + output: 'export', + headers: [ + { + source: '/foo', + headers: [{ key: 'x-foo', value: 'val' }], + }, + ], + })}`, + }, + }) + + it('should not error with "headers" config', async () => { + expect(next.cliOutput).not.toContain( + 'Specified "headers" will not automatically work with "output: export".' + ) + }) + }) +}) diff --git a/test/integration/config-output-export/next.config.js b/test/development/config-output-export/next.config.js similarity index 100% rename from test/integration/config-output-export/next.config.js rename to test/development/config-output-export/next.config.js diff --git a/test/integration/bundle-size-profiling/pages/index.js b/test/development/config-output-export/pages/index.js similarity index 100% rename from test/integration/bundle-size-profiling/pages/index.js rename to test/development/config-output-export/pages/index.js diff --git a/test/integration/config/.gitignore b/test/development/config/.gitignore similarity index 100% rename from test/integration/config/.gitignore rename to test/development/config/.gitignore diff --git a/test/integration/config/components/hello-webpack-css.css b/test/development/config/components/hello-webpack-css.css similarity index 100% rename from test/integration/config/components/hello-webpack-css.css rename to test/development/config/components/hello-webpack-css.css diff --git a/test/integration/config/components/hello-webpack-css.js b/test/development/config/components/hello-webpack-css.js similarity index 100% rename from test/integration/config/components/hello-webpack-css.js rename to test/development/config/components/hello-webpack-css.js diff --git a/test/integration/config/components/hello-webpack-sass.scss b/test/development/config/components/hello-webpack-sass.scss similarity index 100% rename from test/integration/config/components/hello-webpack-sass.scss rename to test/development/config/components/hello-webpack-sass.scss diff --git a/test/development/config/config.test.ts b/test/development/config/config.test.ts new file mode 100644 index 000000000000..46a03df9062b --- /dev/null +++ b/test/development/config/config.test.ts @@ -0,0 +1,29 @@ +import cheerio from 'cheerio' +import { nextTestSetup } from 'e2e-utils' + +describe('Configuration', () => { + const { next } = nextTestSetup({ files: __dirname }) + + async function get$(path: string) { + const html = await next.render(path) + return cheerio.load(html) + } + + it('should disable X-Powered-By header support', async () => { + const res = await next.fetch('/') + const header = res.headers.get('X-Powered-By') + expect(header).not.toBe('Next.js') + }) + + test('correctly imports a package that defines `module` but no `main` in package.json', async () => { + const $ = await get$('/module-only-content') + expect($('#messageInAPackage').text()).toBe('OK') + }) + + it('should have env variables available on the client', async () => { + const browser = await next.browser('/next-config') + const envValue = await browser.elementByCss('#env').text() + expect(envValue).toBe('hello') + await browser.close() + }) +}) diff --git a/test/integration/config/next.config.js b/test/development/config/next.config.js similarity index 100% rename from test/integration/config/next.config.js rename to test/development/config/next.config.js diff --git a/test/development/config/node_modules/css-framework/framework.css b/test/development/config/node_modules/css-framework/framework.css new file mode 100644 index 000000000000..b76358dac766 --- /dev/null +++ b/test/development/config/node_modules/css-framework/framework.css @@ -0,0 +1,3 @@ +.frameworkClass { + background: blue; +} diff --git a/test/integration/config/node_modules/module-only-package/modern.js b/test/development/config/node_modules/module-only-package/modern.js similarity index 100% rename from test/integration/config/node_modules/module-only-package/modern.js rename to test/development/config/node_modules/module-only-package/modern.js diff --git a/test/integration/config/node_modules/module-only-package/package.json b/test/development/config/node_modules/module-only-package/package.json similarity index 100% rename from test/integration/config/node_modules/module-only-package/package.json rename to test/development/config/node_modules/module-only-package/package.json diff --git a/test/integration/config/pages/build-id.js b/test/development/config/pages/build-id.js similarity index 100% rename from test/integration/config/pages/build-id.js rename to test/development/config/pages/build-id.js diff --git a/test/integration/config/pages/module-only-content.js b/test/development/config/pages/module-only-content.js similarity index 100% rename from test/integration/config/pages/module-only-content.js rename to test/development/config/pages/module-only-content.js diff --git a/test/integration/config/pages/next-config.js b/test/development/config/pages/next-config.js similarity index 100% rename from test/integration/config/pages/next-config.js rename to test/development/config/pages/next-config.js diff --git a/test/development/css-features/css-modules-support.test.ts b/test/development/css-features/css-modules-support.test.ts new file mode 100644 index 000000000000..23516e17cc33 --- /dev/null +++ b/test/development/css-features/css-modules-support.test.ts @@ -0,0 +1,57 @@ +/* eslint-env jest */ + +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' +import { join } from 'path' + +describe('Has CSS Module in computed styles in Development', () => { + const { next } = nextTestSetup({ + files: join(__dirname, 'fixtures', 'dev-module'), + }) + + it('should have CSS for page', async () => { + const browser = await next.browser('/') + + const currentColor = await browser.eval( + `window.getComputedStyle(document.querySelector('#verify-red')).color` + ) + expect(currentColor).toMatchInlineSnapshot(`"rgb(255, 0, 0)"`) + }) +}) + +describe('Can hot reload CSS Module without losing state', () => { + const { next } = nextTestSetup({ + files: join(__dirname, 'fixtures', 'hmr-module'), + patchFileDelay: 500, + }) + + it('should update CSS color without remounting ', async () => { + const browser = await next.browser('/') + + const desiredText = 'hello world' + await browser.elementById('text-input').type(desiredText) + expect(await browser.elementById('text-input').getValue()).toBe(desiredText) + + const currentColor = await browser.eval( + `window.getComputedStyle(document.querySelector('#verify-red')).color` + ) + expect(currentColor).toMatchInlineSnapshot(`"rgb(255, 0, 0)"`) + + await next.patchFile( + 'pages/index.module.css', + (content) => content.replace('color: red', 'color: purple'), + async () => { + await retry(async () => { + const refreshedColor = await browser.eval( + `window.getComputedStyle(document.querySelector('#verify-red')).color` + ) + expect(refreshedColor).toMatchInlineSnapshot(`"rgb(128, 0, 128)"`) + }) + + expect(await browser.elementById('text-input').getValue()).toBe( + desiredText + ) + } + ) + }) +}) diff --git a/test/development/css-features/dev-css-handling.test.ts b/test/development/css-features/dev-css-handling.test.ts new file mode 100644 index 000000000000..3d211abd6640 --- /dev/null +++ b/test/development/css-features/dev-css-handling.test.ts @@ -0,0 +1,111 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' +import { join } from 'path' + +describe('Can hot reload CSS without losing state', () => { + const { next } = nextTestSetup({ + files: join(__dirname, 'fixtures/multi-page'), + }) + + it('should update CSS color without remounting ', async () => { + const browser = await next.browser('/page1') + + const desiredText = 'hello world' + await browser.elementById('text-input').type(desiredText) + expect(await browser.elementById('text-input').getValue()).toBe(desiredText) + + const currentColor = await browser.eval( + `window.getComputedStyle(document.querySelector('.red-text')).color` + ) + expect(currentColor).toMatchInlineSnapshot(`"rgb(255, 0, 0)"`) + + const cssPath = 'styles/global1.css' + const originalCss = await next.readFile(cssPath) + await next.patchFile( + cssPath, + originalCss.replace('color: red', 'color: purple') + ) + + try { + await retry(async () => { + const color = await browser.eval( + `window.getComputedStyle(document.querySelector('.red-text')).color` + ) + expect(color).toBe('rgb(128, 0, 128)') + }) + + expect(await browser.elementById('text-input').getValue()).toBe( + desiredText + ) + } finally { + await next.patchFile(cssPath, originalCss) + } + }) +}) + +describe('Has CSS in computed styles in Development', () => { + const { next } = nextTestSetup({ + files: join(__dirname, 'fixtures/multi-page'), + }) + + it('should have CSS for page', async () => { + const browser = await next.browser('/page2') + + const currentColor = await browser.eval( + `window.getComputedStyle(document.querySelector('.blue-text')).color` + ) + expect(currentColor).toMatchInlineSnapshot(`"rgb(0, 0, 255)"`) + }) +}) + +describe('Body is not hidden when unused in Development', () => { + const { next } = nextTestSetup({ + files: join(__dirname, 'fixtures/unused'), + }) + + it('should have body visible', async () => { + const browser = await next.browser('/') + const currentDisplay = await browser.eval( + `window.getComputedStyle(document.querySelector('body')).display` + ) + expect(currentDisplay).toBe('block') + }) +}) + +describe('Body is not hidden when broken in Development', () => { + const { next } = nextTestSetup({ + files: join(__dirname, 'fixtures/unused'), + }) + + it('should have body visible', async () => { + await next.patchFile( + 'pages/index.js', + (content) => content!.replace('
', '
'), + async () => { + const browser = await next.browser('/') + await retry(async () => { + const currentDisplay = await browser.eval( + `window.getComputedStyle(document.querySelector('body')).display` + ) + expect(currentDisplay).toBe('block') + }) + } + ) + }) +}) + +describe('React Lifecyce Order (dev)', () => { + const { next } = nextTestSetup({ + files: join(__dirname, 'fixtures/transition-react'), + }) + + it('should have the correct color on mount after navigation', async () => { + const browser = await next.browser('/') + + await browser.waitForElementByCss('#link-other').click() + await retry(async () => { + const text = await browser.elementByCss('#red-title').text() + expect(text).toMatchInlineSnapshot(`"rgb(255, 0, 0)"`) + }) + }) +}) diff --git a/test/integration/404-page-app/next.config.js b/test/development/css-features/fixtures/dev-module/next.config.js similarity index 100% rename from test/integration/404-page-app/next.config.js rename to test/development/css-features/fixtures/dev-module/next.config.js diff --git a/test/integration/css-fixtures/basic-module/pages/index.js b/test/development/css-features/fixtures/dev-module/pages/index.js similarity index 100% rename from test/integration/css-fixtures/basic-module/pages/index.js rename to test/development/css-features/fixtures/dev-module/pages/index.js diff --git a/test/integration/css-fixtures/basic-module/pages/index.module.css b/test/development/css-features/fixtures/dev-module/pages/index.module.css similarity index 100% rename from test/integration/css-fixtures/basic-module/pages/index.module.css rename to test/development/css-features/fixtures/dev-module/pages/index.module.css diff --git a/test/integration/css-fixtures/hmr-module/pages/index.js b/test/development/css-features/fixtures/hmr-module/pages/index.js similarity index 100% rename from test/integration/css-fixtures/hmr-module/pages/index.js rename to test/development/css-features/fixtures/hmr-module/pages/index.js diff --git a/test/integration/css-fixtures/dev-module/pages/index.module.css b/test/development/css-features/fixtures/hmr-module/pages/index.module.css similarity index 100% rename from test/integration/css-fixtures/dev-module/pages/index.module.css rename to test/development/css-features/fixtures/hmr-module/pages/index.module.css diff --git a/test/integration/css-fixtures/composes-ordering/.gitignore b/test/development/css-features/fixtures/multi-page/.gitignore similarity index 100% rename from test/integration/css-fixtures/composes-ordering/.gitignore rename to test/development/css-features/fixtures/multi-page/.gitignore diff --git a/test/integration/css-fixtures/multi-global/pages/_app.js b/test/development/css-features/fixtures/multi-page/pages/_app.js similarity index 100% rename from test/integration/css-fixtures/multi-global/pages/_app.js rename to test/development/css-features/fixtures/multi-page/pages/_app.js diff --git a/test/integration/css-fixtures/multi-page/pages/page1.js b/test/development/css-features/fixtures/multi-page/pages/page1.js similarity index 100% rename from test/integration/css-fixtures/multi-page/pages/page1.js rename to test/development/css-features/fixtures/multi-page/pages/page1.js diff --git a/test/integration/css-fixtures/multi-page/pages/page2.js b/test/development/css-features/fixtures/multi-page/pages/page2.js similarity index 100% rename from test/integration/css-fixtures/multi-page/pages/page2.js rename to test/development/css-features/fixtures/multi-page/pages/page2.js diff --git a/test/integration/css-fixtures/multi-global-reversed/styles/global1.css b/test/development/css-features/fixtures/multi-page/styles/global1.css similarity index 100% rename from test/integration/css-fixtures/multi-global-reversed/styles/global1.css rename to test/development/css-features/fixtures/multi-page/styles/global1.css diff --git a/test/integration/css-fixtures/multi-global-reversed/styles/global2.css b/test/development/css-features/fixtures/multi-page/styles/global2.css similarity index 100% rename from test/integration/css-fixtures/multi-global-reversed/styles/global2.css rename to test/development/css-features/fixtures/multi-page/styles/global2.css diff --git a/test/integration/css-fixtures/global-and-module-ordering/.gitignore b/test/development/css-features/fixtures/transition-react/.gitignore similarity index 100% rename from test/integration/css-fixtures/global-and-module-ordering/.gitignore rename to test/development/css-features/fixtures/transition-react/.gitignore diff --git a/test/integration/css-fixtures/transition-react/pages/index.js b/test/development/css-features/fixtures/transition-react/pages/index.js similarity index 100% rename from test/integration/css-fixtures/transition-react/pages/index.js rename to test/development/css-features/fixtures/transition-react/pages/index.js diff --git a/test/integration/css-fixtures/transition-react/pages/other.js b/test/development/css-features/fixtures/transition-react/pages/other.js similarity index 100% rename from test/integration/css-fixtures/transition-react/pages/other.js rename to test/development/css-features/fixtures/transition-react/pages/other.js diff --git a/test/integration/css-fixtures/transition-react/pages/other.module.css b/test/development/css-features/fixtures/transition-react/pages/other.module.css similarity index 100% rename from test/integration/css-fixtures/transition-react/pages/other.module.css rename to test/development/css-features/fixtures/transition-react/pages/other.module.css diff --git a/test/integration/css-fixtures/bad-custom-configuration-arr-1/pages/index.js b/test/development/css-features/fixtures/unused/pages/index.js similarity index 100% rename from test/integration/css-fixtures/bad-custom-configuration-arr-1/pages/index.js rename to test/development/css-features/fixtures/unused/pages/index.js diff --git a/test/development/css-modules/css-modules.test.ts b/test/development/css-modules/css-modules.test.ts new file mode 100644 index 000000000000..1eb49967c9c9 --- /dev/null +++ b/test/development/css-modules/css-modules.test.ts @@ -0,0 +1,59 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' +import { join } from 'path' + +describe('Has CSS Module in computed styles in Development', () => { + const { next } = nextTestSetup({ + files: join(__dirname, 'fixtures/dev-module'), + }) + + it('should have CSS for page', async () => { + const browser = await next.browser('/') + + const currentColor = await browser.eval( + `window.getComputedStyle(document.querySelector('#verify-red')).color` + ) + expect(currentColor).toMatchInlineSnapshot(`"rgb(255, 0, 0)"`) + }) +}) + +describe('Can hot reload CSS Module without losing state', () => { + const { next } = nextTestSetup({ + files: join(__dirname, 'fixtures/hmr-module'), + }) + + it('should update CSS color without remounting ', async () => { + const browser = await next.browser('/') + + const desiredText = 'hello world' + await browser.elementById('text-input').type(desiredText) + expect(await browser.elementById('text-input').getValue()).toBe(desiredText) + + const currentColor = await browser.eval( + `window.getComputedStyle(document.querySelector('#verify-red')).color` + ) + expect(currentColor).toMatchInlineSnapshot(`"rgb(255, 0, 0)"`) + + const cssFilePath = 'pages/index.module.css' + const originalContent = await next.readFile(cssFilePath) + try { + await next.patchFile( + cssFilePath, + originalContent.replace('color: red', 'color: purple') + ) + + await retry(async () => { + const refreshedColor = await browser.eval( + `window.getComputedStyle(document.querySelector('#verify-red')).color` + ) + expect(refreshedColor).toMatchInlineSnapshot(`"rgb(128, 0, 128)"`) + }) + + expect(await browser.elementById('text-input').getValue()).toBe( + desiredText + ) + } finally { + await next.patchFile(cssFilePath, originalContent) + } + }) +}) diff --git a/test/integration/404-page-ssg/next.config.js b/test/development/css-modules/fixtures/dev-module/next.config.js similarity index 100% rename from test/integration/404-page-ssg/next.config.js rename to test/development/css-modules/fixtures/dev-module/next.config.js diff --git a/test/integration/css-fixtures/dev-module/pages/index.js b/test/development/css-modules/fixtures/dev-module/pages/index.js similarity index 100% rename from test/integration/css-fixtures/dev-module/pages/index.js rename to test/development/css-modules/fixtures/dev-module/pages/index.js diff --git a/test/integration/css-fixtures/hmr-module/pages/index.module.css b/test/development/css-modules/fixtures/dev-module/pages/index.module.css similarity index 100% rename from test/integration/css-fixtures/hmr-module/pages/index.module.css rename to test/development/css-modules/fixtures/dev-module/pages/index.module.css diff --git a/test/development/css-modules/fixtures/hmr-module/pages/index.js b/test/development/css-modules/fixtures/hmr-module/pages/index.js new file mode 100644 index 000000000000..4acb2fdfccca --- /dev/null +++ b/test/development/css-modules/fixtures/hmr-module/pages/index.js @@ -0,0 +1,15 @@ +import { redText } from './index.module.css' + +function Home() { + return ( + <> +
+ This text should be red. +
+
+ + + ) +} + +export default Home diff --git a/test/integration/css-fixtures/nm-module/node_modules/example/index.module.css b/test/development/css-modules/fixtures/hmr-module/pages/index.module.css similarity index 100% rename from test/integration/css-fixtures/nm-module/node_modules/example/index.module.css rename to test/development/css-modules/fixtures/hmr-module/pages/index.module.css diff --git a/test/development/development-hmr-refresh/development-hmr-refresh.test.ts b/test/development/development-hmr-refresh/development-hmr-refresh.test.ts new file mode 100644 index 000000000000..6bccc6df7c7a --- /dev/null +++ b/test/development/development-hmr-refresh/development-hmr-refresh.test.ts @@ -0,0 +1,18 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('development HMR refresh', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + // see issue #22099 + it('page should not reload when the file is not changed', async () => { + const browser = await next.browser('/with+Special&Chars=') + + await browser.eval(`window.doesNotReloadCheck = true`) + + await new Promise((resolve) => setTimeout(resolve, 10000)) + + expect(await browser.eval('window.doesNotReloadCheck')).toBe(true) + }) +}) diff --git a/test/integration/development-hmr-refresh/pages/with+Special&Chars=.js b/test/development/development-hmr-refresh/pages/with+Special&Chars=.js similarity index 100% rename from test/integration/development-hmr-refresh/pages/with+Special&Chars=.js rename to test/development/development-hmr-refresh/pages/with+Special&Chars=.js diff --git a/test/development/document-head-warnings/document-head-warnings.test.ts b/test/development/document-head-warnings/document-head-warnings.test.ts new file mode 100644 index 000000000000..57ca7490d3c2 --- /dev/null +++ b/test/development/document-head-warnings/document-head-warnings.test.ts @@ -0,0 +1,28 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('Custom Document Head Warnings', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('warns when using a in document/head', async () => { + await next.render('/') + expect(next.cliOutput).toMatch( + /.*Warning: <title> should not be used in _document.js's <Head>\..*/ + ) + }) + + it('warns when using viewport meta tags in document/head', async () => { + await next.render('/') + expect(next.cliOutput).toMatch( + /.*Warning: viewport meta tags should not be used in _document.js's <Head>\..*/ + ) + }) + + it('warns when using a crossOrigin attribute on document/head', async () => { + await next.render('/') + expect(next.cliOutput).toMatch( + /.*Warning: `Head` attribute `crossOrigin` is deprecated\..*/ + ) + }) +}) diff --git a/test/integration/document-head-warnings/pages/_document.js b/test/development/document-head-warnings/pages/_document.js similarity index 100% rename from test/integration/document-head-warnings/pages/_document.js rename to test/development/document-head-warnings/pages/_document.js diff --git a/test/integration/document-head-warnings/pages/index.js b/test/development/document-head-warnings/pages/index.js similarity index 100% rename from test/integration/document-head-warnings/pages/index.js rename to test/development/document-head-warnings/pages/index.js diff --git a/test/development/dynamic-require/index.test.ts b/test/development/dynamic-require/index.test.ts new file mode 100644 index 000000000000..bedc464aca6a --- /dev/null +++ b/test/development/dynamic-require/index.test.ts @@ -0,0 +1,16 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('Dynamic require', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: { + react: '19.3.0-canary-fef12a01-20260413', + 'react-dom': '19.3.0-canary-fef12a01-20260413', + }, + }) + + it('should not throw error when dynamic require is used', async () => { + const html = await next.render('/') + expect(html).toMatch(/If you can see this then we are good/) + }) +}) diff --git a/test/integration/dynamic-require/locales/en.js b/test/development/dynamic-require/locales/en.js similarity index 100% rename from test/integration/dynamic-require/locales/en.js rename to test/development/dynamic-require/locales/en.js diff --git a/test/integration/dynamic-require/locales/ru.js b/test/development/dynamic-require/locales/ru.js similarity index 100% rename from test/integration/dynamic-require/locales/ru.js rename to test/development/dynamic-require/locales/ru.js diff --git a/test/integration/dynamic-require/pages/index.js b/test/development/dynamic-require/pages/index.js similarity index 100% rename from test/integration/dynamic-require/pages/index.js rename to test/development/dynamic-require/pages/index.js diff --git a/test/development/dynamic-route-rename/dynamic-route-rename.test.ts b/test/development/dynamic-route-rename/dynamic-route-rename.test.ts new file mode 100644 index 000000000000..79bebf09af1d --- /dev/null +++ b/test/development/dynamic-route-rename/dynamic-route-rename.test.ts @@ -0,0 +1,29 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('Dynamic route rename casing', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should not throw error when changing casing of dynamic route file', async () => { + const html = await next.render('/abc') + expect(html).toContain('hi') + + await next.renameFile('pages/[pid].js', 'pages/[PiD].js') + + await retry(async () => { + expect(next.cliOutput).not.toContain( + `You cannot use different slug names for the same dynamic path` + ) + }) + + await next.renameFile('pages/[PiD].js', 'pages/[pid].js') + + await retry(async () => { + expect(next.cliOutput).not.toContain( + `You cannot use different slug names for the same dynamic path` + ) + }) + }) +}) diff --git a/test/integration/dynamic-route-rename/pages/[pid].js b/test/development/dynamic-route-rename/pages/[pid].js similarity index 100% rename from test/integration/dynamic-route-rename/pages/[pid].js rename to test/development/dynamic-route-rename/pages/[pid].js diff --git a/test/development/empty-object-getInitialProps/empty-object-getInitialProps.test.ts b/test/development/empty-object-getInitialProps/empty-object-getInitialProps.test.ts new file mode 100644 index 000000000000..0cae7dd99dfb --- /dev/null +++ b/test/development/empty-object-getInitialProps/empty-object-getInitialProps.test.ts @@ -0,0 +1,47 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('empty-object-getInitialProps', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should show empty object warning on SSR', async () => { + await next.render('/') + await retry(() => { + expect(next.cliOutput).toMatch( + /returned an empty object from `getInitialProps`/ + ) + }) + }) + + it('should not show empty object warning for page without `getInitialProps`', async () => { + const outputIndex = next.cliOutput.length + await next.render('/static') + await retry(() => { + const newOutput = next.cliOutput.slice(outputIndex) + expect(newOutput).not.toMatch( + /returned an empty object from `getInitialProps`/ + ) + }) + }) + + it('should show empty object warning during client transition', async () => { + const browser = await next.browser('/static') + await browser.eval(`(function() { + window.gotWarn = false + const origWarn = console.warn + window.console.warn = function () { + if (arguments[0].match(/returned an empty object from \`getInitialProps\`/)) { + window.gotWarn = true + } + origWarn.apply(this, arguments) + } + window.next.router.replace('/another') + })()`) + await retry(async () => { + const gotWarn = await browser.eval(`window.gotWarn`) + expect(gotWarn).toBe(true) + }) + }) +}) diff --git a/test/integration/empty-object-getInitialProps/pages/another.js b/test/development/empty-object-getInitialProps/pages/another.js similarity index 100% rename from test/integration/empty-object-getInitialProps/pages/another.js rename to test/development/empty-object-getInitialProps/pages/another.js diff --git a/test/integration/empty-object-getInitialProps/pages/index.js b/test/development/empty-object-getInitialProps/pages/index.js similarity index 100% rename from test/integration/empty-object-getInitialProps/pages/index.js rename to test/development/empty-object-getInitialProps/pages/index.js diff --git a/test/integration/empty-object-getInitialProps/pages/static.js b/test/development/empty-object-getInitialProps/pages/static.js similarity index 100% rename from test/integration/empty-object-getInitialProps/pages/static.js rename to test/development/empty-object-getInitialProps/pages/static.js diff --git a/test/development/empty-project/empty-project.test.ts b/test/development/empty-project/empty-project.test.ts new file mode 100644 index 000000000000..8e6c90b58cc1 --- /dev/null +++ b/test/development/empty-project/empty-project.test.ts @@ -0,0 +1,18 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('Empty Project', () => { + const { next } = nextTestSetup({ + files: __dirname, + skipStart: true, + }) + + beforeAll(async () => { + await next.deleteFile('pages/.gitkeep') + await next.start() + }) + + it('Should not time out and return 404', async () => { + const res = await next.fetch('/', { timeout: 10_000 }) + expect(res.status).toBe(404) + }) +}) diff --git a/test/integration/client-404/next.config.js b/test/development/empty-project/next.config.js similarity index 100% rename from test/integration/client-404/next.config.js rename to test/development/empty-project/next.config.js diff --git a/test/integration/empty-project/pages/.gitkeep b/test/development/empty-project/pages/.gitkeep similarity index 100% rename from test/integration/empty-project/pages/.gitkeep rename to test/development/empty-project/pages/.gitkeep diff --git a/test/development/gssp-redirect-with-rewrites/gssp-redirect-with-rewrites.test.ts b/test/development/gssp-redirect-with-rewrites/gssp-redirect-with-rewrites.test.ts new file mode 100644 index 000000000000..22cdfe37ec47 --- /dev/null +++ b/test/development/gssp-redirect-with-rewrites/gssp-redirect-with-rewrites.test.ts @@ -0,0 +1,30 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('getServerSideProps redirects', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should use a client-side navigation for a rewritten URL', async () => { + const browser = await next.browser('/alias-to-main-content') + + await browser.eval('window.__SAME_PAGE = true') + await browser.elementByCss('#link-with-rewritten-url').click() + await browser.waitForElementByCss('.refreshed') + + expect(await browser.eval('window.__SAME_PAGE')).toBe(true) + }) + + it('should fallback to browser navigation for an unknown URL', async () => { + const browser = await next.browser('/alias-to-main-content') + + await browser.eval('window.__SAME_PAGE = true') + await browser.elementByCss('#link-unknown-url').click() + + await retry(async () => { + const val = await browser.eval('window.__SAME_PAGE') + expect(val).toBeFalsy() + }) + }) +}) diff --git a/test/integration/gssp-redirect-with-rewrites/next.config.js b/test/development/gssp-redirect-with-rewrites/next.config.js similarity index 100% rename from test/integration/gssp-redirect-with-rewrites/next.config.js rename to test/development/gssp-redirect-with-rewrites/next.config.js diff --git a/test/integration/gssp-redirect-with-rewrites/pages/main-content.js b/test/development/gssp-redirect-with-rewrites/pages/main-content.js similarity index 100% rename from test/integration/gssp-redirect-with-rewrites/pages/main-content.js rename to test/development/gssp-redirect-with-rewrites/pages/main-content.js diff --git a/test/integration/gssp-redirect-with-rewrites/pages/redirector.js b/test/development/gssp-redirect-with-rewrites/pages/redirector.js similarity index 100% rename from test/integration/gssp-redirect-with-rewrites/pages/redirector.js rename to test/development/gssp-redirect-with-rewrites/pages/redirector.js diff --git a/test/development/invalid-revalidate-values/invalid-revalidate-values.test.ts b/test/development/invalid-revalidate-values/invalid-revalidate-values.test.ts new file mode 100644 index 000000000000..512670e1616d --- /dev/null +++ b/test/development/invalid-revalidate-values/invalid-revalidate-values.test.ts @@ -0,0 +1,94 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('Invalid revalidate values', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should not show error initially', async () => { + const html = await next.render('/ssg') + expect(html).toContain('a-ok') + }) + + it('should not show error for false revalidate value', async () => { + const originalContent = await next.readFile('pages/ssg.js') + await next.patchFile( + 'pages/ssg.js', + originalContent.replace('revalidate: 1', 'revalidate: false') + ) + + await retry(async () => { + const html = await next.render('/ssg') + expect(html).toContain('a-ok') + }) + + await next.patchFile('pages/ssg.js', originalContent) + }) + + it('should not show error for true revalidate value', async () => { + const originalContent = await next.readFile('pages/ssg.js') + await next.patchFile( + 'pages/ssg.js', + originalContent.replace('revalidate: 1', 'revalidate: true') + ) + + await retry(async () => { + const html = await next.render('/ssg') + expect(html).toContain('a-ok') + }) + + await next.patchFile('pages/ssg.js', originalContent) + }) + + it('should show error for string revalidate value', async () => { + const originalContent = await next.readFile('pages/ssg.js') + await next.patchFile( + 'pages/ssg.js', + originalContent.replace('revalidate: 1', 'revalidate: "1"') + ) + + await retry(async () => { + const html = await next.render('/ssg') + expect(html).toMatch( + /A page's revalidate option must be seconds expressed as a natural number. Mixed numbers and strings cannot be used. Received/ + ) + }) + + await next.patchFile('pages/ssg.js', originalContent) + }) + + it('should show error for null revalidate value', async () => { + const originalContent = await next.readFile('pages/ssg.js') + await next.patchFile( + 'pages/ssg.js', + originalContent.replace('revalidate: 1', 'revalidate: null') + ) + + await retry(async () => { + const html = await next.render('/ssg') + expect(html).toMatch( + /A page's revalidate option must be seconds expressed as a natural number. Mixed numbers and strings cannot be used. Received/ + ) + }) + + await next.patchFile('pages/ssg.js', originalContent) + }) + + it('should show error for float revalidate value', async () => { + const originalContent = await next.readFile('pages/ssg.js') + await next.patchFile( + 'pages/ssg.js', + originalContent.replace('revalidate: 1', 'revalidate: 1.1') + ) + + await retry(async () => { + const html = await next.render('/ssg') + expect(html).toMatch( + /A page's revalidate option must be seconds expressed as a natural number for \/ssg. Mixed numbers, such as/ + ) + }) + + await next.patchFile('pages/ssg.js', originalContent) + }) +}) diff --git a/test/integration/invalid-revalidate-values/pages/ssg.js b/test/development/invalid-revalidate-values/pages/ssg.js similarity index 100% rename from test/integration/invalid-revalidate-values/pages/ssg.js rename to test/development/invalid-revalidate-values/pages/ssg.js diff --git a/test/integration/jsconfig-paths-wildcard/.gitignore b/test/development/jsconfig-paths-wildcard/.gitignore similarity index 100% rename from test/integration/jsconfig-paths-wildcard/.gitignore rename to test/development/jsconfig-paths-wildcard/.gitignore diff --git a/test/development/jsconfig-paths-wildcard/jsconfig-paths-wildcard.test.ts b/test/development/jsconfig-paths-wildcard/jsconfig-paths-wildcard.test.ts new file mode 100644 index 000000000000..6999c0b057ff --- /dev/null +++ b/test/development/jsconfig-paths-wildcard/jsconfig-paths-wildcard.test.ts @@ -0,0 +1,39 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('jsconfig paths wildcard', () => { + describe('default behavior', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should resolve a wildcard alias', async () => { + const $ = await next.render$('/wildcard-alias') + expect($('body').text()).toMatch(/world/) + }) + }) + + describe('without baseUrl', () => { + const { next } = nextTestSetup({ + files: __dirname, + overrideFiles: { + 'jsconfig.json': JSON.stringify( + { + compilerOptions: { + paths: { + '*': ['./node_modules/*'], + }, + }, + exclude: ['node_modules', '**/*.test.ts', '**/*.test.tsx'], + }, + null, + 2 + ), + }, + }) + + it('should resolve a wildcard alias', async () => { + const $ = await next.render$('/wildcard-alias') + expect($('body').text()).toMatch(/world/) + }) + }) +}) diff --git a/test/integration/jsconfig-paths-wildcard/jsconfig.json b/test/development/jsconfig-paths-wildcard/jsconfig.json similarity index 100% rename from test/integration/jsconfig-paths-wildcard/jsconfig.json rename to test/development/jsconfig-paths-wildcard/jsconfig.json diff --git a/test/integration/disable-js/next.config.js b/test/development/jsconfig-paths-wildcard/next.config.js similarity index 100% rename from test/integration/disable-js/next.config.js rename to test/development/jsconfig-paths-wildcard/next.config.js diff --git a/test/integration/jsconfig-paths-wildcard/node_modules/mypackage/data.js b/test/development/jsconfig-paths-wildcard/node_modules/mypackage/data.js similarity index 100% rename from test/integration/jsconfig-paths-wildcard/node_modules/mypackage/data.js rename to test/development/jsconfig-paths-wildcard/node_modules/mypackage/data.js diff --git a/test/integration/jsconfig-paths-wildcard/node_modules/mypackage/myfile.js b/test/development/jsconfig-paths-wildcard/node_modules/mypackage/myfile.js similarity index 100% rename from test/integration/jsconfig-paths-wildcard/node_modules/mypackage/myfile.js rename to test/development/jsconfig-paths-wildcard/node_modules/mypackage/myfile.js diff --git a/test/integration/jsconfig-paths-wildcard/pages/wildcard-alias.js b/test/development/jsconfig-paths-wildcard/pages/wildcard-alias.js similarity index 100% rename from test/integration/jsconfig-paths-wildcard/pages/wildcard-alias.js rename to test/development/jsconfig-paths-wildcard/pages/wildcard-alias.js diff --git a/test/development/link-with-encoding/link-with-encoding.test.ts b/test/development/link-with-encoding/link-with-encoding.test.ts new file mode 100644 index 000000000000..84a9087c448b --- /dev/null +++ b/test/development/link-with-encoding/link-with-encoding.test.ts @@ -0,0 +1,219 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('Link Component with Encoding', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + describe('spaces', () => { + it('should have correct query on SSR', async () => { + const browser = await next.browser(encodeURI('/single/hello world ')) + const text = await browser.elementByCss('#query-content').text() + expect(text).toMatchInlineSnapshot(`"{"slug":"hello world "}"`) + }) + + it('should have correct query on Router#push', async () => { + const browser = await next.browser('/') + await browser.eval( + `window.next.router.push( + { pathname: '/single/[slug]' }, + { pathname: encodeURI('/single/hello world ') } + )` + ) + await retry(async () => { + expect(await browser.hasElementByCssSelector('#query-content')).toBe( + true + ) + }) + const text = await browser.elementByCss('#query-content').text() + expect(text).toMatchInlineSnapshot(`"{"slug":"hello world "}"`) + }) + + it('should have correct query on simple client-side <Link>', async () => { + const browser = await next.browser('/') + await browser.elementByCss('#single-spaces').click() + await retry(async () => { + expect(await browser.hasElementByCssSelector('#query-content')).toBe( + true + ) + }) + const text = await browser.elementByCss('#query-content').text() + expect(text).toMatchInlineSnapshot(`"{"slug":"hello world "}"`) + }) + }) + + describe('percent', () => { + it('should have correct query on SSR', async () => { + const browser = await next.browser(encodeURI('/single/hello%world')) + const text = await browser.elementByCss('#query-content').text() + expect(text).toMatchInlineSnapshot(`"{"slug":"hello%world"}"`) + }) + + it('should have correct query on Router#push', async () => { + const browser = await next.browser('/') + await browser.eval( + `window.next.router.push( + { pathname: '/single/[slug]' }, + { pathname: encodeURI('/single/hello%world') } + )` + ) + await retry(async () => { + expect(await browser.hasElementByCssSelector('#query-content')).toBe( + true + ) + }) + const text = await browser.elementByCss('#query-content').text() + expect(text).toMatchInlineSnapshot(`"{"slug":"hello%world"}"`) + }) + + it('should have correct query on simple client-side <Link>', async () => { + const browser = await next.browser('/') + await browser.elementByCss('#single-percent').click() + await retry(async () => { + expect(await browser.hasElementByCssSelector('#query-content')).toBe( + true + ) + }) + const text = await browser.elementByCss('#query-content').text() + expect(text).toMatchInlineSnapshot(`"{"slug":"hello%world"}"`) + }) + }) + + describe('forward slash', () => { + it('should have correct query on SSR', async () => { + const browser = await next.browser( + `/single/hello${encodeURIComponent('/')}world` + ) + const text = await browser.elementByCss('#query-content').text() + expect(text).toMatchInlineSnapshot(`"{"slug":"hello/world"}"`) + }) + + it('should have correct query on Router#push', async () => { + const browser = await next.browser('/') + await browser.eval( + `window.next.router.push( + { pathname: '/single/[slug]' }, + { pathname: '/single/hello${encodeURIComponent('/')}world' } + )` + ) + await retry(async () => { + expect(await browser.hasElementByCssSelector('#query-content')).toBe( + true + ) + }) + const text = await browser.elementByCss('#query-content').text() + expect(text).toMatchInlineSnapshot(`"{"slug":"hello/world"}"`) + }) + + it('should have correct query on simple client-side <Link>', async () => { + const browser = await next.browser('/') + await browser.elementByCss('#single-slash').click() + await retry(async () => { + expect(await browser.hasElementByCssSelector('#query-content')).toBe( + true + ) + }) + const text = await browser.elementByCss('#query-content').text() + expect(text).toMatchInlineSnapshot(`"{"slug":"hello/world"}"`) + }) + }) + + describe('double quote', () => { + it('should have correct query on SSR', async () => { + const browser = await next.browser( + `/single/hello${encodeURIComponent('"')}world` + ) + const text = await browser.elementByCss('#query-content').text() + expect(JSON.parse(text)).toMatchInlineSnapshot(` + { + "slug": "hello"world", + } + `) + }) + + it('should have correct query on Router#push', async () => { + const browser = await next.browser('/') + await browser.eval( + `window.next.router.push( + { pathname: '/single/[slug]' }, + { pathname: '/single/hello${encodeURIComponent('"')}world' } + )` + ) + await retry(async () => { + expect(await browser.hasElementByCssSelector('#query-content')).toBe( + true + ) + }) + const text = await browser.elementByCss('#query-content').text() + expect(JSON.parse(text)).toMatchInlineSnapshot(` + { + "slug": "hello"world", + } + `) + }) + + it('should have correct query on simple client-side <Link>', async () => { + const browser = await next.browser('/') + await browser.elementByCss('#single-double-quote').click() + await retry(async () => { + expect(await browser.hasElementByCssSelector('#query-content')).toBe( + true + ) + }) + const text = await browser.elementByCss('#query-content').text() + expect(JSON.parse(text)).toMatchInlineSnapshot(` + { + "slug": "hello"world", + } + `) + }) + }) + + describe('colon', () => { + it('should have correct query on SSR', async () => { + const browser = await next.browser( + `/single/hello${encodeURIComponent(':')}world` + ) + const text = await browser.elementByCss('#query-content').text() + expect(text).toMatchInlineSnapshot(`"{"slug":"hello:world"}"`) + }) + + it('should have correct query on Router#push', async () => { + const browser = await next.browser('/') + await browser.eval( + `window.next.router.push( + { pathname: '/single/[slug]' }, + { pathname: '/single/hello${encodeURIComponent(':')}world' } + )` + ) + await retry(async () => { + expect(await browser.hasElementByCssSelector('#query-content')).toBe( + true + ) + }) + const text = await browser.elementByCss('#query-content').text() + expect(text).toMatchInlineSnapshot(`"{"slug":"hello:world"}"`) + }) + + it('should have correct query on simple client-side <Link>', async () => { + const browser = await next.browser('/') + await browser.elementByCss('#single-colon').click() + await retry(async () => { + expect(await browser.hasElementByCssSelector('#query-content')).toBe( + true + ) + }) + const text = await browser.elementByCss('#query-content').text() + expect(text).toMatchInlineSnapshot(`"{"slug":"hello:world"}"`) + }) + + it('should have correct parsing of url query params', async () => { + const browser = await next.browser('/') + await browser.waitForElementByCss('#url-param').click() + const content = await browser.waitForElementByCss('#query-content').text() + const query = JSON.parse(content) + expect(query).toHaveProperty('id', 'http://example.com/') + }) + }) +}) diff --git a/test/integration/link-with-encoding/pages/index.js b/test/development/link-with-encoding/pages/index.js similarity index 100% rename from test/integration/link-with-encoding/pages/index.js rename to test/development/link-with-encoding/pages/index.js diff --git a/test/integration/link-with-encoding/pages/query.js b/test/development/link-with-encoding/pages/query.js similarity index 100% rename from test/integration/link-with-encoding/pages/query.js rename to test/development/link-with-encoding/pages/query.js diff --git a/test/integration/link-with-encoding/pages/single/[slug].js b/test/development/link-with-encoding/pages/single/[slug].js similarity index 100% rename from test/integration/link-with-encoding/pages/single/[slug].js rename to test/development/link-with-encoding/pages/single/[slug].js diff --git a/test/development/middleware-dev-update/middleware-dev-update.test.ts b/test/development/middleware-dev-update/middleware-dev-update.test.ts new file mode 100644 index 000000000000..674c1fe7725c --- /dev/null +++ b/test/development/middleware-dev-update/middleware-dev-update.test.ts @@ -0,0 +1,127 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('Middleware development errors', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + let middlewareContent: string + beforeAll(async () => { + middlewareContent = await next.readFile('middleware.js') + }) + + async function assertMiddlewareFetch(hasMiddleware: boolean, path = '/') { + await retry(async () => { + const res = await next.fetch(path) + expect(res.status).toBe(200) + expect(res.headers.get('x-from-middleware')).toBe( + hasMiddleware ? 'true' : null + ) + }) + } + + async function assertMiddlewareRender(hasMiddleware: boolean, path = '/') { + const browser = await next.browser(path) + await retry(async () => { + const fromMiddleware = await browser.elementById('from-middleware').text() + expect(fromMiddleware).toBe(hasMiddleware ? 'true' : 'null') + }) + } + + describe('when middleware is removed', () => { + let stderrLog = '' + let onStderr: ((msg: string) => void) | undefined + + beforeEach(async () => { + stderrLog = '' + onStderr = (msg: string) => { + stderrLog += msg + } + next.on('stderr', onStderr) + + await next.patchFile('middleware.js', middlewareContent) + await assertMiddlewareFetch(true) + await next.deleteFile('middleware.js') + }) + + afterEach(async () => { + if (onStderr) { + next.off('stderr', onStderr) + } + await next.patchFile('middleware.js', middlewareContent) + }) + + it('sends response correctly', async () => { + await assertMiddlewareFetch(false) + await assertMiddlewareRender(false) + + // Mirrors integration `assert no extra message on stderr` (context.logs.stderr). + await retry(async () => { + expect(stderrLog).not.toContain('error') + }) + }) + }) + + describe('when middleware is removed and re-added', () => { + beforeEach(async () => { + await next.patchFile('middleware.js', middlewareContent) + await assertMiddlewareFetch(true) + await next.deleteFile('middleware.js') + await assertMiddlewareFetch(false) + await next.patchFile('middleware.js', middlewareContent) + }) + + it('sends response correctly', async () => { + await assertMiddlewareFetch(true) + await assertMiddlewareRender(true) + }) + }) + + describe('when middleware is added', () => { + beforeEach(async () => { + await next.deleteFile('middleware.js') + await assertMiddlewareFetch(false) + await next.patchFile('middleware.js', middlewareContent) + }) + + it('sends response correctly', async () => { + await retry(() => assertMiddlewareFetch(true)) + await assertMiddlewareRender(true) + }) + }) + + describe('when matcher is added', () => { + beforeEach(async () => { + await next.patchFile( + 'middleware.js', + middlewareContent + + ` + export const config = { + matcher: '/', + } + ` + ) + await assertMiddlewareFetch(true) + + await next.patchFile( + 'middleware.js', + middlewareContent + + ` + export const config = { + matcher: '/asdf', + } + ` + ) + }) + + afterEach(async () => { + await next.patchFile('middleware.js', middlewareContent) + }) + + it('sends response correctly', async () => { + await retry(() => assertMiddlewareFetch(true, '/asdf')) + await retry(() => assertMiddlewareRender(true, '/asdf')) + }) + }) +}) diff --git a/test/integration/middleware-dev-update/middleware.js b/test/development/middleware-dev-update/middleware.js similarity index 100% rename from test/integration/middleware-dev-update/middleware.js rename to test/development/middleware-dev-update/middleware.js diff --git a/test/integration/middleware-dev-update/pages/index.js b/test/development/middleware-dev-update/pages/index.js similarity index 100% rename from test/integration/middleware-dev-update/pages/index.js rename to test/development/middleware-dev-update/pages/index.js diff --git a/test/development/middleware-overrides-node.js-api/middleware-overrides-node.js-api.test.ts b/test/development/middleware-overrides-node.js-api/middleware-overrides-node.js-api.test.ts new file mode 100644 index 000000000000..86304036fc6d --- /dev/null +++ b/test/development/middleware-overrides-node.js-api/middleware-overrides-node.js-api.test.ts @@ -0,0 +1,21 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('Middleware overriding a Node.js API', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('does not show a warning and allows overriding', async () => { + const res = await next.fetch('/') + expect(res.status).toBe(200) + + await retry(async () => { + expect(next.cliOutput).toContain('fixed-value') + }) + + expect(next.cliOutput).not.toContain('TypeError') + expect(next.cliOutput).not.toContain('A Node.js API is used (process.env') + expect(next.cliOutput).not.toContain('A Node.js API is used (process.cwd') + }) +}) diff --git a/test/integration/middleware-overrides-node.js-api/middleware.js b/test/development/middleware-overrides-node.js-api/middleware.js similarity index 100% rename from test/integration/middleware-overrides-node.js-api/middleware.js rename to test/development/middleware-overrides-node.js-api/middleware.js diff --git a/test/integration/middleware-build-errors/pages/index.js b/test/development/middleware-overrides-node.js-api/pages/index.js similarity index 100% rename from test/integration/middleware-build-errors/pages/index.js rename to test/development/middleware-overrides-node.js-api/pages/index.js diff --git a/test/integration/missing-document-component-error/test/index.test.ts b/test/development/missing-document-component-error/missing-document-component-error.test.ts similarity index 76% rename from test/integration/missing-document-component-error/test/index.test.ts rename to test/development/missing-document-component-error/missing-document-component-error.test.ts index dee1b7de9d21..444a4b4665d8 100644 --- a/test/integration/missing-document-component-error/test/index.test.ts +++ b/test/development/missing-document-component-error/missing-document-component-error.test.ts @@ -1,41 +1,26 @@ -/* eslint-env jest */ - -import fs from 'fs-extra' -import { join } from 'path' -import { - findPort, - killApp, - launchApp, - check, - renderViaHTTP, -} from 'next-test-utils' - -const appDir = join(__dirname, '..') -const docPath = join(appDir, 'pages/_document.js') -let appPort -let app - -const checkMissing = async (missing = [], docContent) => { - await fs.writeFile(docPath, docContent) - let stderr = '' - - appPort = await findPort() - app = await launchApp(appDir, appPort, { - onStderr(msg) { - stderr += msg || '' - }, +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('Missing _document components error', () => { + const { next } = nextTestSetup({ + files: __dirname, }) - await renderViaHTTP(appPort, '/') + async function checkMissing(missing: string[], docContent: string) { + const outputIndex = next.cliOutput.length + await next.patchFile('pages/_document.js', docContent) - await check(() => stderr, new RegExp(`missing-document-component`)) - await check(() => stderr, new RegExp(`${missing.join(', ')}`)) + await next.render('/').catch(() => {}) - await killApp(app) - await fs.remove(docPath) -} + await retry(async () => { + const newOutput = next.cliOutput.slice(outputIndex) + expect(newOutput).toContain('missing-document-component') + expect(newOutput).toContain(missing.join(', ')) + }) + + await next.deleteFile('pages/_document.js') + } -describe('Missing _document components error', () => { it('should detect missing Html component', async () => { await checkMissing( ['<Html />'], diff --git a/test/integration/missing-document-component-error/pages/index.js b/test/development/missing-document-component-error/pages/index.js similarity index 100% rename from test/integration/missing-document-component-error/pages/index.js rename to test/development/missing-document-component-error/pages/index.js diff --git a/test/development/next-image-new/export-config/export-config.test.ts b/test/development/next-image-new/export-config/export-config.test.ts new file mode 100644 index 000000000000..de98129ec5cb --- /dev/null +++ b/test/development/next-image-new/export-config/export-config.test.ts @@ -0,0 +1,17 @@ +import { nextTestSetup } from 'e2e-utils' +import { getRedboxHeader, waitForRedbox } from 'next-test-utils' + +describe('next/image with output export config', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should error', async () => { + const browser = await next.browser('/') + const msg = + "Image Optimization using the default loader is not compatible with `{ output: 'export' }`." + await waitForRedbox(browser) + expect(await getRedboxHeader(browser)).toContain(msg) + expect(next.cliOutput).toContain(msg) + }) +}) diff --git a/test/integration/export-image-default/next.config.js b/test/development/next-image-new/export-config/next.config.js similarity index 100% rename from test/integration/export-image-default/next.config.js rename to test/development/next-image-new/export-config/next.config.js diff --git a/test/integration/next-image-new/export-config/pages/index.js b/test/development/next-image-new/export-config/pages/index.js similarity index 100% rename from test/integration/next-image-new/export-config/pages/index.js rename to test/development/next-image-new/export-config/pages/index.js diff --git a/test/integration/image-optimizer/app/public/test.webp b/test/development/next-image-new/export-config/public/test.webp similarity index 100% rename from test/integration/image-optimizer/app/public/test.webp rename to test/development/next-image-new/export-config/public/test.webp diff --git a/test/development/next-image-new/invalid-image-import/invalid-image-import.test.ts b/test/development/next-image-new/invalid-image-import/invalid-image-import.test.ts new file mode 100644 index 000000000000..3b217efc41ee --- /dev/null +++ b/test/development/next-image-new/invalid-image-import/invalid-image-import.test.ts @@ -0,0 +1,49 @@ +import { nextTestSetup } from 'e2e-utils' +import { + getRedboxDescription, + getRedboxSource, + waitForRedbox, +} from 'next-test-utils' + +describe('Invalid Image Import (dev)', () => { + const { next, isTurbopack } = nextTestSetup({ + files: __dirname, + }) + + it('should show error', async () => { + const browser = await next.browser('/') + await waitForRedbox(browser) + const description = await getRedboxDescription(browser) + if (isTurbopack) { + expect(description).toContain('Processing image failed') + } else if (process.env.NEXT_RSPACK) { + expect(description).toContain( + 'Image import "../public/invalid.svg" is not a valid image file. The image may be corrupted or an unsupported format.' + ) + } else { + expect(description).toContain( + 'Image import "../public/invalid.svg" is not a valid image file. The image may be corrupted or an unsupported format.' + ) + } + const source = await getRedboxSource(browser) + if (isTurbopack) { + expect(source).toContain('Processing image failed') + expect(source).toContain( + 'Failed to parse svg source code for image dimensions' + ) + expect(source).toContain( + 'Source code does not contain a <svg> root element' + ) + } else if (process.env.NEXT_RSPACK) { + expect(source).toContain('./pages/index.js') + expect(source).toContain( + 'Image import "../public/invalid.svg" is not a valid image file. The image may be corrupted or an unsupported format.' + ) + } else { + expect(source).toContain('./pages/index.js') + expect(source).toContain( + 'Image import "../public/invalid.svg" is not a valid image file. The image may be corrupted or an unsupported format.' + ) + } + }) +}) diff --git a/test/integration/next-image-new/invalid-image-import/pages/index.js b/test/development/next-image-new/invalid-image-import/pages/index.js similarity index 100% rename from test/integration/next-image-new/invalid-image-import/pages/index.js rename to test/development/next-image-new/invalid-image-import/pages/index.js diff --git a/test/integration/next-image-new/invalid-image-import/public/invalid.svg b/test/development/next-image-new/invalid-image-import/public/invalid.svg similarity index 100% rename from test/integration/next-image-new/invalid-image-import/public/invalid.svg rename to test/development/next-image-new/invalid-image-import/public/invalid.svg diff --git a/test/development/next-image-new/middleware/middleware-intercept.test.ts b/test/development/next-image-new/middleware/middleware-intercept.test.ts new file mode 100644 index 000000000000..2bd93e32b31a --- /dev/null +++ b/test/development/next-image-new/middleware/middleware-intercept.test.ts @@ -0,0 +1,20 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('Image is intercepted by Middleware', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should find log from _next/image intercept', async () => { + const browser = await next.browser('/') + + await browser.waitForIdleNetwork() + + await retry(async () => { + expect(next.cliOutput).toContain('GET /') + }) + + expect(next.cliOutput).toContain(`x-_next-image: /small.jpg`) + }) +}) diff --git a/test/integration/next-image-new/middleware/middleware.js b/test/development/next-image-new/middleware/middleware.js similarity index 100% rename from test/integration/next-image-new/middleware/middleware.js rename to test/development/next-image-new/middleware/middleware.js diff --git a/test/development/next-image-new/middleware/middleware.test.ts b/test/development/next-image-new/middleware/middleware.test.ts new file mode 100644 index 000000000000..5a9f76a97825 --- /dev/null +++ b/test/development/next-image-new/middleware/middleware.test.ts @@ -0,0 +1,18 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('Image with middleware in edge func', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should not error', async () => { + await next.browser('/') + await retry(async () => { + expect(next.cliOutput).toContain('GET /') + }) + expect(next.cliOutput).not.toContain( + `'preload' is not exported from 'react-dom'` + ) + }) +}) diff --git a/test/integration/next-image-new/middleware/pages/index.js b/test/development/next-image-new/middleware/pages/index.js similarity index 100% rename from test/integration/next-image-new/middleware/pages/index.js rename to test/development/next-image-new/middleware/pages/index.js diff --git a/test/integration/next-image-legacy/default/public/small.jpg b/test/development/next-image-new/middleware/public/small.jpg similarity index 100% rename from test/integration/next-image-legacy/default/public/small.jpg rename to test/development/next-image-new/middleware/public/small.jpg diff --git a/test/development/no-override-next-props/no-override-next-props.test.ts b/test/development/no-override-next-props/no-override-next-props.test.ts new file mode 100644 index 000000000000..bf418b54a256 --- /dev/null +++ b/test/development/no-override-next-props/no-override-next-props.test.ts @@ -0,0 +1,12 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('no-override-next-props', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should show error when a Next prop is returned in _app.getInitialProps', async () => { + const html = await next.render('/') + expect(html).toMatch(/\/cant-override-next-props/) + }) +}) diff --git a/test/integration/no-override-next-props/pages/_app.js b/test/development/no-override-next-props/pages/_app.js similarity index 100% rename from test/integration/no-override-next-props/pages/_app.js rename to test/development/no-override-next-props/pages/_app.js diff --git a/test/integration/no-override-next-props/pages/index.js b/test/development/no-override-next-props/pages/index.js similarity index 100% rename from test/integration/no-override-next-props/pages/index.js rename to test/development/no-override-next-props/pages/index.js diff --git a/test/integration/ondemand/components/hello.js b/test/development/ondemand/components/hello.js similarity index 100% rename from test/integration/ondemand/components/hello.js rename to test/development/ondemand/components/hello.js diff --git a/test/integration/ondemand/next.config.js b/test/development/ondemand/next.config.js similarity index 100% rename from test/integration/ondemand/next.config.js rename to test/development/ondemand/next.config.js diff --git a/test/development/ondemand/ondemand.test.ts b/test/development/ondemand/ondemand.test.ts new file mode 100644 index 000000000000..6959193e2fdd --- /dev/null +++ b/test/development/ondemand/ondemand.test.ts @@ -0,0 +1,63 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry, shouldUseTurbopack, waitFor } from 'next-test-utils' +;(shouldUseTurbopack() ? describe.skip : describe)('On Demand Entries', () => { + const { next } = nextTestSetup({ + files: __dirname, + startCommand: 'node server.js', + serverReadyPattern: /- Local:/, + dependencies: { + 'get-port': '5.1.1', + }, + }) + + it('should compile pages for SSR', async () => { + const pageContent = await next.render('/') + expect(pageContent).toContain('Index Page') + }) + + it('should compile pages for JSON page requests', async () => { + await next.render('/about') + const manifest = JSON.parse( + await next.readFile('.next/dev/build-manifest.json') + ) + const pageFiles = manifest.pages['/about'] + expect(pageFiles).toBeDefined() + const pageFile = pageFiles[pageFiles.length - 1] + expect(pageFile).toMatch(/\.js$/) + expect(pageFile).toContain('pages/about') + const pageContent = await next.render(`/_next/${pageFile}`) + expect(pageContent).toContain('About Page') + }) + + it('should dispose inactive pages', async () => { + await next.render('/') + + await next.render('/about') + + await next.render('/third') + + for (let i = 0; i < 30; ++i) { + await waitFor(1000) + try { + const manifest = JSON.parse( + await next.readFile('.next/dev/build-manifest.json') + ) + expect(manifest.pages['/']).toBeUndefined() + expect(manifest.pages['/about']).toBeDefined() + expect(manifest.pages['/third']).toBeDefined() + return + } catch { + continue + } + } + }) + + it('should navigate to pages with dynamic imports', async () => { + const browser = await next.browser('/nav') + await browser.eval('document.getElementById("to-dynamic").click()') + await retry(async () => { + const text = await browser.elementByCss('body').text() + expect(text).toMatch(/Hello/) + }) + }) +}) diff --git a/test/integration/ondemand/pages/about.js b/test/development/ondemand/pages/about.js similarity index 100% rename from test/integration/ondemand/pages/about.js rename to test/development/ondemand/pages/about.js diff --git a/test/integration/ondemand/pages/index.js b/test/development/ondemand/pages/index.js similarity index 100% rename from test/integration/ondemand/pages/index.js rename to test/development/ondemand/pages/index.js diff --git a/test/integration/ondemand/pages/nav/dynamic.js b/test/development/ondemand/pages/nav/dynamic.js similarity index 100% rename from test/integration/ondemand/pages/nav/dynamic.js rename to test/development/ondemand/pages/nav/dynamic.js diff --git a/test/integration/ondemand/pages/nav/index.js b/test/development/ondemand/pages/nav/index.js similarity index 100% rename from test/integration/ondemand/pages/nav/index.js rename to test/development/ondemand/pages/nav/index.js diff --git a/test/integration/ondemand/pages/third.js b/test/development/ondemand/pages/third.js similarity index 100% rename from test/integration/ondemand/pages/third.js rename to test/development/ondemand/pages/third.js diff --git a/test/integration/ondemand/server.js b/test/development/ondemand/server.js similarity index 65% rename from test/integration/ondemand/server.js rename to test/development/ondemand/server.js index dc2b784fb485..802144c80a03 100644 --- a/test/integration/ondemand/server.js +++ b/test/development/ondemand/server.js @@ -1,11 +1,11 @@ const http = require('http') const next = require('next') +const getPort = require('get-port') const { assetPrefix } = require('./next.config') const dev = process.env.NODE_ENV !== 'production' const dir = __dirname -const port = process.env.PORT || 3000 function rewriteAssetPrefix(req) { if (assetPrefix) { @@ -16,18 +16,21 @@ function rewriteAssetPrefix(req) { const app = next({ dev, dir }) const nextReqHandler = app.getRequestHandler() -app.prepare().then(() => { +async function main() { + await app.prepare() + const port = await getPort() + const server = new http.Server((req, res) => { rewriteAssetPrefix(req) - return nextReqHandler(req, res) }) - server.listen(port, (err) => { - if (err) { - throw err - } - - console.log(`> Ready on http://localhost:${port}`) + server.listen(port, () => { + console.log(`- Local: http://localhost:${port}`) }) +} + +main().catch((err) => { + console.error(err) + process.exit(1) }) diff --git a/test/integration/plugin-mdx-rs/components/button.js b/test/development/plugin-mdx-rs/components/button.js similarity index 100% rename from test/integration/plugin-mdx-rs/components/button.js rename to test/development/plugin-mdx-rs/components/button.js diff --git a/test/integration/plugin-mdx-rs/components/marker.js b/test/development/plugin-mdx-rs/components/marker.js similarity index 100% rename from test/integration/plugin-mdx-rs/components/marker.js rename to test/development/plugin-mdx-rs/components/marker.js diff --git a/test/integration/plugin-mdx-rs/mdx-components.js b/test/development/plugin-mdx-rs/mdx-components.js similarity index 100% rename from test/integration/plugin-mdx-rs/mdx-components.js rename to test/development/plugin-mdx-rs/mdx-components.js diff --git a/test/integration/plugin-mdx-rs/next.config.js b/test/development/plugin-mdx-rs/next.config.js similarity index 100% rename from test/integration/plugin-mdx-rs/next.config.js rename to test/development/plugin-mdx-rs/next.config.js diff --git a/test/integration/plugin-mdx-rs/pages/button.mdx b/test/development/plugin-mdx-rs/pages/button.mdx similarity index 100% rename from test/integration/plugin-mdx-rs/pages/button.mdx rename to test/development/plugin-mdx-rs/pages/button.mdx diff --git a/test/integration/plugin-mdx-rs/pages/gfm.mdx b/test/development/plugin-mdx-rs/pages/gfm.mdx similarity index 100% rename from test/integration/plugin-mdx-rs/pages/gfm.mdx rename to test/development/plugin-mdx-rs/pages/gfm.mdx diff --git a/test/integration/plugin-mdx-rs/pages/index.mdx b/test/development/plugin-mdx-rs/pages/index.mdx similarity index 100% rename from test/integration/plugin-mdx-rs/pages/index.mdx rename to test/development/plugin-mdx-rs/pages/index.mdx diff --git a/test/integration/plugin-mdx-rs/pages/provider.mdx b/test/development/plugin-mdx-rs/pages/provider.mdx similarity index 100% rename from test/integration/plugin-mdx-rs/pages/provider.mdx rename to test/development/plugin-mdx-rs/pages/provider.mdx diff --git a/test/development/plugin-mdx-rs/plugin-mdx-rs.test.ts b/test/development/plugin-mdx-rs/plugin-mdx-rs.test.ts new file mode 100644 index 000000000000..9d5e031ac93f --- /dev/null +++ b/test/development/plugin-mdx-rs/plugin-mdx-rs.test.ts @@ -0,0 +1,60 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('MDX-rs Plugin support', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: { + '@next/mdx': 'canary', + '@mdx-js/loader': '*', + '@mdx-js/react': '*', + }, + }) + + it('should render an MDX page correctly', async () => { + expect(await next.render('/')).toMatch(/Hello MDX/) + }) + + it('should render an MDX page with component correctly', async () => { + expect(await next.render('/button')).toMatch(/Look, a button!/) + }) + + it('should render an MDX page with globally provided components (from `mdx-components.js`) correctly', async () => { + expect(await next.render('/provider')).toMatch(/Marker was rendered!/) + }) +}) + +describe('MDX-rs Plugin support with mdx transform options', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: { + '@next/mdx': 'canary', + '@mdx-js/loader': '*', + '@mdx-js/react': '*', + }, + skipStart: true, + }) + + beforeAll(async () => { + await next.patchFile( + 'next.config.js', + ` + const withMDX = require('@next/mdx')({ + extension: /\\.mdx?$/, + }) + module.exports = withMDX({ + pageExtensions: ['js', 'jsx', 'mdx'], + experimental: { + mdxRs: { + mdxType: 'gfm' + }, + }, + }) + ` + ) + await next.start() + }) + + it('should render an MDX page correctly', async () => { + expect(await next.render('/gfm')).toMatch(/<table>\n<thead>\n<tr>\n<th>foo/) + }) +}) diff --git a/test/integration/prerender/pages/blog/[post]/index.js b/test/development/prerender/pages/blog/[post]/index.js similarity index 100% rename from test/integration/prerender/pages/blog/[post]/index.js rename to test/development/prerender/pages/blog/[post]/index.js diff --git a/test/development/prerender/prerender.test.ts b/test/development/prerender/prerender.test.ts new file mode 100644 index 000000000000..44b3c7788993 --- /dev/null +++ b/test/development/prerender/prerender.test.ts @@ -0,0 +1,54 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('SSG Prerender', () => { + describe('development mode getStaticPaths', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: { + firebase: '7.14.5', + }, + nextConfig: { + experimental: { + cpus: 1, + }, + }, + }) + + it('should work with firebase import and getStaticPaths', async () => { + const html = await next.render('/blog/post-1') + expect(html).toContain('post-1') + expect(html).not.toContain('Error: Failed to load') + + const html2 = await next.render('/blog/post-1') + expect(html2).toContain('post-1') + expect(html2).not.toContain('Error: Failed to load') + }) + + it('should not cache getStaticPaths errors', async () => { + const errMsg = /The `fallback` key must be returned from getStaticPaths/ + + await retry(async () => { + const html = await next.render('/blog/post-1') + expect(html).toMatch(/post-1/) + }) + + await next.patchFile( + 'pages/blog/[post]/index.js', + (content) => + content!.replace('fallback: true,', '/* fallback: true, */'), + async () => { + await retry(async () => { + const html = await next.render('/blog/post-1') + expect(html).toMatch(errMsg) + }) + } + ) + + await retry(async () => { + const html = await next.render('/blog/post-1') + expect(html).toMatch(/post-1/) + }) + }) + }) +}) diff --git a/test/integration/server-side-dev-errors/pages/api/blog/[slug].js b/test/development/server-side-dev-errors/pages/api/blog/[slug].js similarity index 100% rename from test/integration/server-side-dev-errors/pages/api/blog/[slug].js rename to test/development/server-side-dev-errors/pages/api/blog/[slug].js diff --git a/test/integration/server-side-dev-errors/pages/api/hello.js b/test/development/server-side-dev-errors/pages/api/hello.js similarity index 100% rename from test/integration/server-side-dev-errors/pages/api/hello.js rename to test/development/server-side-dev-errors/pages/api/hello.js diff --git a/test/integration/server-side-dev-errors/pages/blog/[slug].js b/test/development/server-side-dev-errors/pages/blog/[slug].js similarity index 100% rename from test/integration/server-side-dev-errors/pages/blog/[slug].js rename to test/development/server-side-dev-errors/pages/blog/[slug].js diff --git a/test/integration/server-side-dev-errors/pages/gsp.js b/test/development/server-side-dev-errors/pages/gsp.js similarity index 100% rename from test/integration/server-side-dev-errors/pages/gsp.js rename to test/development/server-side-dev-errors/pages/gsp.js diff --git a/test/integration/server-side-dev-errors/pages/gssp.js b/test/development/server-side-dev-errors/pages/gssp.js similarity index 100% rename from test/integration/server-side-dev-errors/pages/gssp.js rename to test/development/server-side-dev-errors/pages/gssp.js diff --git a/test/integration/server-side-dev-errors/pages/uncaught-empty-exception.js b/test/development/server-side-dev-errors/pages/uncaught-empty-exception.js similarity index 100% rename from test/integration/server-side-dev-errors/pages/uncaught-empty-exception.js rename to test/development/server-side-dev-errors/pages/uncaught-empty-exception.js diff --git a/test/integration/server-side-dev-errors/pages/uncaught-empty-rejection.js b/test/development/server-side-dev-errors/pages/uncaught-empty-rejection.js similarity index 100% rename from test/integration/server-side-dev-errors/pages/uncaught-empty-rejection.js rename to test/development/server-side-dev-errors/pages/uncaught-empty-rejection.js diff --git a/test/integration/server-side-dev-errors/pages/uncaught-exception.js b/test/development/server-side-dev-errors/pages/uncaught-exception.js similarity index 100% rename from test/integration/server-side-dev-errors/pages/uncaught-exception.js rename to test/development/server-side-dev-errors/pages/uncaught-exception.js diff --git a/test/integration/server-side-dev-errors/pages/uncaught-rejection.js b/test/development/server-side-dev-errors/pages/uncaught-rejection.js similarity index 100% rename from test/integration/server-side-dev-errors/pages/uncaught-rejection.js rename to test/development/server-side-dev-errors/pages/uncaught-rejection.js diff --git a/test/development/server-side-dev-errors/server-side-dev-errors.test.ts b/test/development/server-side-dev-errors/server-side-dev-errors.test.ts new file mode 100644 index 000000000000..3a536cc2a321 --- /dev/null +++ b/test/development/server-side-dev-errors/server-side-dev-errors.test.ts @@ -0,0 +1,454 @@ +import { nextTestSetup } from 'e2e-utils' +import { waitForNoRedbox, retry } from 'next-test-utils' +import stripAnsi from 'strip-ansi' + +describe('server-side dev errors', () => { + const { next, isTurbopack } = nextTestSetup({ + files: __dirname, + }) + + function stripInternalHandler(output) { + return output + .replace(/Creating turbopack project \{[\s\S]*?\}\s*/g, '') + .replace(/WARNING: The git repository is dirty[^\n]*\n?/g, '') + .replace(/.*at async handler .*next-route-loader.*/g, '') + .replace(/.*at async handleResponse.*/g, '') + .replace(/.*at async doRender \(.*/g, '') + .split(/\n/) + .filter((item) => { + const trimmed = item.trim() + if (!trimmed) return false + // Drop bootstrap/startup banner lines that may appear after + // `next.cliOutput` was sliced. The Experiments banner is logged + // asynchronously after the dev server reports ready (see + // `logExperimentalInfo` in `start-server.ts`), so it can race + // with the test capturing `cliOutputIdx`. + if (trimmed.startsWith('- ')) return false + if (/^[✓⚠△] /.test(trimmed)) return false + // Drop compiling indicator lines (e.g. "○ Compiling /gsp ..."). + if (trimmed.startsWith('○ ')) return false + return true + }) + .join('\n') + } + + it('should show server-side error for gsp page correctly', async () => { + const content = await next.readFile('pages/gsp.js') + + try { + const cliOutputIdx = next.cliOutput.length + await next.patchFile( + 'pages/gsp.js', + content.replace('return {', 'missingVar;return {') + ) + const browser = await next.browser('/gsp') + + await retry(() => { + expect(next.cliOutput.slice(cliOutputIdx)).toContain( + 'ReferenceError: missingVar is not defined' + ) + }) + + const stderrOutput = stripInternalHandler( + stripAnsi(next.cliOutput.slice(cliOutputIdx)).trim() + ) + + expect(stderrOutput).toStartWith( + '⨯ ReferenceError: missingVar is not defined\n at getStaticProps' + ) + expect(stderrOutput).toContain('gsp.js:6:3') + expect(stderrOutput).toContain( + ' 5 | export async function getStaticProps() {\n' + + '> 6 | missingVar;return {\n' + + ' | ^' + ) + + await expect(browser).toDisplayRedbox(` + { + "code": "E394", + "description": "missingVar is not defined", + "environmentLabel": null, + "label": "Runtime ReferenceError", + "source": "pages/gsp.js (6:3) @ getStaticProps + > 6 | missingVar;return { + | ^", + "stack": [ + "getStaticProps pages/gsp.js (6:3)", + ], + } + `) + + await next.patchFile('pages/gsp.js', content) + await waitForNoRedbox(browser) + } finally { + await next.patchFile('pages/gsp.js', content) + } + }) + + it('should show server-side error for gssp page correctly', async () => { + const content = await next.readFile('pages/gssp.js') + + try { + const cliOutputIdx = next.cliOutput.length + await next.patchFile( + 'pages/gssp.js', + content.replace('return {', 'missingVar;return {') + ) + const browser = await next.browser('/gssp') + + await retry(() => { + expect(next.cliOutput.slice(cliOutputIdx)).toContain( + 'ReferenceError: missingVar is not defined' + ) + }) + + const stderrOutput = stripInternalHandler( + stripAnsi(next.cliOutput.slice(cliOutputIdx)).trim() + ) + expect(stderrOutput).toStartWith( + '⨯ ReferenceError: missingVar is not defined\n at getServerSideProps' + ) + expect(stderrOutput).toContain('gssp.js:6:3') + expect(stderrOutput).toContain( + ' 5 | export async function getServerSideProps() {\n' + + '> 6 | missingVar;return {\n' + + ' | ^' + ) + + await expect(browser).toDisplayRedbox(` + { + "code": "E394", + "description": "missingVar is not defined", + "environmentLabel": null, + "label": "Runtime ReferenceError", + "source": "pages/gssp.js (6:3) @ getServerSideProps + > 6 | missingVar;return { + | ^", + "stack": [ + "getServerSideProps pages/gssp.js (6:3)", + ], + } + `) + + await next.patchFile('pages/gssp.js', content) + await waitForNoRedbox(browser) + } finally { + await next.patchFile('pages/gssp.js', content) + } + }) + + it('should show server-side error for dynamic gssp page correctly', async () => { + const content = await next.readFile('pages/blog/[slug].js') + + try { + const cliOutputIdx = next.cliOutput.length + await next.patchFile( + 'pages/blog/[slug].js', + content.replace('return {', 'missingVar;return {') + ) + const browser = await next.browser('/blog/first') + + await retry(() => { + expect(next.cliOutput.slice(cliOutputIdx)).toContain( + 'ReferenceError: missingVar is not defined' + ) + }) + + const stderrOutput = stripInternalHandler( + stripAnsi(next.cliOutput.slice(cliOutputIdx)).trim() + ) + expect(stderrOutput).toStartWith( + '⨯ ReferenceError: missingVar is not defined\n at getServerSideProps' + ) + expect(stderrOutput).toContain('[slug].js:6:3') + expect(stderrOutput).toContain( + ' 5 | export async function getServerSideProps() {\n' + + '> 6 | missingVar;return {\n' + + ' | ^' + ) + + await expect(browser).toDisplayRedbox(` + { + "code": "E394", + "description": "missingVar is not defined", + "environmentLabel": null, + "label": "Runtime ReferenceError", + "source": "pages/blog/[slug].js (6:3) @ getServerSideProps + > 6 | missingVar;return { + | ^", + "stack": [ + "getServerSideProps pages/blog/[slug].js (6:3)", + ], + } + `) + + await next.patchFile('pages/blog/[slug].js', content) + } finally { + await next.patchFile('pages/blog/[slug].js', content) + } + }) + + it('should show server-side error for api route correctly', async () => { + const content = await next.readFile('pages/api/hello.js') + + try { + const cliOutputIdx = next.cliOutput.length + await next.patchFile( + 'pages/api/hello.js', + content.replace('res.status', 'missingVar;res.status') + ) + const browser = await next.browser('/api/hello') + + await retry(() => { + expect(next.cliOutput.slice(cliOutputIdx)).toContain( + 'ReferenceError: missingVar is not defined' + ) + }) + + const stderrOutput = stripAnsi(next.cliOutput.slice(cliOutputIdx)).trim() + if (isTurbopack) { + expect(stderrOutput).toStartWith( + '⨯ ReferenceError: missingVar is not defined\n at handler' + ) + expect(stderrOutput).toContain('hello.js:2:3') + expect(stderrOutput).toContain( + ' 1 | export default function handler(req, res) {\n' + + "> 2 | missingVar;res.status(200).json({ hello: 'world' })\n" + + ' | ^' + ) + } else { + expect(stderrOutput).toStartWith( + '⨯ ReferenceError: missingVar is not defined\n at handler' + ) + expect(stderrOutput).toContain('hello.js:2:3') + // TODO(veil): Why not ignore-listed? + expect(stderrOutput).toContain('\n at ') + expect(stderrOutput).toContain( + ' 1 | export default function handler(req, res) {\n' + + "> 2 | missingVar;res.status(200).json({ hello: 'world' })\n" + + ' | ^' + ) + } + + await expect(browser).toDisplayRedbox(` + { + "code": "E394", + "description": "missingVar is not defined", + "environmentLabel": null, + "label": "Runtime ReferenceError", + "source": "pages/api/hello.js (2:3) @ handler + > 2 | missingVar;res.status(200).json({ hello: 'world' }) + | ^", + "stack": [ + "handler pages/api/hello.js (2:3)", + ], + } + `) + + await next.patchFile('pages/api/hello.js', content) + + await retry(async () => { + await browser.refresh() + await waitForNoRedbox(browser) + }) + } finally { + await next.patchFile('pages/api/hello.js', content) + } + }) + + it('should show server-side error for dynamic api route correctly', async () => { + const content = await next.readFile('pages/api/blog/[slug].js') + + try { + const cliOutputIdx = next.cliOutput.length + await next.patchFile( + 'pages/api/blog/[slug].js', + content.replace('res.status', 'missingVar;res.status') + ) + const browser = await next.browser('/api/blog/first') + + await retry(() => { + expect(next.cliOutput.slice(cliOutputIdx)).toContain( + 'ReferenceError: missingVar is not defined' + ) + }) + + const stderrOutput = stripAnsi(next.cliOutput.slice(cliOutputIdx)).trim() + if (isTurbopack) { + expect(stderrOutput).toStartWith( + '⨯ ReferenceError: missingVar is not defined\n at handler' + ) + expect(stderrOutput).toContain('[slug].js:2:3') + expect(stderrOutput).toContain( + ' 1 | export default function handler(req, res) {\n' + + '> 2 | missingVar;res.status(200).json({ slug: req.query.slug })\n' + + ' | ^' + ) + } else { + expect(stderrOutput).toStartWith( + '⨯ ReferenceError: missingVar is not defined\n at handler' + ) + expect(stderrOutput).toContain('[slug].js:2:3') + // TODO(veil): Why not ignore-listed? + expect(stderrOutput).toContain('\n at') + expect(stderrOutput).toContain( + ' 1 | export default function handler(req, res) {\n' + + '> 2 | missingVar;res.status(200).json({ slug: req.query.slug })\n' + + ' | ^' + ) + } + + await expect(browser).toDisplayRedbox(` + { + "code": "E394", + "description": "missingVar is not defined", + "environmentLabel": null, + "label": "Runtime ReferenceError", + "source": "pages/api/blog/[slug].js (2:3) @ handler + > 2 | missingVar;res.status(200).json({ slug: req.query.slug }) + | ^", + "stack": [ + "handler pages/api/blog/[slug].js (2:3)", + ], + } + `) + + await next.patchFile('pages/api/blog/[slug].js', content) + + await retry(async () => { + await browser.refresh() + await waitForNoRedbox(browser) + }) + } finally { + await next.patchFile('pages/api/blog/[slug].js', content) + } + }) + + it('should show server-side error for uncaught rejection correctly', async () => { + const cliOutputIdx = next.cliOutput.length + await next.browser('/uncaught-rejection') + + await retry(() => { + expect(next.cliOutput.slice(cliOutputIdx)).toContain( + 'Error: catch this rejection' + ) + }) + + const stderrOutput = stripAnsi(next.cliOutput.slice(cliOutputIdx)) + .replace( + '⚠ Fast Refresh had to perform a full reload due to a Runtime ReferenceError.', + '' + ) + .trim() + + // FIXME(veil): error repeated + expect(stderrOutput).toContain('Error: catch this rejection') + expect(stderrOutput).toContain('uncaught-rejection.js:7:20') + if (isTurbopack) { + expect(stderrOutput).toContain('at Timeout._onTimeout') + } else { + expect(stderrOutput).toContain('at Timeout.eval [as _onTimeout]') + } + expect(stderrOutput).toContain( + ' 5 | export async function getServerSideProps() {\n' + + ' 6 | setTimeout(() => {\n' + + "> 7 | Promise.reject(new Error('catch this rejection'))" + ) + expect(stderrOutput).toContain( + '⨯ unhandledRejection: Error: catch this rejection' + ) + }) + + it('should show server-side error for uncaught empty rejection correctly', async () => { + const cliOutputIdx = next.cliOutput.length + await next.browser('/uncaught-empty-rejection') + + await retry(() => { + expect(next.cliOutput.slice(cliOutputIdx)).toContain('Error:') + }) + + const stderrOutput = stripAnsi(next.cliOutput.slice(cliOutputIdx)) + .replace( + '⚠ Fast Refresh had to perform a full reload due to a Runtime ReferenceError.', + '' + ) + .trim() + + // FIXME(veil): error repeated + expect(stderrOutput).toContain('uncaught-empty-rejection.js:7:20') + if (isTurbopack) { + expect(stderrOutput).toContain('at Timeout._onTimeout') + } else { + expect(stderrOutput).toContain('at Timeout.eval [as _onTimeout]') + } + expect(stderrOutput).toContain( + ' 5 | export async function getServerSideProps() {\n' + + ' 6 | setTimeout(() => {\n' + + '> 7 | Promise.reject(new Error())' + ) + expect(stderrOutput).toContain('⨯ unhandledRejection: Error:') + }) + + it('should show server-side error for uncaught exception correctly', async () => { + const cliOutputIdx = next.cliOutput.length + await next.browser('/uncaught-exception') + + await retry(() => { + expect(next.cliOutput.slice(cliOutputIdx)).toContain('Error:') + }) + + const stderrOutput = stripAnsi(next.cliOutput.slice(cliOutputIdx)) + .replace( + '⚠ Fast Refresh had to perform a full reload due to a Runtime ReferenceError.', + '' + ) + .trim() + + // FIXME(veil): error repeated + expect(stderrOutput).toContain('Error: catch this exception') + expect(stderrOutput).toContain('uncaught-exception.js:7:11') + if (isTurbopack) { + expect(stderrOutput).toContain('at Timeout._onTimeout') + } else { + expect(stderrOutput).toContain('at Timeout.eval [as _onTimeout]') + } + expect(stderrOutput).toContain( + ' 5 | export async function getServerSideProps() {\n' + + ' 6 | setTimeout(() => {\n' + + "> 7 | throw new Error('catch this exception')" + ) + expect(stderrOutput).toContain( + '⨯ uncaughtException: Error: catch this exception' + ) + }) + + it('should show server-side error for uncaught empty exception correctly', async () => { + const cliOutputIdx = next.cliOutput.length + await next.browser('/uncaught-empty-exception') + + await retry(() => { + expect(next.cliOutput.slice(cliOutputIdx)).toContain('Error:') + }) + + const stderrOutput = stripAnsi(next.cliOutput.slice(cliOutputIdx)) + .replace( + '⚠ Fast Refresh had to perform a full reload due to a Runtime ReferenceError.', + '' + ) + .trim() + + // FIXME(veil): error repeated + expect(stderrOutput).toContain('uncaught-empty-exception.js:7:11') + if (isTurbopack) { + expect(stderrOutput).toContain('at Timeout._onTimeout') + } else { + expect(stderrOutput).toContain('at Timeout.eval [as _onTimeout]') + } + expect(stderrOutput).toContain( + ' 5 | export async function getServerSideProps() {\n' + + ' 6 | setTimeout(() => {\n' + + '> 7 | throw new Error()' + ) + expect(stderrOutput).toContain('⨯ uncaughtException: Error:') + }) +}) diff --git a/test/integration/trailing-slash-dist/next.config.js b/test/development/trailing-slash-dist/next.config.js similarity index 100% rename from test/integration/trailing-slash-dist/next.config.js rename to test/development/trailing-slash-dist/next.config.js diff --git a/test/integration/404-page-app/pages/index.js b/test/development/trailing-slash-dist/pages/index.js similarity index 100% rename from test/integration/404-page-app/pages/index.js rename to test/development/trailing-slash-dist/pages/index.js diff --git a/test/development/trailing-slash-dist/trailing-slash-dist.test.ts b/test/development/trailing-slash-dist/trailing-slash-dist.test.ts new file mode 100644 index 000000000000..8e03a2903c17 --- /dev/null +++ b/test/development/trailing-slash-dist/trailing-slash-dist.test.ts @@ -0,0 +1,17 @@ +import { nextTestSetup } from 'e2e-utils' +import { join } from 'path' +import { getPageFileFromBuildManifest } from 'next-test-utils' + +describe('trailing-slash-dist', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('supports trailing slash in distDir', async () => { + // Make sure the page is rendered before getting the file + await next.render('/') + const file = getPageFileFromBuildManifest(next.testDir, '/') + const res = await next.fetch(join('/_next', file)) + expect(res.status).toBe(200) + }) +}) diff --git a/test/integration/404-page/next.config.js b/test/development/turbopack-unsupported-log/fixtures/empty-config/next.config.js similarity index 100% rename from test/integration/404-page/next.config.js rename to test/development/turbopack-unsupported-log/fixtures/empty-config/next.config.js diff --git a/test/integration/turbopack-unsupported-log/app/pages/index.js b/test/development/turbopack-unsupported-log/fixtures/empty-config/pages/index.js similarity index 100% rename from test/integration/turbopack-unsupported-log/app/pages/index.js rename to test/development/turbopack-unsupported-log/fixtures/empty-config/pages/index.js diff --git a/test/development/turbopack-unsupported-log/fixtures/no-config/pages/index.js b/test/development/turbopack-unsupported-log/fixtures/no-config/pages/index.js new file mode 100644 index 000000000000..ff7159d9149f --- /dev/null +++ b/test/development/turbopack-unsupported-log/fixtures/no-config/pages/index.js @@ -0,0 +1,3 @@ +export default function Page() { + return <p>hello world</p> +} diff --git a/test/integration/script-loader/partytown/next.config.js b/test/development/turbopack-unsupported-log/fixtures/unsupported-config/next.config.js similarity index 60% rename from test/integration/script-loader/partytown/next.config.js rename to test/development/turbopack-unsupported-log/fixtures/unsupported-config/next.config.js index 4e1d99a3bdbd..8e2d3b72dc5d 100644 --- a/test/integration/script-loader/partytown/next.config.js +++ b/test/development/turbopack-unsupported-log/fixtures/unsupported-config/next.config.js @@ -1,5 +1,5 @@ module.exports = { experimental: { - nextScriptWorkers: true, + urlImports: true, }, } diff --git a/test/development/turbopack-unsupported-log/fixtures/unsupported-config/pages/index.js b/test/development/turbopack-unsupported-log/fixtures/unsupported-config/pages/index.js new file mode 100644 index 000000000000..ff7159d9149f --- /dev/null +++ b/test/development/turbopack-unsupported-log/fixtures/unsupported-config/pages/index.js @@ -0,0 +1,3 @@ +export default function Page() { + return <p>hello world</p> +} diff --git a/test/development/turbopack-unsupported-log/turbopack-unsupported-log.test.ts b/test/development/turbopack-unsupported-log/turbopack-unsupported-log.test.ts new file mode 100644 index 000000000000..222472afd071 --- /dev/null +++ b/test/development/turbopack-unsupported-log/turbopack-unsupported-log.test.ts @@ -0,0 +1,65 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' +import path from 'path' + +const reactDependencies = { + react: '19.3.0-canary-fef12a01-20260413', + 'react-dom': '19.3.0-canary-fef12a01-20260413', +} + +// This test only applies to Turbopack +;(!process.env.IS_TURBOPACK_TEST ? describe.skip : describe)( + 'turbopack unsupported features log', + () => { + describe('no config', () => { + const { next } = nextTestSetup({ + files: path.join(__dirname, 'fixtures/no-config'), + dependencies: reactDependencies, + }) + + it('should not warn by default', async () => { + const html = await next.render('/') + expect(html).toContain('hello world') + expect(next.cliOutput).toContain('(Turbopack)') + expect(next.cliOutput).not.toContain( + 'You are using configuration and/or tools that are not yet' + ) + }) + }) + + describe('empty config', () => { + const { next } = nextTestSetup({ + files: path.join(__dirname, 'fixtures/empty-config'), + dependencies: reactDependencies, + }) + + it('should not warn with empty next.config.js', async () => { + const html = await next.render('/') + expect(html).toContain('hello world') + expect(next.cliOutput).toContain('(Turbopack)') + expect(next.cliOutput).not.toContain( + 'You are using configuration and/or tools that are not yet' + ) + }) + }) + + describe('unsupported config', () => { + const { next } = nextTestSetup({ + files: path.join(__dirname, 'fixtures/unsupported-config'), + dependencies: reactDependencies, + }) + + it('should warn with next.config.js with unsupported field', async () => { + // Warning is emitted lazily when a request is served, so we need to + // hit the server before asserting on the CLI output. + await next.render('/') + await retry(async () => { + expect(next.cliOutput).toContain('(Turbopack)') + expect(next.cliOutput).toContain( + 'You are using configuration and/or tools that are not yet' + ) + }) + }) + }) + } +) diff --git a/test/integration/custom-server-types/next-env.d.ts b/test/development/typescript-app-type-declarations/next-env.strictRouteTypes.d.ts similarity index 75% rename from test/integration/custom-server-types/next-env.d.ts rename to test/development/typescript-app-type-declarations/next-env.strictRouteTypes.d.ts index 83311bdfa264..9f660a20db48 100644 --- a/test/integration/custom-server-types/next-env.d.ts +++ b/test/development/typescript-app-type-declarations/next-env.strictRouteTypes.d.ts @@ -1,6 +1,8 @@ /// <reference types="next" /> /// <reference types="next/image-types/global" /> import './.next/dev/types/routes.d.ts' +import './.next/dev/types/cache-life.d.ts' +import './.next/dev/types/validator.ts' // NOTE: This file should not be edited // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/test/integration/typescript-app-type-declarations/pages/index.tsx b/test/development/typescript-app-type-declarations/pages/index.tsx similarity index 100% rename from test/integration/typescript-app-type-declarations/pages/index.tsx rename to test/development/typescript-app-type-declarations/pages/index.tsx diff --git a/test/development/typescript-app-type-declarations/typescript-app-type-declarations.test.ts b/test/development/typescript-app-type-declarations/typescript-app-type-declarations.test.ts new file mode 100644 index 000000000000..09e96a0dc2c2 --- /dev/null +++ b/test/development/typescript-app-type-declarations/typescript-app-type-declarations.test.ts @@ -0,0 +1,106 @@ +import fs from 'fs' +import path from 'path' +import { nextTestSetup } from 'e2e-utils' +import { retry, waitFor } from 'next-test-utils' + +// When `__NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true` is set in CI, Next.js +// regenerates `next-env.d.ts` with additional `cache-life`/`validator` +// imports. The seed fixture must match that output so the very first read of +// `next-env.d.ts` (before any regeneration is observed) lines up with what +// Next.js will write back during the test. +const strictRouteTypes = + process.env.__NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES === 'true' + +const nextEnvDts = strictRouteTypes + ? `/// <reference types="next" /> +/// <reference types="next/image-types/global" /> +import "./.next/dev/types/routes.d.ts"; +import "./.next/dev/types/cache-life.d.ts"; +import "./.next/dev/types/validator.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. +` + : `/// <reference types="next" /> +/// <reference types="next/image-types/global" /> +import "./.next/dev/types/routes.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. +` + +describe('typescript-app-type-declarations', () => { + const { next } = nextTestSetup({ + files: { + 'pages/index.tsx': ` + export default function Index() { + return <div /> + } + `, + 'tsconfig.json': JSON.stringify({ + compilerOptions: { + esModuleInterop: true, + module: 'esnext', + jsx: 'react-jsx', + target: 'es2017', + lib: ['dom', 'dom.iterable', 'esnext'], + allowJs: true, + skipLibCheck: true, + strict: true, + forceConsistentCasingInFileNames: true, + noEmit: true, + incremental: true, + moduleResolution: 'bundler', + resolveJsonModule: true, + isolatedModules: true, + }, + exclude: ['node_modules', '**/*.test.ts', '**/*.test.tsx'], + include: ['next-env.d.ts', 'components', 'pages'], + }), + 'next-env.d.ts': nextEnvDts, + }, + dependencies: { + typescript: 'latest', + '@types/react': 'latest', + '@types/node': 'latest', + }, + }) + + it('should write a new next-env.d.ts if none exist', async () => { + const prevContent = await next.readFile('next-env.d.ts') + await next.deleteFile('next-env.d.ts') + // Next.js writes next-env.d.ts during dev server startup, so restart + // the server to trigger regeneration (matching the original integration + // test which started a fresh server per test). + await next.stop() + await next.start() + await next.render('/') + await retry(async () => { + const content = await next.readFile('next-env.d.ts') + expect(content).toEqual(prevContent) + }) + }) + + it('should overwrite next-env.d.ts if an incorrect one exists', async () => { + const prevContent = await next.readFile('next-env.d.ts') + await next.patchFile('next-env.d.ts', prevContent + 'modification') + await next.stop() + await next.start() + await next.render('/') + await retry(async () => { + const content = await next.readFile('next-env.d.ts') + expect(content).toEqual(prevContent) + }) + }) + + it('should not touch an existing correct next-env.d.ts', async () => { + const envFile = path.join(next.testDir, 'next-env.d.ts') + const prevContent = await next.readFile('next-env.d.ts') + await next.patchFile('next-env.d.ts', prevContent) + const prevStat = fs.statSync(envFile) + await waitFor(1000) + await next.render('/') + const stat = fs.statSync(envFile) + expect(stat.mtimeMs).toEqual(prevStat.mtimeMs) + }) +}) diff --git a/test/development/typescript-external-dir/.gitignore b/test/development/typescript-external-dir/.gitignore new file mode 100644 index 000000000000..fcd2f1ddfa50 --- /dev/null +++ b/test/development/typescript-external-dir/.gitignore @@ -0,0 +1,7 @@ +# Re-allow tsconfig.json fixtures excluded by the broader +# `development/**/tsconfig.json` rule in `test/.gitignore`. These two +# files contain non-default settings (e.g. `paths`) that the test +# relies on; without them, Next.js auto-generates a minimal +# `tsconfig.json` on CI and the test fails. +!project/tsconfig.json +!shared/tsconfig.json diff --git a/test/integration/typescript-external-dir/project/components/world.tsx b/test/development/typescript-external-dir/project/components/world.tsx similarity index 100% rename from test/integration/typescript-external-dir/project/components/world.tsx rename to test/development/typescript-external-dir/project/components/world.tsx diff --git a/test/integration/typescript-external-dir/project/next.config.js b/test/development/typescript-external-dir/project/next.config.js similarity index 100% rename from test/integration/typescript-external-dir/project/next.config.js rename to test/development/typescript-external-dir/project/next.config.js diff --git a/test/integration/typescript-external-dir/project/pages/index.tsx b/test/development/typescript-external-dir/project/pages/index.tsx similarity index 100% rename from test/integration/typescript-external-dir/project/pages/index.tsx rename to test/development/typescript-external-dir/project/pages/index.tsx diff --git a/test/integration/typescript-external-dir/project/tsconfig.json b/test/development/typescript-external-dir/project/tsconfig.json similarity index 100% rename from test/integration/typescript-external-dir/project/tsconfig.json rename to test/development/typescript-external-dir/project/tsconfig.json diff --git a/test/integration/typescript-external-dir/shared/components/counter.tsx b/test/development/typescript-external-dir/shared/components/counter.tsx similarity index 100% rename from test/integration/typescript-external-dir/shared/components/counter.tsx rename to test/development/typescript-external-dir/shared/components/counter.tsx diff --git a/test/integration/typescript-external-dir/shared/libs/inc.ts b/test/development/typescript-external-dir/shared/libs/inc.ts similarity index 100% rename from test/integration/typescript-external-dir/shared/libs/inc.ts rename to test/development/typescript-external-dir/shared/libs/inc.ts diff --git a/test/integration/typescript-external-dir/shared/tsconfig.json b/test/development/typescript-external-dir/shared/tsconfig.json similarity index 100% rename from test/integration/typescript-external-dir/shared/tsconfig.json rename to test/development/typescript-external-dir/shared/tsconfig.json diff --git a/test/development/typescript-external-dir/typescript-external-dir.test.ts b/test/development/typescript-external-dir/typescript-external-dir.test.ts new file mode 100644 index 000000000000..a063d687fe53 --- /dev/null +++ b/test/development/typescript-external-dir/typescript-external-dir.test.ts @@ -0,0 +1,30 @@ +import { join } from 'path' +import { FileRef, nextTestSetup } from 'e2e-utils' +import { shouldUseTurbopack } from 'next-test-utils' + +describe('typescript-external-dir', () => { + const { next } = nextTestSetup({ + files: { + project: new FileRef(join(__dirname, 'project')), + shared: new FileRef(join(__dirname, 'shared')), + }, + // Run Next.js from inside `project/` so its `tsconfig.json` `paths` + // resolve correctly. `shared/` is a sibling at the install root, which + // contains the lockfile, so Turbopack's `rootPath` includes both + // directories and `experimental.externalDir` resolves + // `../../shared/*` from `project/pages`. + packageJson: { + scripts: { + 'dev-project': `cd project && next dev${ + shouldUseTurbopack() ? ' --turbopack' : '' + }`, + }, + }, + startCommand: 'pnpm run dev-project', + }) + + it('should render the page with external TS/TSX dependencies', async () => { + const $ = await next.render$('/') + expect($('body').text()).toMatch(/Hello World!Counter: 0/) + }) +}) diff --git a/test/integration/empty-project/next.config.js b/test/development/typescript-hmr/next.config.js similarity index 100% rename from test/integration/empty-project/next.config.js rename to test/development/typescript-hmr/next.config.js diff --git a/test/integration/typescript-hmr/pages/hello.tsx b/test/development/typescript-hmr/pages/hello.tsx similarity index 100% rename from test/integration/typescript-hmr/pages/hello.tsx rename to test/development/typescript-hmr/pages/hello.tsx diff --git a/test/integration/typescript-hmr/pages/type-error-recover.tsx b/test/development/typescript-hmr/pages/type-error-recover.tsx similarity index 100% rename from test/integration/typescript-hmr/pages/type-error-recover.tsx rename to test/development/typescript-hmr/pages/type-error-recover.tsx diff --git a/test/development/typescript-hmr/typescript-hmr.test.ts b/test/development/typescript-hmr/typescript-hmr.test.ts new file mode 100644 index 000000000000..965bd45c393c --- /dev/null +++ b/test/development/typescript-hmr/typescript-hmr.test.ts @@ -0,0 +1,79 @@ +import { nextTestSetup } from 'e2e-utils' +import { getRedboxHeader, retry } from 'next-test-utils' + +describe('TypeScript HMR', () => { + const { next, isTurbopack } = nextTestSetup({ + files: __dirname, + }) + + describe('delete a page and add it back', () => { + it('should detect the changes to typescript pages and display it', async () => { + const browser = await next.browser('/hello') + await retry(async () => { + expect(await browser.elementByCss('body').text()).toMatch(/Hello World/) + }) + + const originalContent = await next.readFile('pages/hello.tsx') + const editedContent = originalContent.replace('Hello', 'COOL page') + + if (isTurbopack) { + await new Promise((resolve) => setTimeout(resolve, 500)) + } + + await next.patchFile('pages/hello.tsx', editedContent) + await retry(async () => { + expect(await browser.elementByCss('body').text()).toMatch(/COOL page/) + }) + + await next.patchFile('pages/hello.tsx', originalContent) + await retry(async () => { + expect(await browser.elementByCss('body').text()).toMatch(/Hello World/) + }) + }) + }) + + // old behavior: + it.skip('should recover from a type error', async () => { + const browser = await next.browser('/type-error-recover') + const originalContent = await next.readFile('pages/type-error-recover.tsx') + const errContent = originalContent.replace('() =>', '(): boolean =>') + try { + await next.patchFile('pages/type-error-recover.tsx', errContent) + await retry(async () => { + const header = await getRedboxHeader(browser) + expect(header).toMatch( + /Type 'Element' is not assignable to type 'boolean'/ + ) + }) + + await next.patchFile('pages/type-error-recover.tsx', originalContent) + await retry(async () => { + const html = await browser.eval('document.documentElement.innerHTML') + expect(html).not.toMatch(/iframe/) + }) + } finally { + await next.patchFile('pages/type-error-recover.tsx', originalContent) + } + }) + + it('should ignore type errors in development', async () => { + const browser = await next.browser('/type-error-recover') + const originalContent = await next.readFile('pages/type-error-recover.tsx') + const errContent = originalContent.replace( + '() => <p>Hello world</p>', + '(): boolean => <p>hello with error</p>' + ) + if (isTurbopack) { + await new Promise((resolve) => setTimeout(resolve, 500)) + } + try { + await next.patchFile('pages/type-error-recover.tsx', errContent) + await retry(async () => { + const text = await browser.eval('document.querySelector("p").innerText') + expect(text).toMatch(/hello with error/) + }) + } finally { + await next.patchFile('pages/type-error-recover.tsx', originalContent) + } + }) +}) diff --git a/test/e2e/404-page-app/404-page-app.test.ts b/test/e2e/404-page-app/404-page-app.test.ts new file mode 100644 index 000000000000..bb1cbd67deeb --- /dev/null +++ b/test/e2e/404-page-app/404-page-app.test.ts @@ -0,0 +1,45 @@ +import { nextTestSetup, isNextStart } from 'e2e-utils' + +describe('404 Page Support with _app', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + const gip404Err = + /`pages\/404` can not have getInitialProps\/getServerSideProps/ + + if (isNextStart) { + it('should build successfully', async () => { + expect(next.cliOutput).toContain('Compiled successfully') + expect(next.cliOutput).not.toMatch(gip404Err) + expect(next.cliOutput).not.toContain('Build error occurred') + }) + + it('should not output static 404 if _app has getInitialProps', async () => { + const browser = await next.browser('/404') + const isAutoExported = await browser.eval('__NEXT_DATA__.autoExport') + expect(isAutoExported).toBeFalsy() + }) + + it('specify to use the 404 page still in the routes-manifest', async () => { + const manifest = await next.readJSON('.next/routes-manifest.json') + expect(manifest.pages404).toBe(true) + }) + } + + it('should still use 404 page', async () => { + const $ = await next.render$('/abc') + expect($('#404-title').text()).toBe('Hi There') + const res = await next.fetch('/abc') + expect(res.status).toBe(404) + }) + + it('should not show pages/404 GIP error', async () => { + const res = await next.fetch('/abc') + expect(res.status).toBe(404) + const $ = await next.render$('/abc') + expect($('#404-title').text()).toBe('Hi There') + expect(next.cliOutput).not.toMatch(gip404Err) + expect(next.cliOutput).not.toContain('Build error occurred') + }) +}) diff --git a/test/integration/500-page/next.config.js b/test/e2e/404-page-app/next.config.js similarity index 100% rename from test/integration/500-page/next.config.js rename to test/e2e/404-page-app/next.config.js diff --git a/test/integration/404-page-app/pages/404.js b/test/e2e/404-page-app/pages/404.js similarity index 100% rename from test/integration/404-page-app/pages/404.js rename to test/e2e/404-page-app/pages/404.js diff --git a/test/integration/404-page-app/pages/_app.js b/test/e2e/404-page-app/pages/_app.js similarity index 100% rename from test/integration/404-page-app/pages/_app.js rename to test/e2e/404-page-app/pages/_app.js diff --git a/test/integration/404-page-app/pages/err.js b/test/e2e/404-page-app/pages/err.js similarity index 100% rename from test/integration/404-page-app/pages/err.js rename to test/e2e/404-page-app/pages/err.js diff --git a/test/integration/404-page-custom-error/pages/index.js b/test/e2e/404-page-app/pages/index.js similarity index 100% rename from test/integration/404-page-custom-error/pages/index.js rename to test/e2e/404-page-app/pages/index.js 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 new file mode 100644 index 000000000000..fc7f83bdd2a0 --- /dev/null +++ b/test/e2e/404-page-custom-error/404-page-custom-error.test.ts @@ -0,0 +1,48 @@ +/* eslint-disable jest/no-standalone-expect */ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' + +const shouldSkip = + (isNextStart && !!process.env.TURBOPACK_DEV) || + (isNextDev && !!process.env.TURBOPACK_BUILD) + +;(shouldSkip ? describe.skip : describe)( + 'Default 404 Page with custom _error', + () => { + const { next } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + }) + + it('should respond to 404 correctly', async () => { + const res = await next.fetch('/404') + expect(res.status).toBe(404) + expect(await res.text()).toContain('This page could not be found') + }) + + it('should render error correctly', async () => { + const text = await next.render('/err') + expect(text).toContain(isNextDev ? 'oops' : 'Internal Server Error') + }) + + it('should render index page normal', async () => { + const html = await next.render('/') + expect(html).toContain('hello from index') + }) + ;(isNextStart ? it : it.skip)( + 'should set pages404 in routes-manifest correctly', + async () => { + const data = JSON.parse( + await next.readFile('.next/routes-manifest.json') + ) + expect(data.pages404).toBe(true) + } + ) + ;(isNextStart ? it : it.skip)('should have output 404.html', async () => { + const pagesManifest = await next.readJSON( + '.next/server/pages-manifest.json' + ) + const page = pagesManifest['/404'] + expect(page.endsWith('.html')).toBe(true) + }) + } +) diff --git a/test/integration/404-page-custom-error/pages/_error.js b/test/e2e/404-page-custom-error/pages/_error.js similarity index 100% rename from test/integration/404-page-custom-error/pages/_error.js rename to test/e2e/404-page-custom-error/pages/_error.js diff --git a/test/integration/404-page-custom-error/pages/err.js b/test/e2e/404-page-custom-error/pages/err.js similarity index 100% rename from test/integration/404-page-custom-error/pages/err.js rename to test/e2e/404-page-custom-error/pages/err.js diff --git a/test/integration/404-page-ssg/pages/index.js b/test/e2e/404-page-custom-error/pages/index.js similarity index 100% rename from test/integration/404-page-ssg/pages/index.js rename to test/e2e/404-page-custom-error/pages/index.js diff --git a/test/e2e/404-page-ssg/404-page-ssg.test.ts b/test/e2e/404-page-ssg/404-page-ssg.test.ts new file mode 100644 index 000000000000..1096d0304fc8 --- /dev/null +++ b/test/e2e/404-page-ssg/404-page-ssg.test.ts @@ -0,0 +1,72 @@ +import { nextTestSetup, isNextStart } from 'e2e-utils' + +describe('404 Page Support SSG', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + disableAutoSkewProtection: true, + // Assertions don't apply to deploy mode (output differs vs. local Next.js server). + skipDeployment: true, + }) + if (skipped) return + + it('should respond to 404 correctly', async () => { + const res = await next.fetch('/404') + expect(res.status).toBe(404) + expect(await res.text()).toContain('custom 404 page') + }) + + it('should render error correctly', async () => { + const text = await next.render('/err') + if (isNextStart) { + expect(text).toContain('Internal Server Error') + } else { + expect(text).toContain('oops') + } + }) + + it('should not show an error in the logs for 404 SSG', async () => { + const gip404Err = + /`pages\/404` can not have getInitialProps\/getServerSideProps/ + await next.render('/non-existent') + expect(next.cliOutput).not.toMatch(gip404Err) + }) + + it('should render index page normal', async () => { + const html = await next.render('/') + expect(html).toContain('hello from index') + }) + + if (isNextStart) { + it('should not revalidate custom 404 page', async () => { + const res1 = await next.render('/non-existent') + const res2 = await next.render('/non-existent') + const res3 = await next.render('/non-existent') + const res4 = await next.render('/non-existent') + + expect(res1 === res2 && res2 === res3 && res3 === res4).toBe(true) + expect(res1).toContain('custom 404 page') + }) + + it('should set pages404 in routes-manifest correctly', async () => { + const data = await next.readJSON('.next/routes-manifest.json') + expect(data.pages404).toBe(true) + }) + + it('should have 404 page in prerender-manifest', async () => { + const data = await next.readJSON('.next/prerender-manifest.json') + expect(data.routes['/404']).toEqual({ + allowHeader: [ + 'host', + 'x-matched-path', + 'x-prerender-revalidate', + 'x-prerender-revalidate-if-generated', + 'x-next-revalidated-tags', + 'x-next-revalidate-tag-token', + ], + initialRevalidateSeconds: false, + srcRoute: null, + dataRoute: `/_next/data/${next.buildId}/404.json`, + }) + }) + } +}) diff --git a/test/integration/app-dynamic-error/next.config.js b/test/e2e/404-page-ssg/next.config.js similarity index 100% rename from test/integration/app-dynamic-error/next.config.js rename to test/e2e/404-page-ssg/next.config.js diff --git a/test/integration/404-page-ssg/pages/404.js b/test/e2e/404-page-ssg/pages/404.js similarity index 100% rename from test/integration/404-page-ssg/pages/404.js rename to test/e2e/404-page-ssg/pages/404.js diff --git a/test/integration/404-page-ssg/pages/_app.js b/test/e2e/404-page-ssg/pages/_app.js similarity index 100% rename from test/integration/404-page-ssg/pages/_app.js rename to test/e2e/404-page-ssg/pages/_app.js diff --git a/test/integration/404-page-ssg/pages/err.js b/test/e2e/404-page-ssg/pages/err.js similarity index 100% rename from test/integration/404-page-ssg/pages/err.js rename to test/e2e/404-page-ssg/pages/err.js diff --git a/test/integration/404-page/pages/index.js b/test/e2e/404-page-ssg/pages/index.js similarity index 100% rename from test/integration/404-page/pages/index.js rename to test/e2e/404-page-ssg/pages/index.js diff --git a/test/e2e/404-page/404-page.test.ts b/test/e2e/404-page/404-page.test.ts new file mode 100644 index 000000000000..90a3355d5bd8 --- /dev/null +++ b/test/e2e/404-page/404-page.test.ts @@ -0,0 +1,264 @@ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('404 Page Support', () => { + const { next } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + }) + + const gip404Err = + /`pages\/404` can not have getInitialProps\/getServerSideProps/ + + it('should use pages/404', async () => { + const html = await next.render('/abc') + expect(html).toContain('custom 404 page') + }) + + it('should set correct status code with pages/404', async () => { + const res = await next.fetch('/abc') + expect(res.status).toBe(404) + }) + + it('should use pages/404 for .d.ts file', async () => { + const html = await next.render('/invalidExtension') + expect(html).toContain('custom 404 page') + }) + + it('should not error when visited directly', async () => { + const res = await next.fetch('/404') + expect(res.status).toBe(404) + expect(await res.text()).toContain('custom 404 page') + }) + + it('should render _error for a 500 error still', async () => { + const html = await next.render('/err') + expect(html).not.toContain('custom 404 page') + expect(html).toContain(isNextDev ? 'oops' : 'Internal Server Error') + }) + + if (isNextStart) { + it('should output 404.html during build', async () => { + const manifest = await next.readJSON('.next/server/pages-manifest.json') + const page = manifest['/404'] + expect(page.endsWith('.html')).toBe(true) + }) + + it('should still output 404.js anyway', async () => { + expect(await next.hasFile('.next/server/pages/404.js')).toBe(true) + }) + + it('should add /404 to pages-manifest correctly', async () => { + const manifest = await next.readJSON('.next/server/pages-manifest.json') + expect('/404' in manifest).toBe(true) + }) + } + + if (isNextDev) { + it('falls back to _error correctly without pages/404', async () => { + const original404 = await next.readFile('pages/404.js') + try { + await next.deleteFile('pages/404.js') + await retry(async () => { + const res = await next.fetch('/abc') + expect(res.status).toBe(404) + expect(await res.text()).toContain('This page could not be found') + }) + } finally { + await next.patchFile('pages/404.js', original404) + } + }) + + it('shows error with getInitialProps in pages/404 dev', async () => { + const original404 = await next.readFile('pages/404.js') + try { + await next.patchFile( + 'pages/404.js', + ` + const page = () => 'custom 404 page' + page.getInitialProps = () => ({ a: 'b' }) + export default page + ` + ) + await next.render('/abc') + await retry(async () => { + expect(next.cliOutput).toMatch(gip404Err) + }) + } finally { + await next.patchFile('pages/404.js', original404) + } + }) + + it('does not show error with getStaticProps in pages/404 dev', async () => { + const original404 = await next.readFile('pages/404.js') + const getOutput = next.getCliOutputFromHere() + try { + await next.patchFile( + 'pages/404.js', + ` + const page = () => 'custom 404 page' + export const getStaticProps = () => ({ props: { a: 'b' } }) + export default page + ` + ) + await next.render('/abc') + await retry(async () => { + const html = await next.render('/abc') + expect(html).toContain('custom 404 page') + }) + expect(getOutput()).not.toMatch(gip404Err) + } finally { + await next.patchFile('pages/404.js', original404) + } + }) + + it('shows error with getServerSideProps in pages/404 dev', async () => { + const original404 = await next.readFile('pages/404.js') + try { + await next.patchFile( + 'pages/404.js', + ` + const page = () => 'custom 404 page' + export const getServerSideProps = () => ({ props: { a: 'b' } }) + export default page + ` + ) + await next.render('/abc') + await retry(async () => { + expect(next.cliOutput).toMatch(gip404Err) + }) + } finally { + await next.patchFile('pages/404.js', original404) + } + }) + } +}) +;(isNextStart ? describe : describe.skip)('404 Page build validation', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + if (skipped) return + + const gip404Err = + /`pages\/404` can not have getInitialProps\/getServerSideProps/ + const original404Content = `const page = () => 'custom 404 page' +export default page +` + + beforeEach(async () => { + // Restore original 404.js content before each test + await next.patchFile('pages/404.js', original404Content) + // Stop server if running + try { + await next.stop() + } catch (e) { + // Ignore if already stopped + } + }) + + it('shows error with getInitialProps in pages/404 build', async () => { + await next.patchFile( + 'pages/404.js', + ` + const page = () => 'custom 404 page' + page.getInitialProps = () => ({ a: 'b' }) + export default page + ` + ) + const { exitCode } = await next.build() + expect(exitCode).toBe(1) + expect(next.cliOutput).toMatch(gip404Err) + }) + + it('does not show error with getStaticProps in pages/404 build', async () => { + await next.patchFile( + 'pages/404.js', + ` + const page = () => 'custom 404 page' + export const getStaticProps = () => ({ props: { a: 'b' } }) + export default page + ` + ) + const { exitCode, cliOutput } = await next.build() + expect(exitCode).toBe(0) + expect(cliOutput).not.toMatch(gip404Err) + }) + + it('shows error with getServerSideProps in pages/404 build', async () => { + await next.patchFile( + 'pages/404.js', + ` + const page = () => 'custom 404 page' + export const getServerSideProps = () => ({ props: { a: 'b' } }) + export default page + ` + ) + const { exitCode } = await next.build() + expect(exitCode).toBe(1) + expect(next.cliOutput).toMatch(gip404Err) + }) + + it('should not cache for custom 404 page with gssp and revalidate disabled', async () => { + await next.patchFile( + 'pages/404.js', + ` + const page = () => 'custom 404 page' + export async function getStaticProps() { return { props: {} } } + export default page + ` + ) + const { exitCode } = await next.build() + expect(exitCode).toBe(0) + await next.start() + + const res404 = await next.fetch('/404') + const resNext = await next.fetch('/_next/abc') + + expect(res404.headers.get('Cache-Control')).toBe( + 'private, no-cache, no-store, max-age=0, must-revalidate' + ) + expect(resNext.headers.get('Cache-Control')).toBe( + 'private, no-cache, no-store, max-age=0, must-revalidate' + ) + }) + + it('should not cache for custom 404 page with gssp and revalidate enabled', async () => { + await next.patchFile( + 'pages/404.js', + ` + const page = () => 'custom 404 page' + export async function getStaticProps() { return { props: {}, revalidate: 1 } } + export default page + ` + ) + const { exitCode } = await next.build() + expect(exitCode).toBe(0) + await next.start() + + const res404 = await next.fetch('/404') + const resNext = await next.fetch('/_next/abc') + + expect(res404.headers.get('Cache-Control')).toBe( + 'private, no-cache, no-store, max-age=0, must-revalidate' + ) + expect(resNext.headers.get('Cache-Control')).toBe( + 'private, no-cache, no-store, max-age=0, must-revalidate' + ) + }) + + it('should not cache for custom 404 page without gssp', async () => { + const { exitCode } = await next.build() + expect(exitCode).toBe(0) + await next.start() + + const res404 = await next.fetch('/404') + const resNext = await next.fetch('/_next/abc') + + expect(res404.headers.get('Cache-Control')).toBe(null) + expect(resNext.headers.get('Cache-Control')).toBe( + 'private, no-cache, no-store, max-age=0, must-revalidate' + ) + }) +}) diff --git a/test/integration/css-fixtures/dev-module/next.config.js b/test/e2e/404-page/next.config.js similarity index 100% rename from test/integration/css-fixtures/dev-module/next.config.js rename to test/e2e/404-page/next.config.js diff --git a/test/integration/404-page/pages/404.js b/test/e2e/404-page/pages/404.js similarity index 100% rename from test/integration/404-page/pages/404.js rename to test/e2e/404-page/pages/404.js diff --git a/test/integration/404-page/pages/err.js b/test/e2e/404-page/pages/err.js similarity index 100% rename from test/integration/404-page/pages/err.js rename to test/e2e/404-page/pages/err.js diff --git a/test/integration/500-page/pages/index.js b/test/e2e/404-page/pages/index.js similarity index 100% rename from test/integration/500-page/pages/index.js rename to test/e2e/404-page/pages/index.js diff --git a/test/integration/404-page/pages/invalidExtension.d.ts b/test/e2e/404-page/pages/invalidExtension.d.ts similarity index 100% rename from test/integration/404-page/pages/invalidExtension.d.ts rename to test/e2e/404-page/pages/invalidExtension.d.ts diff --git a/test/e2e/500-page/500-page-build.test.ts b/test/e2e/500-page/500-page-build.test.ts new file mode 100644 index 000000000000..95c2a92d0d01 --- /dev/null +++ b/test/e2e/500-page/500-page-build.test.ts @@ -0,0 +1,333 @@ +import { isNextDev, nextTestSetup } from 'e2e-utils' + +// This test exercises `next build` outputs and `next start` behaviour, so it +// is meaningless in dev mode where the dev server bypasses production build +// artifacts (e.g. statically prerendered 500.html from getStaticProps). +;(isNextDev ? describe.skip : describe)('500 Page build validation', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + if (skipped) return + + const gip500Err = + /`pages\/500` can not have getInitialProps\/getServerSideProps/ + + beforeEach(async () => { + // Reset to original 500.js before each test + await next.patchFile( + 'pages/500.js', + `const page = () => { + console.log('rendered 500') + return 'custom 500 page' +} +export default page +` + ) + }) + + it('shows error with getInitialProps in pages/500 build', async () => { + await next.patchFile( + 'pages/500.js', + ` + const page = () => 'custom 500 page' + page.getInitialProps = () => ({ a: 'b' }) + export default page + ` + ) + const { exitCode } = await next.build() + expect(exitCode).toBe(1) + expect(next.cliOutput).toMatch(gip500Err) + }) + + it('does not show error with getStaticProps in pages/500 build', async () => { + await next.patchFile( + 'pages/500.js', + ` + const page = () => 'custom 500 page' + export const getStaticProps = () => ({ props: { a: 'b' } }) + export default page + ` + ) + const { exitCode, cliOutput } = await next.build() + expect(exitCode).toBe(0) + expect(cliOutput).not.toMatch(gip500Err) + }) + + it('shows error with getServerSideProps in pages/500 build', async () => { + await next.patchFile( + 'pages/500.js', + ` + const page = () => 'custom 500 page' + export const getServerSideProps = () => ({ props: { a: 'b' } }) + export default page + ` + ) + const { exitCode } = await next.build() + expect(exitCode).toBe(1) + expect(next.cliOutput).toMatch(gip500Err) + }) + + it('should have correct cache control for 500 page with getStaticProps', async () => { + await next.patchFile( + 'pages/500.js', + ` + export default function Page() { + return <p>custom 500</p> + } + export function getStaticProps() { + return { props: { now: Date.now() } } + } + ` + ) + const { exitCode } = await next.build() + expect(exitCode).toBe(0) + await next.start({ skipBuild: true }) + + try { + const res = await next.fetch('/err') + expect(res.status).toBe(500) + expect(res.headers.get('cache-control')).toBe( + 'private, no-cache, no-store, max-age=0, must-revalidate' + ) + } finally { + await next.stop() + } + }) + + it('does not build 500 statically with getInitialProps in _app', async () => { + await next.patchFile( + 'pages/_app.js', + ` + import App from 'next/app' + const page = ({ Component, pageProps }) => <Component {...pageProps} /> + page.getInitialProps = (ctx) => App.getInitialProps(ctx) + export default page + ` + ) + const { exitCode, cliOutput } = await next.build() + expect(exitCode).toBe(0) + expect(cliOutput).not.toMatch(gip500Err) + expect(cliOutput).not.toContain('rendered 500') + expect(await next.hasFile('.next/server/pages/500.html')).toBe(false) + + await next.start({ skipBuild: true }) + try { + const res = await next.fetch('/err') + expect(res.status).toBe(500) + // Verify the page was rendered at runtime by checking response + expect(await res.text()).toContain('custom 500 page') + } finally { + await next.stop() + } + + await next.deleteFile('pages/_app.js') + }) + + it('does build 500 statically with getInitialProps in _app and getStaticProps in pages/500', async () => { + await next.patchFile( + 'pages/_app.js', + ` + import App from 'next/app' + const page = ({ Component, pageProps }) => <Component {...pageProps} /> + page.getInitialProps = (ctx) => App.getInitialProps(ctx) + export default page + ` + ) + await next.patchFile( + 'pages/500.js', + ` + const page = () => { + console.log('rendered 500') + return 'custom 500 page' + } + export default page + export const getStaticProps = () => { + return { props: {} } + } + ` + ) + const { exitCode, cliOutput } = await next.build() + expect(exitCode).toBe(0) + expect(cliOutput).not.toMatch(gip500Err) + expect(cliOutput).toContain('rendered 500') + expect(await next.hasFile('.next/server/pages/500.html')).toBe(true) + + const outputBeforeStart = next.cliOutput.length + await next.start({ skipBuild: true }) + try { + await next.render('/err') + expect(next.cliOutput.substring(outputBeforeStart)).not.toContain( + 'rendered 500' + ) + } finally { + await next.stop() + } + + await next.deleteFile('pages/_app.js') + }) + + it('builds 500 statically by default with no pages/500', async () => { + await next.deleteFile('pages/500.js') + const { exitCode } = await next.build() + expect(exitCode).toBe(0) + expect(next.cliOutput).not.toMatch(gip500Err) + expect(await next.hasFile('.next/server/pages/500.html')).toBe(true) + + await next.start({ skipBuild: true }) + try { + const browser = await next.browser('/err?hello=world') + const initialTitle = await browser.eval('document.title') + const currentTitle = await browser.eval('document.title') + expect(initialTitle).toBe(currentTitle) + expect(initialTitle).toBe('500: Internal Server Error') + } finally { + await next.stop() + } + }) + + it('builds 500 statically by default with no pages/500 and custom _error without getInitialProps', async () => { + await next.deleteFile('pages/500.js') + await next.patchFile( + 'pages/_error.js', + ` + function Error({ statusCode }) { + return <p>Error status: {statusCode}</p> + } + export default Error + ` + ) + const { exitCode } = await next.build() + expect(exitCode).toBe(0) + expect(next.cliOutput).not.toMatch(gip500Err) + expect(await next.hasFile('.next/server/pages/500.html')).toBe(true) + await next.deleteFile('pages/_error.js') + }) + + it('does not build 500 statically with no pages/500 and custom getInitialProps in _error', async () => { + await next.deleteFile('pages/500.js') + await next.patchFile( + 'pages/_error.js', + ` + function Error({ statusCode }) { + return <p>Error status: {statusCode}</p> + } + Error.getInitialProps = ({ req, res, err }) => { + console.error('called _error.getInitialProps') + if (req.url === '/500') { + throw new Error('should not export /500') + } + return { + statusCode: res && res.statusCode ? res.statusCode : err ? err.statusCode : 404 + } + } + export default Error + ` + ) + const { exitCode } = await next.build() + expect(exitCode).toBe(0) + expect(next.cliOutput).not.toMatch(gip500Err) + expect(await next.hasFile('.next/server/pages/500.html')).toBe(false) + + await next.start({ skipBuild: true }) + try { + const res = await next.fetch('/err') + expect(res.status).toBe(500) + // Verify _error.getInitialProps is called at runtime by checking response content + const text = await res.text() + expect(text).toContain('Error status:') + expect(text).toContain('500') + } finally { + await next.stop() + } + + await next.deleteFile('pages/_error.js') + }) + + it('does not build 500 statically with no pages/500 and custom getInitialProps in _error and _app', async () => { + await next.deleteFile('pages/500.js') + await next.patchFile( + 'pages/_error.js', + ` + function Error({ statusCode }) { + return <p>Error status: {statusCode}</p> + } + Error.getInitialProps = ({ req, res, err }) => { + console.error('called _error.getInitialProps') + if (req.url === '/500') { + throw new Error('should not export /500') + } + return { + statusCode: res && res.statusCode ? res.statusCode : err ? err.statusCode : 404 + } + } + export default Error + ` + ) + await next.patchFile( + 'pages/_app.js', + ` + function App({ pageProps, Component }) { + return <Component {...pageProps} /> + } + App.getInitialProps = async ({ Component, ctx }) => { + let pageProps = {} + if (Component.getInitialProps) { + pageProps = await Component.getInitialProps(ctx) + } + return { pageProps } + } + export default App + ` + ) + const { exitCode } = await next.build() + expect(exitCode).toBe(0) + expect(next.cliOutput).not.toMatch(gip500Err) + expect(await next.hasFile('.next/server/pages/500.html')).toBe(false) + await next.deleteFile('pages/_error.js') + await next.deleteFile('pages/_app.js') + }) + + it('does not build 500 statically with no pages/500 and getServerSideProps in _error', async () => { + await next.deleteFile('pages/500.js') + await next.patchFile( + 'pages/_error.js', + ` + function Error({ statusCode }) { + return <p>Error status: {statusCode}</p> + } + export const getServerSideProps = ({ req, res, err }) => { + console.error('called _error getServerSideProps') + if (req.url === '/500') { + throw new Error('should not export /500') + } + return { + props: { + statusCode: res && res.statusCode ? res.statusCode : err ? err.statusCode : 404 + } + } + } + export default Error + ` + ) + const { exitCode } = await next.build() + expect(exitCode).toBe(0) + expect(next.cliOutput).not.toMatch(gip500Err) + expect(await next.hasFile('.next/server/pages/500.html')).toBe(false) + + await next.start({ skipBuild: true }) + try { + const res = await next.fetch('/err') + expect(res.status).toBe(500) + // Verify _error getServerSideProps is called at runtime by checking response content + const text = await res.text() + expect(text).toContain('Error status:') + expect(text).toContain('500') + } finally { + await next.stop() + } + + await next.deleteFile('pages/_error.js') + }) +}) diff --git a/test/e2e/500-page/500-page.test.ts b/test/e2e/500-page/500-page.test.ts new file mode 100644 index 000000000000..92e00dcce530 --- /dev/null +++ b/test/e2e/500-page/500-page.test.ts @@ -0,0 +1,108 @@ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('500 Page Support', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + }) + if (skipped) return + + it('should use pages/500', async () => { + const html = await next.render('/500') + expect(html).toContain('custom 500 page') + }) + + it('should set correct status code with pages/500', async () => { + const res = await next.fetch('/500') + expect(res.status).toBe(500) + }) + + it('should not error when visited directly', async () => { + const res = await next.fetch('/500') + expect(res.status).toBe(500) + expect(await res.text()).toContain('custom 500 page') + }) + + if (isNextStart) { + it('should output 500.html during build', async () => { + const manifest = await next.readJSON('.next/server/pages-manifest.json') + const page = manifest['/500'] + expect(page.endsWith('.html')).toBe(true) + }) + + it('should add /500 to pages-manifest correctly', async () => { + const manifest = await next.readJSON('.next/server/pages-manifest.json') + expect('/500' in manifest).toBe(true) + }) + } + + if (isNextDev) { + it('shows error with getInitialProps in pages/500 dev', async () => { + const original500 = await next.readFile('pages/500.js') + try { + await next.patchFile( + 'pages/500.js', + ` + const page = () => 'custom 500 page' + page.getInitialProps = () => ({ a: 'b' }) + export default page + ` + ) + await next.render('/500') + await retry(async () => { + expect(next.cliOutput).toMatch( + /`pages\/500` can not have getInitialProps\/getServerSideProps/ + ) + }) + } finally { + await next.patchFile('pages/500.js', original500) + } + }) + + it('does not show error with getStaticProps in pages/500 dev', async () => { + const original500 = await next.readFile('pages/500.js') + const outputBefore = next.cliOutput.length + try { + await next.patchFile( + 'pages/500.js', + ` + const page = () => 'custom 500 page' + export const getStaticProps = () => ({ props: { a: 'b' } }) + export default page + ` + ) + await next.render('/abc') + await retry(async () => { + expect(next.cliOutput.slice(outputBefore)).not.toMatch( + /`pages\/500` can not have getInitialProps\/getServerSideProps/ + ) + }) + } finally { + await next.patchFile('pages/500.js', original500) + } + }) + + it('shows error with getServerSideProps in pages/500 dev', async () => { + const original500 = await next.readFile('pages/500.js') + try { + await next.patchFile( + 'pages/500.js', + ` + const page = () => 'custom 500 page' + export const getServerSideProps = () => ({ props: { a: 'b' } }) + export default page + ` + ) + await next.render('/500') + await retry(async () => { + expect(next.cliOutput).toMatch( + /`pages\/500` can not have getInitialProps\/getServerSideProps/ + ) + }) + } finally { + await next.patchFile('pages/500.js', original500) + } + }) + } +}) diff --git a/test/integration/dynamic-optional-routing-root-fallback/next.config.js b/test/e2e/500-page/next.config.js similarity index 100% rename from test/integration/dynamic-optional-routing-root-fallback/next.config.js rename to test/e2e/500-page/next.config.js diff --git a/test/integration/500-page/pages/500.js b/test/e2e/500-page/pages/500.js similarity index 100% rename from test/integration/500-page/pages/500.js rename to test/e2e/500-page/pages/500.js diff --git a/test/integration/500-page/pages/err.js b/test/e2e/500-page/pages/err.js similarity index 100% rename from test/integration/500-page/pages/err.js rename to test/e2e/500-page/pages/err.js diff --git a/test/integration/port-env-var/pages/index.js b/test/e2e/500-page/pages/index.js similarity index 100% rename from test/integration/port-env-var/pages/index.js rename to test/e2e/500-page/pages/index.js diff --git a/test/e2e/api-body-parser/api-body-parser.test.ts b/test/e2e/api-body-parser/api-body-parser.test.ts new file mode 100644 index 000000000000..b989a591e5d8 --- /dev/null +++ b/test/e2e/api-body-parser/api-body-parser.test.ts @@ -0,0 +1,62 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('API body parser', () => { + describe('without custom server', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + // Uses a custom HTTP/proxy server in front of Next.js; not applicable in deploy mode. + skipDeployment: true, + }) + if (skipped) return + + it('should parse JSON body', async () => { + const res = await next.fetch('/api', { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify([{ title: 'Nextjs' }]), + }) + const data = await res.json() + expect(data).toEqual([{ title: 'Nextjs' }]) + }) + }) + + describe('with custom server (pre-parsed body)', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + startCommand: 'node server.js', + serverReadyPattern: /- Local:/, + env: { CUSTOM_SERVER: 'true' }, + dependencies: { + express: '4', + }, + // Uses a custom HTTP/proxy server in front of Next.js; not applicable in deploy mode. + skipDeployment: true, + }) + if (skipped) return + + it('should not throw if request body is already parsed in custom middleware', async () => { + const res = await next.fetch('/api', { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify([{ title: 'Nextjs' }]), + }) + const data = await res.json() + expect(data).toEqual([{ title: 'Nextjs' }]) + }) + + it("should not throw if request's content-type is invalid", async () => { + const res = await next.fetch('/api', { + method: 'POST', + headers: { + 'Content-Type': 'application/json;', + }, + body: JSON.stringify([{ title: 'Nextjs' }]), + }) + expect(res.status).toBe(200) + }) + }) +}) diff --git a/test/integration/api-body-parser/pages/api/index.js b/test/e2e/api-body-parser/pages/api/index.js similarity index 100% rename from test/integration/api-body-parser/pages/api/index.js rename to test/e2e/api-body-parser/pages/api/index.js diff --git a/test/integration/api-body-parser/server.js b/test/e2e/api-body-parser/server.js similarity index 61% rename from test/integration/api-body-parser/server.js rename to test/e2e/api-body-parser/server.js index f37ee6845f7d..2dfe274ac09f 100644 --- a/test/integration/api-body-parser/server.js +++ b/test/e2e/api-body-parser/server.js @@ -3,7 +3,7 @@ const express = require('express') const dev = process.env.NODE_ENV !== 'production' const dir = __dirname -const port = process.env.PORT || 3000 +const port = parseInt(process.env.PORT || '3000', 10) const app = next({ dev, dir }) const handleNextRequests = app.getRequestHandler() @@ -18,11 +18,14 @@ app.prepare().then(() => { handleNextRequests(req, res) }) - server.listen(port, (err) => { + const httpServer = server.listen(port, (err) => { if (err) { throw err } - console.log(`> Ready on http://localhost:${port}`) + const address = httpServer.address() + const actualPort = + typeof address === 'object' && address ? address.port : port + console.log(`- Local: http://localhost:${actualPort}`) }) }) diff --git a/test/e2e/api-catch-all/api-catch-all.test.ts b/test/e2e/api-catch-all/api-catch-all.test.ts new file mode 100644 index 000000000000..cc4575cd2457 --- /dev/null +++ b/test/e2e/api-catch-all/api-catch-all.test.ts @@ -0,0 +1,45 @@ +import { nextTestSetup, isNextStart } from 'e2e-utils' +;(process.env.TURBOPACK_DEV && isNextStart ? describe.skip : describe)( + 'API routes', + () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + // Assertions don't apply to deploy mode (output differs vs. local Next.js server). + skipDeployment: true, + }) + if (skipped) return + + it('should return data when catch-all', async () => { + const data = await next + .fetch('/api/users/1', {}) + .then((res) => res.ok && res.json()) + + expect(data).toEqual({ slug: ['1'] }) + }) + + it('should return redirect when catch-all with index and trailing slash', async () => { + const res = await next.fetch('/api/users/', { + redirect: 'manual', + }) + expect(res.status).toBe(308) + const text = await res.text() + expect(text).toEqual('/api/users') + }) + + it('should return data when catch-all with index and trailing slash', async () => { + const data = await next + .fetch('/api/users/', {}) + .then((res) => res.ok && res.json()) + + expect(data).toEqual({}) + }) + + it('should return data when catch-all with index and no trailing slash', async () => { + const data = await next + .fetch('/api/users', {}) + .then((res) => res.ok && res.json()) + + expect(data).toEqual({}) + }) + } +) diff --git a/test/integration/api-catch-all/pages/api/users/[...slug].js b/test/e2e/api-catch-all/pages/api/users/[...slug].js similarity index 100% rename from test/integration/api-catch-all/pages/api/users/[...slug].js rename to test/e2e/api-catch-all/pages/api/users/[...slug].js diff --git a/test/integration/api-catch-all/pages/api/users/index.js b/test/e2e/api-catch-all/pages/api/users/index.js similarity index 100% rename from test/integration/api-catch-all/pages/api/users/index.js rename to test/e2e/api-catch-all/pages/api/users/index.js diff --git a/test/integration/api-support/test/index.test.ts b/test/e2e/api-support/api-support.test.ts similarity index 52% rename from test/integration/api-support/test/index.test.ts rename to test/e2e/api-support/api-support.test.ts index 0ee059bf0dca..c2ef079e9124 100644 --- a/test/integration/api-support/test/index.test.ts +++ b/test/e2e/api-support/api-support.test.ts @@ -1,123 +1,93 @@ -/* eslint-env jest */ - -import fs from 'fs-extra' -import { join } from 'path' import AbortController from 'abort-controller' import { - killApp, - findPort, - launchApp, - fetchViaHTTP, - File, - renderViaHTTP, - nextBuild, - nextStart, getPageFileFromBuildManifest, getPageFileFromPagesManifest, - check, + retry, } from 'next-test-utils' -import json from '../big.json' - -const appDir = join(__dirname, '../') -const originalIsNextDev = global.isNextDev -let appPort -let stderr -let mode -let app +import { nextTestSetup, isNextDev } from 'e2e-utils' +import json from './big.json' -function runTests(dev = false) { - beforeAll(() => { - // isNextDev is used for getDistDir, where it is used for reading the build manifest files. - global.isNextDev = dev - }) - afterAll(() => { - global.isNextDev = originalIsNextDev - }) +describe('API routes', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + dependencies: { + 'http-proxy': 'latest', + cors: 'latest', + 'node-fetch': '2.6.7', + }, + skipDeployment: true, + disableAutoSkewProtection: true, + }) + if (skipped) return it('should not strip .json from API route', async () => { - const res = await fetchViaHTTP(appPort, '/api/hello.json') + const res = await next.fetch('/api/hello.json') expect(res.status).toBe(200) expect(await res.json()).toEqual({ post: 'hello.json' }) }) it('should handle proxying to self correctly', async () => { - const res1 = await fetchViaHTTP(appPort, '/api/proxy-self') + const res1 = await next.fetch('/api/proxy-self') expect(res1.status).toBe(200) expect(await res1.text()).toContain('User') - const buildId = dev + const buildId = isNextDev ? 'development' - : await fs.readFile(join(appDir, '.next', 'BUILD_ID'), 'utf8') + : await next.readFile('.next/BUILD_ID') - const res2 = await fetchViaHTTP( - appPort, - `/api/proxy-self?buildId=${buildId}` - ) + const res2 = await next.fetch(`/api/proxy-self?buildId=${buildId}`) expect(res2.status).toBe(200) expect(await res2.text()).toContain('__SSG_MANIFEST') }) it('should respond from /api/auth/[...nextauth] correctly', async () => { - const res = await fetchViaHTTP(appPort, '/api/auth/signin', undefined, { - redirect: 'manual', - }) + const res = await next.fetch('/api/auth/signin', { redirect: 'manual' }) expect(res.status).toBe(200) expect(await res.json()).toEqual({ from: 'auth' }) }) it('should handle 204 status correctly', async () => { - const res = await fetchViaHTTP(appPort, '/api/status-204', undefined, { - redirect: 'manual', - }) + const res = await next.fetch('/api/status-204', { redirect: 'manual' }) expect(res.status).toBe(204) expect(res.headers.get('content-type')).toBe(null) expect(res.headers.get('content-length')).toBe(null) expect(res.headers.get('transfer-encoding')).toBe(null) - const stderrIdx = stderr.length - const res2 = await fetchViaHTTP( - appPort, - '/api/status-204', - { invalid: '1' }, - { - redirect: 'manual', - } - ) + const cliOutputBefore = next.cliOutput.length + const res2 = await next.fetch('/api/status-204?invalid=1', { + redirect: 'manual', + }) expect(res2.status).toBe(204) expect(res2.headers.get('content-type')).toBe(null) expect(res2.headers.get('content-length')).toBe(null) expect(res2.headers.get('transfer-encoding')).toBe(null) - if (dev) { - await check( - () => stderr.slice(stderrIdx), - /A body was attempted to be set with a 204 statusCode/ - ) + if (isNextDev) { + await retry(() => { + expect(next.cliOutput.slice(cliOutputBefore)).toContain( + 'A body was attempted to be set with a 204 statusCode' + ) + }) } }) it('should render page', async () => { - const html = await renderViaHTTP(appPort, '/') + const html = await next.render('/') expect(html).toMatch(/API - support/) }) it('should return 404 for undefined path', async () => { - const { status } = await fetchViaHTTP( - appPort, - '/api/not/unexisting/page/really', - null, - {} - ) + const { status } = await next.fetch('/api/not/unexisting/page/really') expect(status).toEqual(404) }) it('should not conflict with /api routes', async () => { - const res = await fetchViaHTTP(appPort, '/api-conflict') + const res = await next.fetch('/api-conflict') expect(res.status).not.toEqual(404) }) it('should set cors headers when adding cors middleware', async () => { - const res = await fetchViaHTTP(appPort, '/api/cors', null, { + const res = await next.fetch('/api/cors', { method: 'OPTIONS', headers: { origin: 'example.com', @@ -131,26 +101,24 @@ function runTests(dev = false) { }) it('should work with index api', async () => { - const text = await fetchViaHTTP(appPort, '/api', null, {}).then( - (res) => res.ok && res.text() - ) + const text = await next.fetch('/api').then((res) => res.ok && res.text()) expect(text).toEqual('Index should work') }) it('should return custom error', async () => { - const data = await fetchViaHTTP(appPort, '/api/error', null, {}) - const json = await data.json() + const data = await next.fetch('/api/error') + const body = await data.json() expect(data.status).toEqual(500) - expect(json).toEqual({ error: 'Server error!' }) + expect(body).toEqual({ error: 'Server error!' }) }) it('should throw Internal Server Error', async () => { - const res = await fetchViaHTTP(appPort, '/api/user-error', null, {}) + const res = await next.fetch('/api/user-error') const text = await res.text() expect(res.status).toBe(500) - if (dev) { + if (isNextDev) { expect(text).toContain('User error') } else { expect(text).toBe('Internal Server Error') @@ -158,11 +126,11 @@ function runTests(dev = false) { }) it('should throw Internal Server Error (async)', async () => { - const res = await fetchViaHTTP(appPort, '/api/user-error-async', null, {}) + const res = await next.fetch('/api/user-error-async') const text = await res.text() expect(res.status).toBe(500) - if (dev) { + if (isNextDev) { expect(text).toContain('User error') } else { expect(text).toBe('Internal Server Error') @@ -170,30 +138,34 @@ function runTests(dev = false) { }) it('should parse JSON body', async () => { - const data = await fetchViaHTTP(appPort, '/api/parse', null, { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=utf-8', - }, - body: JSON.stringify([{ title: 'Nextjs' }]), - }).then((res) => res.ok && res.json()) + const data = await next + .fetch('/api/parse', { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify([{ title: 'Nextjs' }]), + }) + .then((res) => res.ok && res.json()) expect(data).toEqual([{ title: 'Nextjs' }]) }) it('should special-case empty JSON body', async () => { - const data = await fetchViaHTTP(appPort, '/api/parse', null, { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=utf-8', - }, - }).then((res) => res.ok && res.json()) + const data = await next + .fetch('/api/parse', { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }) + .then((res) => res.ok && res.json()) expect(data).toEqual({}) }) it('should support boolean for JSON in api page', async () => { - const res = await fetchViaHTTP(appPort, '/api/bool', null, {}) + const res = await next.fetch('/api/bool') const body = res.ok ? await res.json() : null expect(res.status).toBe(200) expect(res.headers.get('content-type')).toBe( @@ -203,19 +175,19 @@ function runTests(dev = false) { }) it('should support undefined response body', async () => { - const res = await fetchViaHTTP(appPort, '/api/json-undefined', null, {}) + const res = await next.fetch('/api/json-undefined') const body = res.ok ? await res.text() : null expect(body).toBe('') }) it('should support string in JSON response body', async () => { - const res = await fetchViaHTTP(appPort, '/api/json-string', null, {}) + const res = await next.fetch('/api/json-string') const body = res.ok ? await res.text() : null expect(body).toBe('"Hello world!"') }) it('should support null in JSON response body', async () => { - const res = await fetchViaHTTP(appPort, '/api/json-null') + const res = await next.fetch('/api/json-null') const body = res.ok ? await res.json() : 'Not null' expect(res.status).toBe(200) expect(res.headers.get('content-type')).toBe( @@ -225,7 +197,7 @@ function runTests(dev = false) { }) it('should return error with invalid JSON', async () => { - const data = await fetchViaHTTP(appPort, '/api/parse', null, { + const data = await next.fetch('/api/parse', { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8', @@ -242,7 +214,7 @@ function runTests(dev = false) { let error try { - res = await fetchViaHTTP(appPort, '/api/parse', null, { + res = await next.fetch('/api/parse', { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8', @@ -254,11 +226,6 @@ function runTests(dev = false) { } if (error) { - // This is a temporary workaround for testing since node doesn't handle - // closed connections when POSTing data to an endpoint correctly - // https://github.com/nodejs/node/issues/12339 - // TODO: investigate re-enabling this after above issue has been - // addressed in node or `node-fetch` expect(error.code).toBe('EPIPE') } else { expect(res.status).toEqual(413) @@ -267,7 +234,7 @@ function runTests(dev = false) { }) it('should parse bigger body then 1mb', async () => { - const data = await fetchViaHTTP(appPort, '/api/big-parse', null, { + const data = await next.fetch('/api/big-parse', { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8', @@ -279,10 +246,10 @@ function runTests(dev = false) { }) it('should support etag spec', async () => { - const response = await fetchViaHTTP(appPort, '/api/blog') + const response = await next.fetch('/api/blog') const etag = response.headers.get('etag') - const unmodifiedResponse = await fetchViaHTTP(appPort, '/api/blog', null, { + const unmodifiedResponse = await next.fetch('/api/blog', { headers: { 'If-None-Match': etag }, }) @@ -301,13 +268,15 @@ function runTests(dev = false) { }) .join('&') - const data = await fetchViaHTTP(appPort, '/api/parse', null, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-Form-urlencoded', - }, - body: formBody, - }).then((res) => res.ok && res.json()) + const data = await next + .fetch('/api/parse', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-Form-urlencoded', + }, + body: formBody, + }) + .then((res) => res.ok && res.json()) expect(data).toEqual({ title: 'Nextjs', @@ -316,53 +285,55 @@ function runTests(dev = false) { }) it('should parse body in handler', async () => { - const data = await fetchViaHTTP(appPort, '/api/no-parsing', null, { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=utf-8', - }, - body: JSON.stringify([{ title: 'Nextjs' }]), - }).then((res) => res.ok && res.json()) + const data = await next + .fetch('/api/no-parsing', { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify([{ title: 'Nextjs' }]), + }) + .then((res) => res.ok && res.json()) expect(data).toEqual([{ title: 'Nextjs' }]) }) it('should parse body with config', async () => { - const data = await fetchViaHTTP(appPort, '/api/parsing', null, { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=utf-8', - }, - body: JSON.stringify([{ title: 'Nextjs' }]), - }).then((res) => res.ok && res.json()) + const data = await next + .fetch('/api/parsing', { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify([{ title: 'Nextjs' }]), + }) + .then((res) => res.ok && res.json()) expect(data).toEqual({ message: 'Parsed body' }) }) it('should show friendly error for invalid redirect', async () => { - await fetchViaHTTP(appPort, '/api/redirect-error', null, {}) + await next.fetch('/api/redirect-error') - await check(() => { - expect(stderr).toContain( + await retry(() => { + expect(next.cliOutput).toContain( `Invalid redirect arguments. Please use a single argument URL, e.g. res.redirect('/destination') or use a status code and URL, e.g. res.redirect(307, '/destination').` ) - return 'yes' - }, 'yes') + }) }) it('should show friendly error in case of passing null as first argument redirect', async () => { - await fetchViaHTTP(appPort, '/api/redirect-null', null, {}) + await next.fetch('/api/redirect-null') - check(() => { - expect(stderr).toContain( + await retry(() => { + expect(next.cliOutput).toContain( `Invalid redirect arguments. Please use a single argument URL, e.g. res.redirect('/destination') or use a status code and URL, e.g. res.redirect(307, '/destination').` ) - return 'yes' - }, 'yes') + }) }) it('should redirect with status code 307', async () => { - const res = await fetchViaHTTP(appPort, '/api/redirect-307', null, { + const res = await next.fetch('/api/redirect-307', { redirect: 'manual', }) @@ -372,14 +343,14 @@ function runTests(dev = false) { }) it('should redirect to login', async () => { - const res = await fetchViaHTTP(appPort, '/api/redirect-307', null, {}) + const res = await next.fetch('/api/redirect-307') expect(res.redirected).toBe(true) expect(res.url).toContain('/login') }) it('should redirect with status code 301', async () => { - const res = await fetchViaHTTP(appPort, '/api/redirect-301', null, { + const res = await next.fetch('/api/redirect-301', { redirect: 'manual', }) @@ -389,40 +360,39 @@ function runTests(dev = false) { }) it('should return empty query object', async () => { - const data = await fetchViaHTTP(appPort, '/api/query', null, {}).then( - (res) => res.ok && res.json() - ) + const data = await next + .fetch('/api/query') + .then((res) => res.ok && res.json()) expect(data).toEqual({}) }) it('should parse query correctly', async () => { - const data = await fetchViaHTTP( - appPort, - '/api/query?a=1&b=2&a=3', - null, - {} - ).then((res) => res.ok && res.json()) + const data = await next + .fetch('/api/query?a=1&b=2&a=3') + .then((res) => res.ok && res.json()) expect(data).toEqual({ a: ['1', '3'], b: '2' }) }) it('should return empty cookies object', async () => { - const data = await fetchViaHTTP(appPort, '/api/cookies', null, {}).then( - (res) => res.ok && res.json() - ) + const data = await next + .fetch('/api/cookies') + .then((res) => res.ok && res.json()) expect(data).toEqual({}) }) it('should return cookies object', async () => { - const data = await fetchViaHTTP(appPort, '/api/cookies', null, { - headers: { - Cookie: 'nextjs=cool;', - }, - }).then((res) => res.ok && res.json()) + const data = await next + .fetch('/api/cookies', { + headers: { + Cookie: 'nextjs=cool;', + }, + }) + .then((res) => res.ok && res.json()) expect(data).toEqual({ nextjs: 'cool' }) }) it('should return 200 on POST on pages', async () => { - const res = await fetchViaHTTP(appPort, '/user', null, { + const res = await next.fetch('/user', { method: 'POST', }) @@ -430,136 +400,123 @@ function runTests(dev = false) { }) it('should return JSON on post on API', async () => { - const data = await fetchViaHTTP(appPort, '/api/blog?title=Nextjs', null, { - method: 'POST', - }).then((res) => res.ok && res.json()) + const data = await next + .fetch('/api/blog?title=Nextjs', { + method: 'POST', + }) + .then((res) => res.ok && res.json()) expect(data).toEqual([{ title: 'Nextjs' }]) }) it('should return data on dynamic route', async () => { - const data = await fetchViaHTTP(appPort, '/api/post-1', null, {}).then( - (res) => res.ok && res.json() - ) + const data = await next + .fetch('/api/post-1') + .then((res) => res.ok && res.json()) expect(data).toEqual({ post: 'post-1' }) }) it('should work with dynamic params and search string', async () => { - const data = await fetchViaHTTP( - appPort, - '/api/post-1?val=1', - null, - {} - ).then((res) => res.ok && res.json()) + const data = await next + .fetch('/api/post-1?val=1') + .then((res) => res.ok && res.json()) expect(data).toEqual({ val: '1', post: 'post-1' }) }) it('should work with dynamic params and search string like lambda', async () => { - const res = await fetchViaHTTP(appPort, '/api/post-1?val=1') - const json = await res.json() + const res = await next.fetch('/api/post-1?val=1') + const body = await res.json() - expect(json).toEqual({ val: '1', post: 'post-1' }) + expect(body).toEqual({ val: '1', post: 'post-1' }) }) it('should prioritize a non-dynamic page', async () => { - const data = await fetchViaHTTP( - appPort, - '/api/post-1/comments', - null, - {} - ).then((res) => res.ok && res.json()) + const data = await next + .fetch('/api/post-1/comments') + .then((res) => res.ok && res.json()) expect(data).toEqual([{ message: 'Prioritize a non-dynamic api page' }]) }) it('should return data on dynamic nested route', async () => { - const data = await fetchViaHTTP( - appPort, - '/api/post-1/comment-1', - null, - {} - ).then((res) => res.ok && res.json()) + const data = await next + .fetch('/api/post-1/comment-1') + .then((res) => res.ok && res.json()) expect(data).toEqual({ post: 'post-1', comment: 'comment-1' }) }) it('should 404 on optional dynamic api page', async () => { - const res = await fetchViaHTTP(appPort, '/api/blog/543/comment', null, {}) + const res = await next.fetch('/api/blog/543/comment') expect(res.status).toBe(404) }) it('should return data on dynamic optional nested route', async () => { - const data = await fetchViaHTTP( - appPort, - '/api/blog/post-1/comment/1', - null, - {} - ).then((res) => res.ok && res.json()) + const data = await next + .fetch('/api/blog/post-1/comment/1') + .then((res) => res.ok && res.json()) expect(data).toEqual({ post: 'post-1', id: '1' }) }) it('should work with child_process correctly', async () => { - const data = await renderViaHTTP(appPort, '/api/child-process') + const data = await next.render('/api/child-process') expect(data).toBe('hi') }) it('should work with nullable payload', async () => { - const data = await renderViaHTTP(appPort, '/api/nullable-payload') + const data = await next.render('/api/nullable-payload') expect(data).toBe('') }) it('should warn if response body is larger than 4MB', async () => { - let res = await fetchViaHTTP(appPort, '/api/large-response') + let res = await next.fetch('/api/large-response') expect(res.ok).toBeTruthy() - expect(stderr).toContain( + expect(next.cliOutput).toContain( 'API response for /api/large-response exceeds 4MB. API Routes are meant to respond quickly.' ) - res = await fetchViaHTTP(appPort, '/api/large-chunked-response') + res = await next.fetch('/api/large-chunked-response') expect(res.ok).toBeTruthy() - expect(stderr).toContain( + expect(next.cliOutput).toContain( 'API response for /api/large-chunked-response exceeds 4MB. API Routes are meant to respond quickly.' ) }) it('should not warn if response body is larger than 4MB with responseLimit config = false', async () => { - await check(async () => { - let res = await fetchViaHTTP(appPort, '/api/large-response-with-config') + await retry(async () => { + let res = await next.fetch('/api/large-response-with-config') expect(res.ok).toBeTruthy() - expect(stderr).not.toContain( + expect(next.cliOutput).not.toContain( 'API response for /api/large-response-with-config exceeds 4MB. API Routes are meant to respond quickly.' ) - return 'success' - }, 'success') + }) }) it('should warn with configured size if response body is larger than configured size', async () => { - await check(async () => { - let res = await fetchViaHTTP( - appPort, - '/api/large-response-with-config-size' - ) + await retry(async () => { + let res = await next.fetch('/api/large-response-with-config-size') expect(res.ok).toBeTruthy() - expect(stderr).toContain( + expect(next.cliOutput).toContain( 'API response for /api/large-response-with-config-size exceeds 5MB. API Routes are meant to respond quickly.' ) - return 'success' - }, 'success') + }) }) - if (dev) { + if (isNextDev) { it('should compile only server code in development', async () => { - await fetchViaHTTP(appPort, '/api/users') + await next.fetch('/api/users') - expect(() => getPageFileFromBuildManifest(appDir, '/api/users')).toThrow( - /No files for page/ - ) + expect(() => + getPageFileFromBuildManifest(next.testDir, '/api/users') + ).toThrow(/No files for page/) - expect(getPageFileFromPagesManifest(appDir, '/api/users')).toBeTruthy() + expect( + getPageFileFromPagesManifest(next.testDir, '/api/users') + ).toBeTruthy() }) it('should show warning when the API resolves without ending the request in development mode', async () => { @@ -567,110 +524,99 @@ function runTests(dev = false) { setTimeout(() => { controller.abort() }, 2000) - await fetchViaHTTP(appPort, '/api/test-no-end', undefined, { - signal: controller.signal, - }).catch(() => {}) - - await check( - () => stderr, - /API resolved without sending a response for \/api\/test-no-end, this may result in stalled requests/ - ) + await next + .fetch('/api/test-no-end', { + signal: controller.signal, + }) + .catch(() => {}) + + await retry(() => { + expect(next.cliOutput).toMatch( + /API resolved without sending a response for \/api\/test-no-end, this may result in stalled requests/ + ) + }) }) it('should not show warning when the API resolves and the response is piped', async () => { - const startIdx = stderr.length > 0 ? stderr.length - 1 : stderr.length - await fetchViaHTTP(appPort, `/api/test-res-pipe`, { port: appPort }) - expect(stderr.slice(startIdx)).not.toContain( + const startIdx = + next.cliOutput.length > 0 + ? next.cliOutput.length - 1 + : next.cliOutput.length + await next.fetch(`/api/test-res-pipe?port=${next.appPort}`) + expect(next.cliOutput.slice(startIdx)).not.toContain( `API resolved without sending a response for /api/test-res-pipe` ) }) it('should show false positive warning if not using externalResolver flag', async () => { const apiURL = '/api/external-resolver-false-positive' - const req = await fetchViaHTTP(appPort, apiURL) + const req = await next.fetch(apiURL) expect(await req.text()).toBe('hello world') - check(() => { - expect(stderr).toContain( + await retry(() => { + expect(next.cliOutput).toContain( `API resolved without sending a response for ${apiURL}, this may result in stalled requests.` ) - return 'yes' - }, 'yes') + }) }) it('should not show warning if using externalResolver flag', async () => { - const startIdx = stderr.length > 0 ? stderr.length - 1 : stderr.length + const startIdx = + next.cliOutput.length > 0 + ? next.cliOutput.length - 1 + : next.cliOutput.length const apiURL = '/api/external-resolver' - const req = await fetchViaHTTP(appPort, apiURL) - expect(stderr.slice(startIdx)).not.toContain( + const req = await next.fetch(apiURL) + expect(next.cliOutput.slice(startIdx)).not.toContain( `API resolved without sending a response for ${apiURL}` ) expect(await req.text()).toBe('hello world') }) } else { - it('should show error with output export', async () => { - const nextConfig = new File(join(appDir, 'next.config.js')) - nextConfig.write(`module.exports = { output: 'export' }`) - try { - const { stderr, code } = await nextBuild(appDir, [], { stderr: true }) - expect(stderr).toContain('https://nextjs.org/docs/messages/gssp-export') - expect(code).toBe(1) - } finally { - nextConfig.delete() - } - }) - it('should build api routes', async () => { const pagesManifest = JSON.parse( - await fs.readFile( - join(appDir, `.next/${mode}/pages-manifest.json`), - 'utf8' - ) + await next.readFile(`.next/server/pages-manifest.json`) ) expect(Object.keys(pagesManifest).includes('/api/[post]')).toBeTruthy() - const res = await fetchViaHTTP(appPort, '/api/nextjs') - const json = await res.json() + const res = await next.fetch('/api/nextjs') + const body = await res.json() - expect(json).toEqual({ post: 'nextjs' }) + expect(body).toEqual({ post: 'nextjs' }) const buildManifest = JSON.parse( - await fs.readFile(join(appDir, '.next/build-manifest.json'), 'utf8') + await next.readFile('.next/build-manifest.json') ) expect( Object.keys(buildManifest.pages).includes('/api-conflict') ).toBeTruthy() }) } -} - -describe('API routes', () => { - describe('dev support', () => { - beforeAll(async () => { - stderr = '' - appPort = await findPort() - app = await launchApp(appDir, appPort, { - onStderr: (msg) => { - stderr += msg - }, - }) - }) - afterAll(() => killApp(app)) - - runTests(true) - }) - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( - 'production mode', - () => { - beforeAll(async () => { - await nextBuild(appDir) - mode = 'server' - appPort = await findPort() - app = await nextStart(appDir, appPort) - }) - afterAll(() => killApp(app)) +}) - runTests() - } - ) +describe('API routes output export error', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + dependencies: { + 'http-proxy': 'latest', + cors: 'latest', + 'node-fetch': '2.6.7', + }, + skipStart: true, + skipDeployment: true, + disableAutoSkewProtection: true, + }) + if (skipped) return + + it('should show error with output export', async () => { + if (isNextDev) return + + await next.patchFile( + 'next.config.js', + `module.exports = { output: 'export' }` + ) + const { exitCode, cliOutput } = await next.build() + expect(cliOutput).toContain('https://nextjs.org/docs/messages/gssp-export') + expect(exitCode).toBe(1) + }) }) diff --git a/test/integration/api-support/big.json b/test/e2e/api-support/big.json similarity index 100% rename from test/integration/api-support/big.json rename to test/e2e/api-support/big.json diff --git a/test/integration/api-support/pages/api-conflict.js b/test/e2e/api-support/pages/api-conflict.js similarity index 100% rename from test/integration/api-support/pages/api-conflict.js rename to test/e2e/api-support/pages/api-conflict.js diff --git a/test/integration/api-support/pages/api/[post]/[comment].js b/test/e2e/api-support/pages/api/[post]/[comment].js similarity index 100% rename from test/integration/api-support/pages/api/[post]/[comment].js rename to test/e2e/api-support/pages/api/[post]/[comment].js diff --git a/test/integration/api-support/pages/api/[post]/comments.js b/test/e2e/api-support/pages/api/[post]/comments.js similarity index 100% rename from test/integration/api-support/pages/api/[post]/comments.js rename to test/e2e/api-support/pages/api/[post]/comments.js diff --git a/test/integration/api-support/pages/api/[post]/index.js b/test/e2e/api-support/pages/api/[post]/index.js similarity index 100% rename from test/integration/api-support/pages/api/[post]/index.js rename to test/e2e/api-support/pages/api/[post]/index.js diff --git a/test/integration/api-support/pages/api/auth/[...nextauth].js b/test/e2e/api-support/pages/api/auth/[...nextauth].js similarity index 100% rename from test/integration/api-support/pages/api/auth/[...nextauth].js rename to test/e2e/api-support/pages/api/auth/[...nextauth].js diff --git a/test/integration/api-support/pages/api/big-parse.js b/test/e2e/api-support/pages/api/big-parse.js similarity index 100% rename from test/integration/api-support/pages/api/big-parse.js rename to test/e2e/api-support/pages/api/big-parse.js diff --git a/test/integration/api-support/pages/api/big-parse.ts b/test/e2e/api-support/pages/api/big-parse.ts similarity index 100% rename from test/integration/api-support/pages/api/big-parse.ts rename to test/e2e/api-support/pages/api/big-parse.ts diff --git a/test/integration/api-support/pages/api/blog/[post]/comment/[id].js b/test/e2e/api-support/pages/api/blog/[post]/comment/[id].js similarity index 100% rename from test/integration/api-support/pages/api/blog/[post]/comment/[id].js rename to test/e2e/api-support/pages/api/blog/[post]/comment/[id].js diff --git a/test/integration/api-support/pages/api/blog/index.js b/test/e2e/api-support/pages/api/blog/index.js similarity index 100% rename from test/integration/api-support/pages/api/blog/index.js rename to test/e2e/api-support/pages/api/blog/index.js diff --git a/test/integration/api-support/pages/api/bool.js b/test/e2e/api-support/pages/api/bool.js similarity index 100% rename from test/integration/api-support/pages/api/bool.js rename to test/e2e/api-support/pages/api/bool.js diff --git a/test/integration/api-support/pages/api/child-process.js b/test/e2e/api-support/pages/api/child-process.js similarity index 100% rename from test/integration/api-support/pages/api/child-process.js rename to test/e2e/api-support/pages/api/child-process.js diff --git a/test/integration/api-support/pages/api/cookies.js b/test/e2e/api-support/pages/api/cookies.js similarity index 100% rename from test/integration/api-support/pages/api/cookies.js rename to test/e2e/api-support/pages/api/cookies.js diff --git a/test/integration/api-support/pages/api/cors.js b/test/e2e/api-support/pages/api/cors.js similarity index 100% rename from test/integration/api-support/pages/api/cors.js rename to test/e2e/api-support/pages/api/cors.js diff --git a/test/integration/api-support/pages/api/error.js b/test/e2e/api-support/pages/api/error.js similarity index 100% rename from test/integration/api-support/pages/api/error.js rename to test/e2e/api-support/pages/api/error.js diff --git a/test/integration/api-support/pages/api/external-resolver-false-positive.js b/test/e2e/api-support/pages/api/external-resolver-false-positive.js similarity index 100% rename from test/integration/api-support/pages/api/external-resolver-false-positive.js rename to test/e2e/api-support/pages/api/external-resolver-false-positive.js diff --git a/test/integration/api-support/pages/api/external-resolver.js b/test/e2e/api-support/pages/api/external-resolver.js similarity index 100% rename from test/integration/api-support/pages/api/external-resolver.js rename to test/e2e/api-support/pages/api/external-resolver.js diff --git a/test/integration/api-support/pages/api/index.js b/test/e2e/api-support/pages/api/index.js similarity index 100% rename from test/integration/api-support/pages/api/index.js rename to test/e2e/api-support/pages/api/index.js diff --git a/test/integration/api-support/pages/api/json-null.js b/test/e2e/api-support/pages/api/json-null.js similarity index 100% rename from test/integration/api-support/pages/api/json-null.js rename to test/e2e/api-support/pages/api/json-null.js diff --git a/test/integration/api-support/pages/api/json-string.js b/test/e2e/api-support/pages/api/json-string.js similarity index 100% rename from test/integration/api-support/pages/api/json-string.js rename to test/e2e/api-support/pages/api/json-string.js diff --git a/test/integration/api-support/pages/api/json-undefined.js b/test/e2e/api-support/pages/api/json-undefined.js similarity index 100% rename from test/integration/api-support/pages/api/json-undefined.js rename to test/e2e/api-support/pages/api/json-undefined.js diff --git a/test/integration/api-support/pages/api/large-chunked-response.js b/test/e2e/api-support/pages/api/large-chunked-response.js similarity index 100% rename from test/integration/api-support/pages/api/large-chunked-response.js rename to test/e2e/api-support/pages/api/large-chunked-response.js diff --git a/test/integration/api-support/pages/api/large-response-with-config-size.js b/test/e2e/api-support/pages/api/large-response-with-config-size.js similarity index 100% rename from test/integration/api-support/pages/api/large-response-with-config-size.js rename to test/e2e/api-support/pages/api/large-response-with-config-size.js diff --git a/test/integration/api-support/pages/api/large-response-with-config.js b/test/e2e/api-support/pages/api/large-response-with-config.js similarity index 100% rename from test/integration/api-support/pages/api/large-response-with-config.js rename to test/e2e/api-support/pages/api/large-response-with-config.js diff --git a/test/integration/api-support/pages/api/large-response.js b/test/e2e/api-support/pages/api/large-response.js similarity index 100% rename from test/integration/api-support/pages/api/large-response.js rename to test/e2e/api-support/pages/api/large-response.js diff --git a/test/integration/api-support/pages/api/no-parsing.js b/test/e2e/api-support/pages/api/no-parsing.js similarity index 100% rename from test/integration/api-support/pages/api/no-parsing.js rename to test/e2e/api-support/pages/api/no-parsing.js diff --git a/test/integration/api-support/pages/api/nullable-payload.js b/test/e2e/api-support/pages/api/nullable-payload.js similarity index 100% rename from test/integration/api-support/pages/api/nullable-payload.js rename to test/e2e/api-support/pages/api/nullable-payload.js diff --git a/test/integration/api-support/pages/api/parse.js b/test/e2e/api-support/pages/api/parse.js similarity index 100% rename from test/integration/api-support/pages/api/parse.js rename to test/e2e/api-support/pages/api/parse.js diff --git a/test/integration/api-support/pages/api/parsing.js b/test/e2e/api-support/pages/api/parsing.js similarity index 100% rename from test/integration/api-support/pages/api/parsing.js rename to test/e2e/api-support/pages/api/parsing.js diff --git a/test/integration/api-support/pages/api/proxy-self.js b/test/e2e/api-support/pages/api/proxy-self.js similarity index 100% rename from test/integration/api-support/pages/api/proxy-self.js rename to test/e2e/api-support/pages/api/proxy-self.js diff --git a/test/integration/api-support/pages/api/query.js b/test/e2e/api-support/pages/api/query.js similarity index 100% rename from test/integration/api-support/pages/api/query.js rename to test/e2e/api-support/pages/api/query.js diff --git a/test/integration/api-support/pages/api/redirect-301.js b/test/e2e/api-support/pages/api/redirect-301.js similarity index 100% rename from test/integration/api-support/pages/api/redirect-301.js rename to test/e2e/api-support/pages/api/redirect-301.js diff --git a/test/integration/api-support/pages/api/redirect-307.js b/test/e2e/api-support/pages/api/redirect-307.js similarity index 100% rename from test/integration/api-support/pages/api/redirect-307.js rename to test/e2e/api-support/pages/api/redirect-307.js diff --git a/test/integration/api-support/pages/api/redirect-error.js b/test/e2e/api-support/pages/api/redirect-error.js similarity index 100% rename from test/integration/api-support/pages/api/redirect-error.js rename to test/e2e/api-support/pages/api/redirect-error.js diff --git a/test/integration/api-support/pages/api/redirect-null.js b/test/e2e/api-support/pages/api/redirect-null.js similarity index 100% rename from test/integration/api-support/pages/api/redirect-null.js rename to test/e2e/api-support/pages/api/redirect-null.js diff --git a/test/integration/api-support/pages/api/status-204.js b/test/e2e/api-support/pages/api/status-204.js similarity index 100% rename from test/integration/api-support/pages/api/status-204.js rename to test/e2e/api-support/pages/api/status-204.js diff --git a/test/integration/api-support/pages/api/test-no-end.js b/test/e2e/api-support/pages/api/test-no-end.js similarity index 100% rename from test/integration/api-support/pages/api/test-no-end.js rename to test/e2e/api-support/pages/api/test-no-end.js diff --git a/test/integration/api-support/pages/api/test-res-pipe.js b/test/e2e/api-support/pages/api/test-res-pipe.js similarity index 100% rename from test/integration/api-support/pages/api/test-res-pipe.js rename to test/e2e/api-support/pages/api/test-res-pipe.js diff --git a/test/integration/api-support/pages/api/user-error-async.js b/test/e2e/api-support/pages/api/user-error-async.js similarity index 100% rename from test/integration/api-support/pages/api/user-error-async.js rename to test/e2e/api-support/pages/api/user-error-async.js diff --git a/test/integration/api-support/pages/api/user-error.js b/test/e2e/api-support/pages/api/user-error.js similarity index 100% rename from test/integration/api-support/pages/api/user-error.js rename to test/e2e/api-support/pages/api/user-error.js diff --git a/test/integration/api-support/pages/api/users.js b/test/e2e/api-support/pages/api/users.js similarity index 100% rename from test/integration/api-support/pages/api/users.js rename to test/e2e/api-support/pages/api/users.js diff --git a/test/integration/api-support/pages/index.js b/test/e2e/api-support/pages/index.js similarity index 100% rename from test/integration/api-support/pages/index.js rename to test/e2e/api-support/pages/index.js diff --git a/test/integration/api-support/pages/user.js b/test/e2e/api-support/pages/user.js similarity index 100% rename from test/integration/api-support/pages/user.js rename to test/e2e/api-support/pages/user.js diff --git a/test/e2e/app-dir-export/test/config.test.ts b/test/e2e/app-dir-export/test/config.test.ts index 6717cae829ea..4008ce409325 100644 --- a/test/e2e/app-dir-export/test/config.test.ts +++ b/test/e2e/app-dir-export/test/config.test.ts @@ -1,4 +1,3 @@ -import { runNextCommand } from 'next-test-utils' import { join } from 'path' import { expectedWhenTrailingSlashTrue, getFiles } from './utils' import { FileRef, isNextStart, nextTestSetup, PatchedFileRef } from 'e2e-utils' @@ -46,27 +45,14 @@ describe('app dir - with output export', () => { expect(exitCode).toBe(0) expect(await getFiles(join(next.testDir, 'out'))).toEqual([]) - let stdout = '' - let stderr = '' - let error = undefined - try { - await runNextCommand(['export'], { - cwd: next.testDir, - onStdout(msg) { - stdout += msg - }, - onStderr(msg) { - stderr += msg - }, - }) - } catch (e) { - error = e - } - expect(error).toBeDefined() - expect(stderr).toContain( + const result = await next.runCommand(['export']) + expect(result.exitCode).not.toBe(0) + expect(result.stderr).toContain( `\`next export\` has been removed in favor of 'output: export' in next.config.js` ) - expect(stdout).not.toContain('Export successful. Files written to') + expect(result.stdout).not.toContain( + 'Export successful. Files written to' + ) expect(await getFiles(join(next.testDir, 'out'))).toEqual([]) }) }) 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 7f8a48288c53..b1b5a160a58b 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 @@ -11,12 +11,15 @@ import path from 'path' // This preserves existing tests for the 30s/5min heuristic (previous router defaults) describe('app dir client cache semantics (30s/5min)', () => { - const { next, isNextDev } = nextTestSetup({ + const { next, isNextDev, skipped } = nextTestSetup({ files: path.join(__dirname, 'fixtures', 'regular'), nextConfig: { experimental: { staleTimes: { dynamic: 30, static: 180 } }, }, + // Assertions don't apply to deploy mode (output differs vs. local Next.js server). + skipDeployment: true, }) + if (skipped) return if (isNextDev) { // dev doesn't support prefetch={true}, so this just performs a basic test to make sure data is reused for 30s diff --git a/test/e2e/app-dir/app-root-params-getters/generate-static-params-error.test.ts b/test/e2e/app-dir/app-root-params-getters/generate-static-params-error.test.ts index e654d16c03ef..3abb253d4678 100644 --- a/test/e2e/app-dir/app-root-params-getters/generate-static-params-error.test.ts +++ b/test/e2e/app-dir/app-root-params-getters/generate-static-params-error.test.ts @@ -2,11 +2,12 @@ import { nextTestSetup } from 'e2e-utils' import path from 'path' describe('app-root-param-getters - generateStaticParams error', () => { - const { next, isNextDev } = nextTestSetup({ + const { next, isNextDev, skipped } = nextTestSetup({ files: path.join(__dirname, 'fixtures', 'generate-static-params-error'), skipStart: true, skipDeployment: true, }) + if (skipped) return beforeAll(async () => { try { diff --git a/test/e2e/app-dir/error-on-next-codemod-comment/error-on-next-codemod-comment.test.ts b/test/e2e/app-dir/error-on-next-codemod-comment/error-on-next-codemod-comment.test.ts index 0022b667987a..c44b60838fa7 100644 --- a/test/e2e/app-dir/error-on-next-codemod-comment/error-on-next-codemod-comment.test.ts +++ b/test/e2e/app-dir/error-on-next-codemod-comment/error-on-next-codemod-comment.test.ts @@ -7,11 +7,12 @@ import { } from 'next-test-utils' describe('app-dir - error-on-next-codemod-comment', () => { - const { next, isNextDev } = nextTestSetup({ + const { next, isNextDev, skipped } = nextTestSetup({ files: __dirname, skipStart: true, skipDeployment: true, }) + if (skipped) return if (isNextDev) { beforeAll(async () => { diff --git a/test/e2e/app-dir/interception-routes-output-export/interception-routes-output-export.test.ts b/test/e2e/app-dir/interception-routes-output-export/interception-routes-output-export.test.ts index ca18ed020972..037e94e8c1cf 100644 --- a/test/e2e/app-dir/interception-routes-output-export/interception-routes-output-export.test.ts +++ b/test/e2e/app-dir/interception-routes-output-export/interception-routes-output-export.test.ts @@ -1,30 +1,49 @@ -import { isNextDev, isNextStart } from 'e2e-utils' -import { findPort, killApp, launchApp, nextBuild, retry } from 'next-test-utils' +import type { ChildProcess } from 'child_process' +import { isNextDev, isNextStart, nextTestSetup } from 'e2e-utils' +import { findPort, killApp, retry } from 'next-test-utils' describe('interception-routes-output-export', () => { + const { next } = nextTestSetup({ + files: __dirname, + skipStart: true, + // Vercel deployment fails to build/deploy this fixture in CI; skip in deploy mode. + skipDeployment: true, + }) + it('should error when using interception routes with static export', async () => { if (isNextStart) { - const { code, stderr } = await nextBuild(__dirname, [], { stderr: true }) - expect(stderr).toContain( + const { exitCode, cliOutput } = await next.build() + expect(cliOutput).toContain( 'Intercepting routes are not supported with static export.' ) - expect(code).toBe(1) + expect(exitCode).toBe(1) } else if (isNextDev) { let stderr = '' + let child: ChildProcess | undefined const port = await findPort() - const app = await launchApp(__dirname, port, { + const exit = next.runCommand(['dev', '-p', String(port)], { onStderr(msg) { stderr += msg || '' }, + instance: (p) => { + child = p + }, }) - await retry(async () => { - expect(stderr).toContain( - 'Intercepting routes are not supported with static export.' - ) - }) - - await killApp(app) + try { + // The dev server can take a while to boot in CI before the + // interception route check runs, so use a generous timeout. + await retry(async () => { + expect(stderr).toContain( + 'Intercepting routes are not supported with static export.' + ) + }, 30 * 1000) + } finally { + if (child) { + await killApp(child).catch(() => {}) + } + await exit.catch(() => {}) + } } }) }) diff --git a/test/e2e/app-dir/logging/fetch-logging.test.ts b/test/e2e/app-dir/logging/fetch-logging.test.ts index f6445a76fb30..c661f77afc65 100644 --- a/test/e2e/app-dir/logging/fetch-logging.test.ts +++ b/test/e2e/app-dir/logging/fetch-logging.test.ts @@ -80,10 +80,11 @@ describe('app-dir - fetch logging', () => { }) describe('app-dir - logging', () => { - const { next, isNextDev } = nextTestSetup({ + const { next, isNextDev, skipped } = nextTestSetup({ skipDeployment: true, files: __dirname, }) + if (skipped) return function runTests({ withFetchesLogging, withFullUrlFetches = false, diff --git a/test/e2e/app-dir/middleware-rsc-external-rewrite/middleware-rsc-external-rewrite.test.ts b/test/e2e/app-dir/middleware-rsc-external-rewrite/middleware-rsc-external-rewrite.test.ts index 8ab8361deb96..75554f030f94 100644 --- a/test/e2e/app-dir/middleware-rsc-external-rewrite/middleware-rsc-external-rewrite.test.ts +++ b/test/e2e/app-dir/middleware-rsc-external-rewrite/middleware-rsc-external-rewrite.test.ts @@ -1,6 +1,5 @@ -import webdriver from 'next-webdriver' -import { findPort, nextBuild, nextStart } from 'next-test-utils' -import { isNextDeploy, isNextDev } from 'e2e-utils' +import { findPort } from 'next-test-utils' +import { isNextDeploy, isNextDev, nextTestSetup } from 'e2e-utils' import { startExternalServer } from './external-server.mjs' describe('middleware RSC external rewrite', () => { @@ -9,65 +8,43 @@ describe('middleware RSC external rewrite', () => { return } - let cleanup: () => Promise<void> - let nextPort: number + const { next } = nextTestSetup({ + files: __dirname, + skipStart: true, + }) + let externalServerManager: { cleanup: () => Promise<void> getReceivedRequests: () => any[] } beforeAll(async () => { - const appDir = __dirname - await nextBuild(appDir, undefined, { cwd: appDir }) - - // Start external server first const externalPort = await findPort() - process.env.EXTERNAL_SERVER_PORT = externalPort.toString() externalServerManager = await startExternalServer(externalPort) - - // Start Next.js server - nextPort = await findPort() - const nextApp = await nextStart(appDir, nextPort, { - env: { - ...process.env, - EXTERNAL_SERVER_PORT: externalPort.toString(), - }, - }) - - cleanup = async () => { - await nextApp.kill() - await externalServerManager.cleanup() - } + next.env.EXTERNAL_SERVER_PORT = String(externalPort) + await next.start() }) afterAll(async () => { - if (cleanup) { - await cleanup() - } + await next.stop() + await externalServerManager?.cleanup() }) test('should forward _rsc parameter to external server on RSC navigation', async () => { - let browser + const browser = await next.browser('/') try { - browser = await webdriver(nextPort, '/') - - // Verify we're on the home page const homeContent = await browser.elementById('home-content') expect(await homeContent.text()).toContain('This is the home page') - // Clear any previous requests const initialRequests = externalServerManager.getReceivedRequests() console.log('Initial requests before navigation:', initialRequests.length) - // Click the link to /about which should trigger RSC navigation const aboutLink = await browser.elementById('about-link') await aboutLink.click() - // Wait a bit for the request to be processed await browser.waitForElementByCss('#external-response', 5000) - // Check that external server received the request const receivedRequests = externalServerManager.getReceivedRequests() console.log('Total requests received:', receivedRequests.length) console.log( @@ -75,7 +52,6 @@ describe('middleware RSC external rewrite', () => { receivedRequests.map((r) => ({ url: r.url, method: r.method })) ) - // Find requests that contain _rsc parameter const rscRequests = receivedRequests.filter((req) => req.url.includes('_rsc=') ) @@ -84,18 +60,14 @@ describe('middleware RSC external rewrite', () => { rscRequests.map((r) => r.url) ) - // Verify that at least one request contains the _rsc parameter expect(rscRequests.length).toBeGreaterThan(0) - // Verify the external server response is displayed const externalResponse = await browser.elementById('external-response') expect(await externalResponse.text()).toBe( 'External server handled the request' ) } finally { - if (browser) { - await browser.close() - } + await browser.close() } }) }) diff --git a/test/e2e/app-dir/ppr-errors/ppr-errors.test.ts b/test/e2e/app-dir/ppr-errors/ppr-errors.test.ts index c17d9c13fa08..ccb8107f8584 100644 --- a/test/e2e/app-dir/ppr-errors/ppr-errors.test.ts +++ b/test/e2e/app-dir/ppr-errors/ppr-errors.test.ts @@ -1,53 +1,48 @@ -import { nextBuild } from 'next-test-utils' -// In order for the global isNextStart to be set -import 'e2e-utils' +import { isNextStart, nextTestSetup } from 'e2e-utils' // TODO(NAR-423): Migrate to Cache Components. describe.skip('ppr build errors', () => { - ;(Boolean((global as any).isNextStart) ? describe : describe.skip)( - 'production only', - () => { - let stderr: string - let stdout: string + ;(isNextStart ? describe : describe.skip)('production only', () => { + const { next } = nextTestSetup({ + files: __dirname, + skipStart: true, + }) - beforeAll(async () => { - const output = await nextBuild(__dirname, [], { - stderr: true, - stdout: true, - }) - stderr = output.stderr - stdout = output.stdout - }) + let cliOutput: string + + beforeAll(async () => { + const output = await next.build() + cliOutput = output.cliOutput + }) - describe('within a suspense boundary', () => { - it('should fail the build for uncaught prerender errors', async () => { - expect(stderr).toContain( - 'Error occurred prerendering page "/regular-error-suspense-boundary".' - ) - expect(stderr).toContain( - 'Error occurred prerendering page "/re-throwing-error".' - ) - }) + describe('within a suspense boundary', () => { + it('should fail the build for uncaught prerender errors', async () => { + expect(cliOutput).toContain( + 'Error occurred prerendering page "/regular-error-suspense-boundary".' + ) + expect(cliOutput).toContain( + 'Error occurred prerendering page "/re-throwing-error".' + ) }) + }) - describe('outside of a suspense boundary', () => { - it('should fail the build for uncaught errors', async () => { - expect(stderr).toContain( - 'Error occurred prerendering page "/regular-error".' - ) - expect(stderr).toContain( - 'Error occurred prerendering page "/no-suspense-boundary-re-throwing-error".' - ) - }) + describe('outside of a suspense boundary', () => { + it('should fail the build for uncaught errors', async () => { + expect(cliOutput).toContain( + 'Error occurred prerendering page "/regular-error".' + ) + expect(cliOutput).toContain( + 'Error occurred prerendering page "/no-suspense-boundary-re-throwing-error".' + ) }) + }) - describe('when a postpone call is caught and logged it should', () => { - it('should include a message telling why', async () => { - expect(stdout).toContain( - 'User land logged error: Route /logging-error needs to bail out of prerendering at this point because it used cookies.' - ) - }) + describe('when a postpone call is caught and logged it should', () => { + it('should include a message telling why', async () => { + expect(cliOutput).toContain( + 'User land logged error: Route /logging-error needs to bail out of prerendering at this point because it used cookies.' + ) }) - } - ) + }) + }) }) diff --git a/test/e2e/app-dir/ppr-missing-root-params/ppr-missing-root-params.test.ts b/test/e2e/app-dir/ppr-missing-root-params/ppr-missing-root-params.test.ts index 59786d94ed30..2f0f9e2e70eb 100644 --- a/test/e2e/app-dir/ppr-missing-root-params/ppr-missing-root-params.test.ts +++ b/test/e2e/app-dir/ppr-missing-root-params/ppr-missing-root-params.test.ts @@ -2,11 +2,12 @@ import { nextTestSetup } from 'e2e-utils' import path from 'path' describe('ppr-missing-root-params (single)', () => { - const { next, isNextDev } = nextTestSetup({ + const { next, isNextDev, skipped } = nextTestSetup({ files: path.join(__dirname, 'fixtures/single'), skipStart: true, skipDeployment: true, }) + if (skipped) return beforeAll(async () => { try { @@ -26,11 +27,12 @@ describe('ppr-missing-root-params (single)', () => { }) describe('ppr-missing-root-params (multiple)', () => { - const { next, isNextDev } = nextTestSetup({ + const { next, isNextDev, skipped } = nextTestSetup({ files: path.join(__dirname, 'fixtures/multiple'), skipStart: true, skipDeployment: true, }) + if (skipped) return beforeAll(async () => { try { @@ -50,11 +52,12 @@ describe('ppr-missing-root-params (multiple)', () => { }) describe('ppr-missing-root-params (nested)', () => { - const { next, isNextDev } = nextTestSetup({ + const { next, isNextDev, skipped } = nextTestSetup({ files: path.join(__dirname, 'fixtures/nested'), skipStart: true, skipDeployment: true, }) + if (skipped) return beforeAll(async () => { try { diff --git a/test/e2e/app-dir/rewrite-headers/rewrite-headers.test.ts b/test/e2e/app-dir/rewrite-headers/rewrite-headers.test.ts index 6ebf12c12c2b..208d4a2ba72c 100644 --- a/test/e2e/app-dir/rewrite-headers/rewrite-headers.test.ts +++ b/test/e2e/app-dir/rewrite-headers/rewrite-headers.test.ts @@ -461,11 +461,12 @@ const cases: { ] describe('rewrite-headers', () => { - const { next } = nextTestSetup({ + const { next, skipped } = nextTestSetup({ files: __dirname, // TODO: re-enable once changes in infrastructure are merged skipDeployment: true, }) + if (skipped) return describe.each(cases)( '$name ($pathname)', diff --git a/test/e2e/app-dir/scss/invalid-global-module/invalid-global-module.test.ts b/test/e2e/app-dir/scss/invalid-global-module/invalid-global-module.test.ts index 2236f623a81a..bb1dd6a2ab58 100644 --- a/test/e2e/app-dir/scss/invalid-global-module/invalid-global-module.test.ts +++ b/test/e2e/app-dir/scss/invalid-global-module/invalid-global-module.test.ts @@ -1,38 +1,28 @@ /* eslint-env jest */ -import { remove } from 'fs-extra' -import { nextBuild } from 'next-test-utils' -import { join } from 'path' -// In order for the global isNextStart to be set -import 'e2e-utils' +import { isNextStart, nextTestSetup } from 'e2e-utils' describe.skip('Invalid CSS Global Module Usage in node_modules', () => { - ;(Boolean((global as any).isNextStart) ? describe : describe.skip)( - 'production only', - () => { - const appDir = __dirname + ;(isNextStart ? describe : describe.skip)('production only', () => { + const { next } = nextTestSetup({ + files: __dirname, + skipStart: true, + }) - beforeAll(async () => { - await remove(join(appDir, '.next')) - }) - - it('should fail to build', async () => { - const { code, stderr } = await nextBuild(appDir, [], { - stderr: true, - }) - expect(code).not.toBe(0) - expect(stderr).toContain('Failed to compile') - expect(stderr).toContain('node_modules/example/index.scss') - expect(stderr).toMatch( - /Global CSS.*cannot.*be imported from within.*node_modules/ + it('should fail to build', async () => { + const { exitCode, cliOutput } = await next.build() + expect(exitCode).not.toBe(0) + expect(cliOutput).toContain('Failed to compile') + expect(cliOutput).toContain('node_modules/example/index.scss') + expect(cliOutput).toMatch( + /Global CSS.*cannot.*be imported from within.*node_modules/ + ) + // Skip: Rspack loaders cannot access module issuer info for location details + if (!process.env.NEXT_RSPACK) { + expect(cliOutput).toMatch( + /Location:.*node_modules[\\/]example[\\/]index\.mjs/ ) - // Skip: Rspack loaders cannot access module issuer info for location details - if (!process.env.NEXT_RSPACK) { - expect(stderr).toMatch( - /Location:.*node_modules[\\/]example[\\/]index\.mjs/ - ) - } - }) - } - ) + } + }) + }) }) diff --git a/test/e2e/app-dir/scss/invalid-module/invalid-module.test.ts b/test/e2e/app-dir/scss/invalid-module/invalid-module.test.ts deleted file mode 100644 index e51ca01f58e9..000000000000 --- a/test/e2e/app-dir/scss/invalid-module/invalid-module.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* eslint-env jest */ - -import { remove } from 'fs-extra' -import { nextBuild } from 'next-test-utils' -import { join } from 'path' -// In order for the global isNextStart to be set -import 'e2e-utils' - -describe.skip('Invalid CSS Module Usage in node_modules', () => { - ;(Boolean((global as any).isNextStart) ? describe : describe.skip)( - 'production only', - () => { - const appDir = __dirname - - beforeAll(async () => { - await remove(join(appDir, '.next')) - }) - - it('should fail to build', async () => { - const { code, stderr } = await nextBuild(appDir, [], { - stderr: true, - }) - expect(code).not.toBe(0) - expect(stderr).toContain('Failed to compile') - expect(stderr).toContain('node_modules/example/index.module.scss') - expect(stderr).toMatch( - /CSS Modules.*cannot.*be imported from within.*node_modules/ - ) - // Skip: Rspack loaders cannot access module issuer info for location details - if (!process.env.NEXT_RSPACK) { - expect(stderr).toMatch( - /Location:.*node_modules[\\/]example[\\/]index\.mjs/ - ) - } - }) - } - ) -}) diff --git a/test/e2e/app-dir/segment-cache/cdn-cache-busting/cdn-cache-busting.test.ts b/test/e2e/app-dir/segment-cache/cdn-cache-busting/cdn-cache-busting.test.ts index 65d116bf3991..da131a6a9b41 100644 --- a/test/e2e/app-dir/segment-cache/cdn-cache-busting/cdn-cache-busting.test.ts +++ b/test/e2e/app-dir/segment-cache/cdn-cache-busting/cdn-cache-busting.test.ts @@ -1,9 +1,10 @@ import type * as Playwright from 'playwright' -import webdriver from 'next-webdriver' +import type { ChildProcess } from 'child_process' +import type { Server } from 'http' import { createRouterAct } from 'router-act' -import { findPort, nextBuild } from 'next-test-utils' -import { isNextDeploy, isNextDev } from 'e2e-utils' -import { start } from './server.mjs' +import { findPort } from 'next-test-utils' +import { isNextDeploy, isNextDev, nextTestSetup } from 'e2e-utils' +import { createFakeCDN } from './server.mjs' describe('segment cache (CDN cache busting)', () => { if (isNextDev || isNextDeploy) { @@ -11,6 +12,11 @@ describe('segment cache (CDN cache busting)', () => { return } + const { next } = nextTestSetup({ + files: __dirname, + skipStart: true, + }) + // TODO(runtime-ppr): add tests for runtime prefetches // To debug these tests locally, run: @@ -19,19 +25,43 @@ describe('segment cache (CDN cache busting)', () => { // This will start the Next app and also a proxy server that simulates a CDN. // Like certain real-world CDNs, our fake CDN doesn't respect the Vary header. // It only uses the URL. - let cleanup: () => Promise<void> + let nextChild: ChildProcess | undefined + let nextExit: Promise<any> | undefined + let cdnServer: Server let port: number beforeAll(async () => { - const appDir = __dirname - await nextBuild(appDir, undefined, { cwd: appDir }) - const proxyPort = (port = await findPort()) + await next.build() const nextPort = await findPort() - cleanup = await start(proxyPort, nextPort) + const proxyPort = (port = await findPort()) + + let resolveReady!: () => void + const readyPromise = new Promise<void>((r) => (resolveReady = r)) + nextExit = next + .runCommand(['start', '-p', String(nextPort)], { + onStdout(msg) { + if (/Ready/.test(msg)) resolveReady() + }, + instance(p) { + nextChild = p + }, + }) + .finally(() => resolveReady()) + await readyPromise + + cdnServer = await createFakeCDN(nextPort) + await new Promise<void>((resolve, reject) => { + cdnServer.on('error', reject) + cdnServer.listen(proxyPort, () => resolve()) + }) }) afterAll(async () => { - await cleanup() + if (cdnServer) { + await new Promise<void>((resolve) => cdnServer.close(() => resolve())) + } + nextChild?.kill() + await nextExit?.catch(() => {}) }) it( @@ -39,17 +69,13 @@ describe('segment cache (CDN cache busting)', () => { 'the Vary header', async () => { let act - const browser = await webdriver(port, '/', { + const browser = await next.browser('/', { + baseUrl: port, beforePageLoad(p: Playwright.Page) { act = createRouterAct(p) }, }) - // Initiate a prefetch. Each segment will be prefetched individually, - // using the pathname of the target page and a custom header specifying - // the segment. If we didn't also set a cache-busting search param, then - // the fake CDN used by this test suite would incorrectly use the same - // entry for every segment, poisoning the cache. await act( async () => { const linkToggle = await browser.elementByCss( @@ -62,13 +88,10 @@ describe('segment cache (CDN cache busting)', () => { } ) - // Navigate to the prefetched target page. await act(async () => { const link = await browser.elementByCss('a[href="/target-page"]') await link.click() - // The page was prefetched, so we're able to render the target - // page immediately. const div = await browser.elementById('target-page') expect(await div.text()).toBe('Target page') }, 'no-requests') @@ -80,7 +103,7 @@ describe('segment cache (CDN cache busting)', () => { 'cache busting query param if a custom header is sent during a prefetch ' + 'without a corresponding cache-busting search param', async () => { - const browser = await webdriver(port, '/') + const browser = await next.browser('/', { baseUrl: port }) const { status, responseUrl, redirected } = await browser.eval( async () => { const res = await fetch('/target-page', { @@ -133,7 +156,8 @@ describe('segment cache (CDN cache busting)', () => { 'performs a redirect', async () => { let act - const browser = await webdriver(port, '/', { + const browser = await next.browser('/', { + baseUrl: port, beforePageLoad(p: Playwright.Page) { act = createRouterAct(p) }, @@ -151,15 +175,12 @@ describe('segment cache (CDN cache busting)', () => { } ) - // Navigate to the prefetched target page. await act(async () => { const link = await browser.elementByCss( 'a[href="/redirect-to-target-page"]' ) await link.click() - // The page was prefetched, so we're able to render the target - // page immediately. const div = await browser.elementById('target-page') expect(await div.text()).toBe('Target page') }, 'no-requests') diff --git a/test/e2e/app-dir/segment-cache/cdn-cache-busting/server.mjs b/test/e2e/app-dir/segment-cache/cdn-cache-busting/server.mjs index 30745c88f0f5..27c4ee31a3c9 100644 --- a/test/e2e/app-dir/segment-cache/cdn-cache-busting/server.mjs +++ b/test/e2e/app-dir/segment-cache/cdn-cache-busting/server.mjs @@ -50,7 +50,7 @@ function isCacheableResponse(res) { return !res.headers['cache-control']?.includes('no-store') } -async function createFakeCDN(destPort) { +export async function createFakeCDN(destPort) { const fakeCDNCache = new Map() const proxy = httpProxy.createProxyServer() diff --git a/test/e2e/app-dir/segment-cache/export/segment-cache-output-export.test.ts b/test/e2e/app-dir/segment-cache/export/segment-cache-output-export.test.ts index d1ce5ca30b08..9a799b34b253 100644 --- a/test/e2e/app-dir/segment-cache/export/segment-cache-output-export.test.ts +++ b/test/e2e/app-dir/segment-cache/export/segment-cache-output-export.test.ts @@ -1,9 +1,11 @@ import type * as Playwright from 'playwright' +import type { Server } from 'http' import webdriver from 'next-webdriver' import { createRouterAct } from 'router-act' -import { findPort, nextBuild } from 'next-test-utils' -import { isNextStart } from 'e2e-utils' -import { server } from './server.mjs' +import { findPort } from 'next-test-utils' +import { isNextStart, nextTestSetup } from 'e2e-utils' +import { join } from 'path' +import { createExportServer } from './server.mjs' describe('segment cache (output: "export")', () => { if (!isNextStart) { @@ -11,6 +13,12 @@ describe('segment cache (output: "export")', () => { return } + const { next } = nextTestSetup({ + files: __dirname, + skipStart: true, + disableAutoSkewProtection: true, + }) + // To debug these tests locally, first build the app, then run: // // node start.mjs @@ -19,19 +27,17 @@ describe('segment cache (output: "export")', () => { // rewrite, which some of the tests below rely on. let port: number + let server: Server beforeAll(async () => { - const appDir = __dirname - await nextBuild(appDir, undefined, { - cwd: appDir, - disableAutoSkewProtection: true, - }) + await next.build() port = await findPort() + server = createExportServer(join(next.testDir, 'out')) server.listen(port) }) afterAll(() => { - server.close() + server?.close() }) it('basic prefetch in output: "export" mode', async () => { @@ -42,7 +48,6 @@ describe('segment cache (output: "export")', () => { }, }) - // Initiate a prefetch await act( async () => { const checkbox = await browser.elementByCss( @@ -55,22 +60,17 @@ describe('segment cache (output: "export")', () => { } ) - // Navigate to the prefetched target page. await act( async () => { const link = await browser.elementByCss('a[href="/target-page"]') await link.click() - // The page was prefetched, so we're able to render the target - // page immediately. const div = await browser.elementById('target-page') expect(await div.text()).toBe('Target page') - // The target page includes a link back to the home page await browser.elementByCss('a[href="/"]') }, { - // Should have prefetched the home page includes: 'Demonstrates that per-segment prefetching works', } ) @@ -84,7 +84,6 @@ describe('segment cache (output: "export")', () => { }, }) - // Initiate a prefetch await act( async () => { const checkbox = await browser.elementByCss( @@ -97,7 +96,6 @@ describe('segment cache (output: "export")', () => { } ) - // Navigate to the prefetched page. await act( async () => { const link = await browser.elementByCss( @@ -105,16 +103,12 @@ describe('segment cache (output: "export")', () => { ) await link.click() - // The page was prefetched, so we're able to render the target - // page immediately. const div = await browser.elementById('target-page') expect(await div.text()).toBe('Target page') - // The target page includes a link back to the home page await browser.elementByCss('a[href="/"]') }, { - // Should have prefetched the home page includes: 'Demonstrates that per-segment prefetching works', } ) @@ -128,7 +122,6 @@ describe('segment cache (output: "export")', () => { }, }) - // Initiate a prefetch await act( async () => { const checkbox = await browser.elementByCss( @@ -141,7 +134,6 @@ describe('segment cache (output: "export")', () => { } ) - // Navigate to the prefetched page. await act( async () => { const link = await browser.elementByCss( @@ -149,16 +141,12 @@ describe('segment cache (output: "export")', () => { ) await link.click() - // The page was prefetched, so we're able to render the target - // page immediately. const div = await browser.elementById('target-page') expect(await div.text()).toBe('Target page') - // The target page includes a link back to the home page await browser.elementByCss('a[href="/"]') }, { - // Should have prefetched the home page includes: 'Demonstrates that per-segment prefetching works', } ) diff --git a/test/e2e/app-dir/segment-cache/export/server.mjs b/test/e2e/app-dir/segment-cache/export/server.mjs index f96006b798a1..71fec24a5ee5 100644 --- a/test/e2e/app-dir/segment-cache/export/server.mjs +++ b/test/e2e/app-dir/segment-cache/export/server.mjs @@ -10,32 +10,36 @@ import { createServer } from 'node:http' // TODO: We should improve our documentation around this. import handler from 'serve-handler' -const OUT_DIR = join(dirname(fileURLToPath(import.meta.url)), 'out') +const DEFAULT_OUT_DIR = join(dirname(fileURLToPath(import.meta.url)), 'out') -export const server = createServer((request, response) => { - // Redirect /redirect-to-target-page to /target-page. Notice that we only have - // to redirect the path of the page, not any other resources. - if (request.url === '/redirect-to-target-page') { - console.log('Redirecting to /target-page') - response.writeHead(302, { Location: '/target-page' }) - response.end() - return - } +export function createExportServer(outDir = DEFAULT_OUT_DIR) { + return createServer((request, response) => { + // Redirect /redirect-to-target-page to /target-page. Notice that we only have + // to redirect the path of the page, not any other resources. + if (request.url === '/redirect-to-target-page') { + console.log('Redirecting to /target-page') + response.writeHead(302, { Location: '/target-page' }) + response.end() + return + } - // Rewrite /rewrite-to-target-page to /target-page - // NOTE: This simulates a rewrite using a proxy, which is not something we - // officially support or document. It's just here to illustrate how it would - // be done in theory. - if (/^\/rewrite-to-target-page\/?[^/]*$/.test(request.url)) { - const newUrl = request.url.replace( - '/rewrite-to-target-page', - '/target-page' - ) - console.log(`Rewriting ${request.url} to ${newUrl}`) - request.url = newUrl - } + // Rewrite /rewrite-to-target-page to /target-page + // NOTE: This simulates a rewrite using a proxy, which is not something we + // officially support or document. It's just here to illustrate how it would + // be done in theory. + if (/^\/rewrite-to-target-page\/?[^/]*$/.test(request.url)) { + const newUrl = request.url.replace( + '/rewrite-to-target-page', + '/target-page' + ) + console.log(`Rewriting ${request.url} to ${newUrl}`) + request.url = newUrl + } - return handler(request, response, { - public: OUT_DIR, + return handler(request, response, { + public: outDir, + }) }) -}) +} + +export const server = createExportServer() diff --git a/test/e2e/app-dir/typed-routes/typed-routes.test.ts b/test/e2e/app-dir/typed-routes/typed-routes.test.ts index 405e8f19260f..def58edbe07d 100644 --- a/test/e2e/app-dir/typed-routes/typed-routes.test.ts +++ b/test/e2e/app-dir/typed-routes/typed-routes.test.ts @@ -1,6 +1,6 @@ import { nextTestSetup } from 'e2e-utils' import execa from 'execa' -import { retry, runNextCommand } from 'next-test-utils' +import { retry } from 'next-test-utils' const expectedDts = ` type AppRoutes = "/" | "/_shop/[[...category]]" | "/dashboard" | "/dashboard/settings" | "/docs/[...slug]" | "/gallery/photo/[id]" | "/project/[slug]" @@ -129,11 +129,9 @@ type InvalidRoute = RouteContext<'/api/users/invalid'>` }) it('should exit typegen successfully', async () => { - const { code } = await runNextCommand(['typegen'], { - cwd: next.testDir, - }) + const { exitCode } = await next.runCommand(['typegen']) - expect(code).toBe(0) + expect(exitCode).toBe(0) }) } }) diff --git a/test/e2e/app-dir/webpack-loader-errors/webpack-loader-errors.test.ts b/test/e2e/app-dir/webpack-loader-errors/webpack-loader-errors.test.ts index 4e2b4a9f5a01..cccf8455fd53 100644 --- a/test/e2e/app-dir/webpack-loader-errors/webpack-loader-errors.test.ts +++ b/test/e2e/app-dir/webpack-loader-errors/webpack-loader-errors.test.ts @@ -3,11 +3,12 @@ import { retry, waitForRedbox, getRedboxSource } from 'next-test-utils' import stripAnsi from 'strip-ansi' describe('webpack-loader-errors', () => { - const { next, isNextDev, isTurbopack } = nextTestSetup({ + const { next, isNextDev, isTurbopack, skipped } = nextTestSetup({ files: __dirname, skipDeployment: true, skipStart: true, }) + if (skipped) return if (!isNextDev) { it('should skip in non-dev mode', () => {}) diff --git a/test/e2e/app-document-import-order/app-document-import-order.test.ts b/test/e2e/app-document-import-order/app-document-import-order.test.ts new file mode 100644 index 000000000000..e911fc32a74d --- /dev/null +++ b/test/e2e/app-document-import-order/app-document-import-order.test.ts @@ -0,0 +1,42 @@ +/* eslint-disable jest/no-standalone-expect */ +import { nextTestSetup } from 'e2e-utils' + +describe('Root components import order', () => { + const { next, isTurbopack } = nextTestSetup({ + files: __dirname, + }) + + it('root components should be imported in order _document > _app > page to respect side effects', async () => { + const $ = await next.render$('/') + + const expectSideEffectsOrder = ['_document', '_app', 'page'] + const sideEffectCalls = $('.side-effect-calls') + + Array.from(sideEffectCalls).forEach((sideEffectCall, index) => { + expect($(sideEffectCall).text()).toEqual(expectSideEffectsOrder[index]) + }) + }) + + // Test relies on webpack splitChunks overrides. + ;(isTurbopack ? it.skip : it)( + '_app chunks should be attached to the dom before page chunks', + async () => { + const $ = await next.render$('/') + + const requiredByRegex = /^\/_next\/static\/chunks\/(requiredBy\w*).*\.js/ + const chunks = Array.from($('head').contents()) + .filter( + (child: any) => + child.type === 'script' && + child.name === 'script' && + child.attribs.src.match(requiredByRegex) + ) + .map((child: any) => child.attribs.src.match(requiredByRegex)[1]) + + const requiredByAppIndex = chunks.indexOf('requiredByApp') + const requiredByPageIndex = chunks.indexOf('requiredByPage') + + expect(requiredByAppIndex).toBeLessThan(requiredByPageIndex) + } + ) +}) diff --git a/test/integration/app-document-import-order/next.config.js b/test/e2e/app-document-import-order/next.config.js similarity index 100% rename from test/integration/app-document-import-order/next.config.js rename to test/e2e/app-document-import-order/next.config.js diff --git a/test/integration/app-document-import-order/pages/_app.js b/test/e2e/app-document-import-order/pages/_app.js similarity index 100% rename from test/integration/app-document-import-order/pages/_app.js rename to test/e2e/app-document-import-order/pages/_app.js diff --git a/test/integration/app-document-import-order/pages/_document.js b/test/e2e/app-document-import-order/pages/_document.js similarity index 100% rename from test/integration/app-document-import-order/pages/_document.js rename to test/e2e/app-document-import-order/pages/_document.js diff --git a/test/integration/app-document-import-order/pages/index.js b/test/e2e/app-document-import-order/pages/index.js similarity index 100% rename from test/integration/app-document-import-order/pages/index.js rename to test/e2e/app-document-import-order/pages/index.js diff --git a/test/integration/app-document-import-order/requiredByApp.js b/test/e2e/app-document-import-order/requiredByApp.js similarity index 100% rename from test/integration/app-document-import-order/requiredByApp.js rename to test/e2e/app-document-import-order/requiredByApp.js diff --git a/test/integration/app-document-import-order/requiredByPage.js b/test/e2e/app-document-import-order/requiredByPage.js similarity index 100% rename from test/integration/app-document-import-order/requiredByPage.js rename to test/e2e/app-document-import-order/requiredByPage.js diff --git a/test/integration/app-document-import-order/sideEffectModule.js b/test/e2e/app-document-import-order/sideEffectModule.js similarity index 100% rename from test/integration/app-document-import-order/sideEffectModule.js rename to test/e2e/app-document-import-order/sideEffectModule.js diff --git a/test/e2e/app-tree/app-tree.test.ts b/test/e2e/app-tree/app-tree.test.ts new file mode 100644 index 000000000000..1bb48149230f --- /dev/null +++ b/test/e2e/app-tree/app-tree.test.ts @@ -0,0 +1,39 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('AppTree', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should provide router context in AppTree on SSR', async () => { + let html = await next.render('/') + expect(html).toMatch(/page:.*?\//) + + html = await next.render('/another') + expect(html).toMatch(/page:.*?\/another/) + }) + + it('should provide router context in AppTree on CSR', async () => { + const browser = await next.browser('/') + let html = await browser.eval(`document.documentElement.innerHTML`) + expect(html).toMatch(/page:.*?\//) + + await browser.elementByCss('#another').click() + await retry(async () => { + html = await browser.eval(`document.documentElement.innerHTML`) + expect(html).toMatch(/page:.*?\//) + }) + + await browser.elementByCss('#home').click() + await retry(async () => { + html = await browser.eval(`document.documentElement.innerHTML`) + expect(html).toMatch(/page:.*?\/another/) + }) + }) + + it('should pass AppTree to NextPageContext', async () => { + const html = await next.render('/hello') + expect(html).toMatch(/saved:.*?Hello world/) + }) +}) diff --git a/test/integration/app-tree/pages/_app.tsx b/test/e2e/app-tree/pages/_app.tsx similarity index 100% rename from test/integration/app-tree/pages/_app.tsx rename to test/e2e/app-tree/pages/_app.tsx diff --git a/test/integration/app-tree/pages/another.js b/test/e2e/app-tree/pages/another.js similarity index 100% rename from test/integration/app-tree/pages/another.js rename to test/e2e/app-tree/pages/another.js diff --git a/test/integration/app-tree/pages/hello.tsx b/test/e2e/app-tree/pages/hello.tsx similarity index 100% rename from test/integration/app-tree/pages/hello.tsx rename to test/e2e/app-tree/pages/hello.tsx diff --git a/test/integration/app-tree/pages/index.js b/test/e2e/app-tree/pages/index.js similarity index 100% rename from test/integration/app-tree/pages/index.js rename to test/e2e/app-tree/pages/index.js diff --git a/test/e2e/auto-export/auto-export.test.ts b/test/e2e/auto-export/auto-export.test.ts new file mode 100644 index 000000000000..13bf3db65452 --- /dev/null +++ b/test/e2e/auto-export/auto-export.test.ts @@ -0,0 +1,56 @@ +import { nextTestSetup, isNextDev } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('Auto Export', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('Supports commonjs 1', async () => { + const browser = await next.browser('/commonjs1') + const html = await browser.eval('document.body.innerHTML') + expect(html).toMatch(/test1/) + expect(html).toMatch(/nextExport/) + }) + + it('Supports commonjs 2', async () => { + const browser = await next.browser('/commonjs2') + const html = await browser.eval('document.body.innerHTML') + expect(html).toMatch(/test2/) + expect(html).toMatch(/nextExport/) + }) + + it('Refreshes query on mount', async () => { + const browser = await next.browser('/post-1') + await retry(async () => { + const html = await browser.eval('document.body.innerHTML') + expect(html).toMatch(/post.*post-1/) + }) + const html = await browser.eval('document.body.innerHTML') + expect(html).toMatch(/nextExport/) + }) + + it('should update asPath after mount', async () => { + const browser = await next.browser('/zeit/cmnt-2') + await retry(async () => { + const html = await browser.eval('document.documentElement.innerHTML') + expect(html).toMatch(/\/zeit\/cmnt-2/) + }) + }) + + it('should not replace URL with page name while asPath is delayed', async () => { + const browser = await next.browser('/zeit/cmnt-1') + const val = await browser.eval(`!!window.pathnames.find(function(p) { + return p !== '/zeit/cmnt-1' + })`) + expect(val).toBe(false) + }) + + if (isNextDev) { + it('should not show hydration warning from mismatching asPath', async () => { + const browser = await next.browser('/zeit/cmnt-1') + const caughtWarns = await browser.eval('window.caughtWarns') + expect(caughtWarns).toEqual([]) + }) + } +}) diff --git a/test/integration/auto-export/pages/[post]/[cmnt].js b/test/e2e/auto-export/pages/[post]/[cmnt].js similarity index 100% rename from test/integration/auto-export/pages/[post]/[cmnt].js rename to test/e2e/auto-export/pages/[post]/[cmnt].js diff --git a/test/integration/auto-export/pages/[post]/index.js b/test/e2e/auto-export/pages/[post]/index.js similarity index 100% rename from test/integration/auto-export/pages/[post]/index.js rename to test/e2e/auto-export/pages/[post]/index.js diff --git a/test/integration/auto-export/pages/commonjs1.js b/test/e2e/auto-export/pages/commonjs1.js similarity index 100% rename from test/integration/auto-export/pages/commonjs1.js rename to test/e2e/auto-export/pages/commonjs1.js diff --git a/test/integration/auto-export/pages/commonjs2.js b/test/e2e/auto-export/pages/commonjs2.js similarity index 100% rename from test/integration/auto-export/pages/commonjs2.js rename to test/e2e/auto-export/pages/commonjs2.js diff --git a/test/e2e/basepath-root-catch-all/basepath-root-catch-all.test.ts b/test/e2e/basepath-root-catch-all/basepath-root-catch-all.test.ts new file mode 100644 index 000000000000..8fb182d2fedd --- /dev/null +++ b/test/e2e/basepath-root-catch-all/basepath-root-catch-all.test.ts @@ -0,0 +1,17 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('basepath root catch-all', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should use correct data URL for root catch-all', async () => { + const browser = await next.browser('/docs/hello') + await browser.elementByCss('#root-catchall-link').click() + await browser.waitForElementByCss('#url') + + const dataUrl = await browser.elementByCss('#url').text() + const { pathname } = new URL(dataUrl, await browser.url()) + expect(pathname).toBe(`/_next/data/${next.buildId}/root/catch-all.json`) + }) +}) diff --git a/test/integration/basepath-root-catch-all/next.config.js b/test/e2e/basepath-root-catch-all/next.config.js similarity index 100% rename from test/integration/basepath-root-catch-all/next.config.js rename to test/e2e/basepath-root-catch-all/next.config.js diff --git a/test/integration/basepath-root-catch-all/pages/[...parts].js b/test/e2e/basepath-root-catch-all/pages/[...parts].js similarity index 100% rename from test/integration/basepath-root-catch-all/pages/[...parts].js rename to test/e2e/basepath-root-catch-all/pages/[...parts].js diff --git a/test/integration/basepath-root-catch-all/pages/hello.js b/test/e2e/basepath-root-catch-all/pages/hello.js similarity index 100% rename from test/integration/basepath-root-catch-all/pages/hello.js rename to test/e2e/basepath-root-catch-all/pages/hello.js diff --git a/test/e2e/bigint/bigint.test.ts b/test/e2e/bigint/bigint.test.ts new file mode 100644 index 000000000000..06a5f02d3579 --- /dev/null +++ b/test/e2e/bigint/bigint.test.ts @@ -0,0 +1,19 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('bigint API route support', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should return 200', async () => { + const res = await next.fetch('/api/bigint') + expect(res.status).toEqual(200) + }) + + it('should return the BigInt result text', async () => { + const res = await next.fetch('/api/bigint') + expect(res.ok).toBe(true) + const text = await res.text() + expect(text).toEqual('3') + }) +}) diff --git a/test/integration/bigint/pages/api/bigint.js b/test/e2e/bigint/pages/api/bigint.js similarity index 100% rename from test/integration/bigint/pages/api/bigint.js rename to test/e2e/bigint/pages/api/bigint.js diff --git a/test/e2e/catches-missing-getStaticProps/catches-missing-getStaticProps.test.ts b/test/e2e/catches-missing-getStaticProps/catches-missing-getStaticProps.test.ts new file mode 100644 index 000000000000..099afd4f89aa --- /dev/null +++ b/test/e2e/catches-missing-getStaticProps/catches-missing-getStaticProps.test.ts @@ -0,0 +1,27 @@ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +;((isNextDev && process.env.TURBOPACK_BUILD) || + (isNextStart && process.env.TURBOPACK_DEV) + ? describe.skip + : describe)('Catches Missing getStaticProps', () => { + const errorRegex = /getStaticPaths was added without a getStaticProps in/ + + const { next } = nextTestSetup({ + files: __dirname, + skipStart: isNextStart, + skipDeployment: true, + }) + + if (isNextDev) { + it('should catch it in development mode', async () => { + const html = await next.render('/hello') + expect(html).toMatch(errorRegex) + }) + } + + if (isNextStart) { + it('should catch it in server build mode', async () => { + const { cliOutput } = await next.build() + expect(cliOutput).toMatch(errorRegex) + }) + } +}) diff --git a/test/integration/catches-missing-getStaticProps/pages/[slug].js b/test/e2e/catches-missing-getStaticProps/pages/[slug].js similarity index 100% rename from test/integration/catches-missing-getStaticProps/pages/[slug].js rename to test/e2e/catches-missing-getStaticProps/pages/[slug].js diff --git a/test/integration/cli/basic/file with spaces to --require.js b/test/e2e/cli/basic/file with spaces to --require.js similarity index 100% rename from test/integration/cli/basic/file with spaces to --require.js rename to test/e2e/cli/basic/file with spaces to --require.js diff --git a/test/integration/cli/basic/file with spaces to-require-with-node-require-option.js b/test/e2e/cli/basic/file with spaces to-require-with-node-require-option.js similarity index 100% rename from test/integration/cli/basic/file with spaces to-require-with-node-require-option.js rename to test/e2e/cli/basic/file with spaces to-require-with-node-require-option.js diff --git a/test/integration/cli/basic/pages/index.js b/test/e2e/cli/basic/pages/index.js similarity index 100% rename from test/integration/cli/basic/pages/index.js rename to test/e2e/cli/basic/pages/index.js diff --git a/test/integration/cli/certificates/localhost-key.pem b/test/e2e/cli/certificates/localhost-key.pem similarity index 100% rename from test/integration/cli/certificates/localhost-key.pem rename to test/e2e/cli/certificates/localhost-key.pem diff --git a/test/integration/cli/certificates/localhost.pem b/test/e2e/cli/certificates/localhost.pem similarity index 100% rename from test/integration/cli/certificates/localhost.pem rename to test/e2e/cli/certificates/localhost.pem diff --git a/test/e2e/cli/cli.test.ts b/test/e2e/cli/cli.test.ts new file mode 100644 index 000000000000..d5ea1d1d10f0 --- /dev/null +++ b/test/e2e/cli/cli.test.ts @@ -0,0 +1,1249 @@ +import { nextTestSetup } from 'e2e-utils' +import { findPort, killApp, retry } from 'next-test-utils' +import path, { join } from 'path' +// @ts-expect-error +import pkg from 'next/package' +import http from 'http' +import stripAnsi from 'strip-ansi' +import type { ChildProcess } from 'child_process' + +const itCI = process.env.NEXT_TEST_CI ? it : it.skip + +const reactDependencies = { + react: '19.3.0-canary-fef12a01-20260413', + 'react-dom': '19.3.0-canary-fef12a01-20260413', +} + +describe('CLI Usage', () => { + const { next, isNextStart } = nextTestSetup({ + files: join(__dirname, 'basic'), + skipStart: true, + dependencies: reactDependencies, + skipDeployment: true, + }) + + /** + * Start a long-running Next.js CLI command (e.g. `next dev`, `next start`) + * and return a handle for externally killing it. + * + * Waits for `readyPattern` to match in stdout before resolving so callers can + * make assertions against a running server. If the process exits before the + * pattern matches, the returned promise resolves anyway and the caller can + * inspect the captured output. + */ + async function launchDevServer( + args: string[], + opts: { + env?: Record<string, string> + cwd?: string + onStdout?: (msg: string) => void + onStderr?: (msg: string) => void + /** Pattern signaling the server is ready. */ + readyPattern?: RegExp + } = {} + ): Promise<{ child: ChildProcess; exit: Promise<any> }> { + const readyPattern = opts.readyPattern ?? /- Local:|✓ Ready/i + let child!: ChildProcess + let ready = false + let resolveReady!: () => void + const readyPromise = new Promise<void>((r) => { + resolveReady = () => { + if (!ready) { + ready = true + r() + } + } + }) + + const exit = next + .runCommand(args, { + env: opts.env, + cwd: opts.cwd, + onStdout: (msg) => { + opts.onStdout?.(msg) + if (readyPattern.test(stripAnsi(msg))) resolveReady() + }, + onStderr: (msg) => { + opts.onStderr?.(msg) + if (readyPattern.test(stripAnsi(msg))) resolveReady() + }, + instance: (p) => { + child = p + }, + }) + .finally(() => { + resolveReady() + }) + + await readyPromise + return { child, exit } + } + + const runAndCaptureOutput = async ({ port }: { port: number }) => { + let stdout = '' + let stderr = '' + + let app = http.createServer((_, res) => { + res.writeHead(200, { 'Content-Type': 'text/plain' }) + res.end('OK') + }) + + await new Promise<void>((resolve, reject) => { + app.on('error', reject) + app.on('listening', () => resolve()) + app.listen(port) + }) + + const { child, exit } = await launchDevServer( + ['dev', next.testDir, '-p', String(port)], + { + onStdout(msg) { + stdout += msg + }, + onStderr(msg) { + stderr += msg + }, + } + ) + + try { + // Give the server a moment to emit the port-in-use error if it hasn't + // already been emitted before the ready pattern matched. + await retry( + () => { + if (!stderr.includes('already in use') && child.exitCode === null) { + throw new Error('waiting for port conflict error') + } + }, + 5000, + 100 + ).catch(() => {}) + } finally { + await killApp(child).catch(() => {}) + await exit.catch(() => {}) + } + + await new Promise((resolve) => app.close(resolve)) + + return { stdout, stderr } + } + + const testExitSignal = async ( + killSignal: NodeJS.Signals | '' = '', + args: string[] = [], + readyRegex = /Creating an optimized production/, + expectedCode = 0 + ) => { + let instance: ChildProcess | undefined + let output = '' + + const cmdPromise = next + .runCommand(args, { + ignoreFail: true, + instance: (p) => { + instance = p + }, + onStdout: (msg) => { + output += stripAnsi(msg) + }, + }) + .catch((err) => expect.fail(err.message)) + + await retry(() => { + expect(output).toMatch(readyRegex) + }) + instance!.kill(killSignal as NodeJS.Signals) + + const { code, signal } = await cmdPromise + // Node can only partially emulate signals on Windows. Our signal handlers won't affect the exit code. + // See: https://nodejs.org/api/process.html#process_signal_events + const expectedExitSignal = process.platform === `win32` ? killSignal : null + expect(signal).toBe(expectedExitSignal) + expect(code).toBe(expectedCode) + } + + ;(isNextStart ? describe : describe.skip)('production mode', () => { + describe('start', () => { + test('should exit when SIGINT is signalled', async () => { + require('console').log('before build') + await next.deleteFile('.next') + await next.build() + require('console').log('build finished') + + const port = await findPort() + await testExitSignal( + 'SIGINT', + ['start', next.testDir, '-p', String(port)], + /- Local:/, + 130 // 128 + 2 (SIGINT) + ) + }) + test('should exit when SIGTERM is signalled', async () => { + await next.deleteFile('.next') + await next.build() + const port = await findPort() + await testExitSignal( + 'SIGTERM', + ['start', next.testDir, '-p', String(port)], + /- Local:/, + 143 // 128 + 15 (SIGTERM) + ) + }) + + test('--help', async () => { + const help = await next.runCommand(['start', '--help']) + expect(help.stdout).toMatch(/Starts Next.js in production mode/) + }) + + test('-h', async () => { + const help = await next.runCommand(['start', '-h']) + expect(help.stdout).toMatch(/Starts Next.js in production mode/) + }) + + test('should format IPv6 addresses correctly', async () => { + await next.build() + const port = await findPort() + + let stdout = '' + const { child, exit } = await launchDevServer( + ['start', next.testDir, '--hostname', '::', '--port', String(port)], + { + onStdout(msg) { + stdout += msg + }, + } + ) + + try { + await retry(() => { + // Only display when hostname is provided + expect(stdout).toMatch( + new RegExp(`Network:\\s*http://\\[::\\]:${port}`) + ) + expect(stdout).toMatch(new RegExp(`http://\\[::1\\]:${port}`)) + }) + } finally { + await killApp(child) + await exit.catch(() => {}) + } + }) + + test('should warn when unknown argument provided', async () => { + const { stderr } = await next.runCommand(['start', '--random']) + expect(stderr).toEqual(`error: unknown option '--random'\n`) + }) + test('should not throw UnhandledPromiseRejectionWarning', async () => { + const { stderr } = await next.runCommand(['start', '--random']) + expect(stderr).not.toContain('UnhandledPromiseRejectionWarning') + }) + + test('invalid directory', async () => { + const output = await next.runCommand(['start', 'non-existent']) + expect(output.stderr).toContain( + 'Invalid project directory provided, no such directory' + ) + }) + + test('--keepAliveTimeout string arg', async () => { + const { stderr } = await next.runCommand([ + 'start', + '--keepAliveTimeout', + 'string', + ]) + expect(stderr).toContain( + `error: option '--keepAliveTimeout <keepAliveTimeout>' argument 'string' is invalid. 'string' is not a non-negative number.` + ) + }) + + test('--keepAliveTimeout negative number', async () => { + const { stderr } = await next.runCommand([ + 'start', + '--keepAliveTimeout=-100', + ]) + expect(stderr).toContain( + `error: option '--keepAliveTimeout <keepAliveTimeout>' argument '-100' is invalid. '-100' is not a non-negative number.` + ) + }) + + test('--keepAliveTimeout Infinity', async () => { + const { stderr } = await next.runCommand([ + 'start', + '--keepAliveTimeout', + 'Infinity', + ]) + expect(stderr).toContain( + `error: option '--keepAliveTimeout <keepAliveTimeout>' argument 'Infinity' is invalid. 'Infinity' is not a non-negative number.` + ) + }) + + test('--keepAliveTimeout happy path', async () => { + await next.build() + const port = await findPort() + + let stderr = '' + const { child, exit } = await launchDevServer( + [ + 'start', + next.testDir, + '--keepAliveTimeout', + '100', + '--port', + String(port), + ], + { + onStderr(msg) { + stderr += msg + }, + } + ) + + try { + expect(stderr).not.toContain( + `error: option '--keepAliveTimeout <keepAliveTimeout>' argument '100' is invalid. '100' is not a non-negative number.` + ) + } finally { + await killApp(child) + await exit.catch(() => {}) + } + }) + + test('should not start on a port out of range', async () => { + const invalidPort = '300001' + const { stderr } = await next.runCommand([ + 'start', + '--port', + invalidPort, + ]) + + expect(stderr).toContain(`options.port should be >= 0 and < 65536.`) + }) + + test('should not start on a reserved port', async () => { + const reservedPort = '4045' + const { stderr } = await next.runCommand([ + 'start', + '--port', + reservedPort, + ]) + + expect(stderr).toContain( + `Bad port: "${reservedPort}" is reserved for npp` + ) + }) + + test('--inspect', async () => { + await next.build() + const port = await findPort() + + let output = '' + let errOutput = '' + const { child, exit } = await launchDevServer( + ['start', next.testDir, '--port', String(port), '--inspect'], + { + onStdout(msg) { + output += stripAnsi(msg) + }, + onStderr(msg) { + errOutput += stripAnsi(msg) + }, + } + ) + + try { + await retry(() => { + expect(output).toMatch(new RegExp(`http://localhost:${port}`)) + }) + await retry(() => { + expect(output).toMatch(/- Debugger port:\s+9229/) + }) + await retry(() => { + expect(errOutput).toMatch(/Debugger listening on/) + }) + expect(errOutput).not.toContain('address already in use') + } finally { + await killApp(child) + await exit.catch(() => {}) + } + }) + }) + + describe('telemetry', () => { + test('--help', async () => { + const help = await next.runCommand(['telemetry', '--help']) + expect(help.stdout).toMatch(/Allows you to enable or disable Next\.js'/) + }) + + test('-h', async () => { + const help = await next.runCommand(['telemetry', '-h']) + expect(help.stdout).toMatch(/Allows you to enable or disable Next\.js'/) + }) + + test('should warn when unknown argument provided', async () => { + const { stderr } = await next.runCommand(['telemetry', '--random']) + expect(stderr).toEqual(`error: unknown option '--random'\n`) + }) + test('should not throw UnhandledPromiseRejectionWarning', async () => { + const { stderr } = await next.runCommand(['telemetry', '--random']) + expect(stderr).not.toContain('UnhandledPromiseRejectionWarning') + }) + }) + + describe('build', () => { + test('--help', async () => { + const help = await next.runCommand(['build', '--help']) + expect(help.stdout).toMatch(/Creates an optimized production build/) + }) + + test('-h', async () => { + const help = await next.runCommand(['build', '-h']) + expect(help.stdout).toMatch(/Creates an optimized production build/) + }) + + test('should warn when unknown argument provided', async () => { + const { stderr } = await next.runCommand(['build', '--random']) + expect(stderr).toEqual(`error: unknown option '--random'\n`) + }) + test('should not throw UnhandledPromiseRejectionWarning', async () => { + const { stderr } = await next.runCommand(['build', '--random']) + expect(stderr).not.toContain('UnhandledPromiseRejectionWarning') + }) + + test('should exit when SIGINT is signalled', async () => { + await testExitSignal('SIGINT', ['build', next.testDir], undefined, 130) + }) + + test('should exit when SIGTERM is signalled', async () => { + await testExitSignal('SIGTERM', ['build', next.testDir], undefined, 143) + }) + + test('invalid directory', async () => { + const output = await next.runCommand(['build', 'non-existent']) + expect(output.stderr).toContain( + 'Invalid project directory provided, no such directory' + ) + }) + }) + }) + + describe('no command', () => { + test('--help', async () => { + const help = await next.runCommand(['--help']) + expect(help.stdout).toMatch( + /The Next.js CLI allows you to develop, build, start/ + ) + }) + + test('-h', async () => { + const help = await next.runCommand(['-h']) + expect(help.stdout).toMatch( + /The Next.js CLI allows you to develop, build, start/ + ) + }) + + test('--version', async () => { + const output = await next.runCommand(['--version']) + expect(output.stdout).toMatch( + new RegExp(`Next\\.js v${pkg.version.replace(/\./g, '\\.')}`) + ) + }) + + test('-v', async () => { + const output = await next.runCommand(['--version']) + expect(output.stdout).toMatch( + new RegExp(`Next\\.js v${pkg.version.replace(/\./g, '\\.')}`) + ) + }) + + test('invalid directory', async () => { + const output = await next.runCommand(['non-existent']) + expect(output.stderr).toContain( + 'Invalid project directory provided, no such directory' + ) + }) + + test('detects command typos', async () => { + const typos = [ + ['buidl', 'build'], + ['buill', 'build'], + ['biild', 'build'], + ['starr', 'start'], + ['dee', 'dev'], + ] + + for (const typo of typos) { + const output = await next.runCommand([typo[0]]) + expect(output.stderr).toContain( + `"next ${typo[0]}" does not exist. Did you mean "next ${typo[1]}"?` + ) + } + }) + }) + + describe('dev', () => { + test('--help', async () => { + const help = await next.runCommand(['dev', '--help']) + expect(help.stdout).toMatch(/Starts Next.js in development mode/) + }) + + test('-h', async () => { + const help = await next.runCommand(['dev', '-h']) + expect(help.stdout).toMatch(/Starts Next.js in development mode/) + }) + + test('custom directory', async () => { + const port = await findPort() + let output = '' + const { child, exit } = await launchDevServer( + ['dev', next.testDir, '--port', String(port)], + { + onStdout(msg) { + output += stripAnsi(msg) + }, + } + ) + try { + await retry(() => { + expect(output).toMatch(/- Local:/i) + }) + } finally { + await killApp(child) + await exit.catch(() => {}) + } + }) + + test('--port', async () => { + const port = await findPort() + let output = '' + const { child, exit } = await launchDevServer( + ['dev', next.testDir, '--port', String(port)], + { + onStdout(msg) { + output += stripAnsi(msg) + }, + } + ) + try { + await retry(() => { + expect(output).toMatch(new RegExp(`http://localhost:${port}`)) + }) + await retry(() => { + expect(output).toMatch( + /Network:\s*http:\/\/[\d]{1,}\.[\d]{1,}\.[\d]{1,}/ + ) + }) + } finally { + await killApp(child) + await exit.catch(() => {}) + } + }) + + test('--port 0', async () => { + const port = await findPort() + let output = '' + const { child, exit } = await launchDevServer( + ['dev', next.testDir, '--port', String(port)], + { + onStdout(msg) { + output += stripAnsi(msg) + }, + } + ) + try { + await retry(() => { + expect(output).toMatch(new RegExp(`http://localhost:${port}`)) + }) + await retry(() => { + expect(output).toMatch( + /Network:\s*http:\/\/[\d]{1,}\.[\d]{1,}\.[\d]{1,}/ + ) + }) + } finally { + await killApp(child) + await exit.catch(() => {}) + } + const matches = /- Local/.exec(output) + expect(matches).not.toBe(null) + + const _port = parseInt(matches![1]) + // Regression test: port 0 was interpreted as if no port had been + // provided, falling back to 3000. + expect(_port).not.toBe(3000) + }) + + test('PORT=0', async () => { + let output = '' + const { child, exit } = await launchDevServer(['dev', next.testDir], { + env: { + PORT: '0', + }, + onStdout(msg) { + output += stripAnsi(msg) + }, + }) + try { + await retry(() => { + expect(output).toMatch(/- Local:/) + }) + // without --hostname, do not log Network: xxx + const matches = /Network:\s*http:\/\/\[::\]:(\d+)/.exec(output) + const _port = parseInt('' + matches) + expect(matches).toBe(null) + // Regression test: port 0 was interpreted as if no port had been + // provided, falling back to 3000. + expect(_port).not.toBe(3000) + } finally { + await killApp(child) + await exit.catch(() => {}) + } + }) + + test("NODE_OPTIONS='--inspect'", async () => { + const port = await findPort() + let output = '' + let errOutput = '' + const { child, exit } = await launchDevServer( + ['dev', next.testDir, '--port', String(port)], + { + onStdout(msg) { + output += stripAnsi(msg) + }, + onStderr(msg) { + errOutput += stripAnsi(msg) + }, + env: { NODE_OPTIONS: '--inspect' }, + } + ) + try { + await retry(() => { + expect(output).toMatch(new RegExp(`http://localhost:${port}`)) + }) + await retry(() => { + expect(output).toMatch(/- Debugger port:\s+\d+/) + }) + await retry(() => { + expect(errOutput).toMatch(/Debugger listening on/) + }) + expect(errOutput).not.toContain('address already in use') + } finally { + await killApp(child) + await exit.catch(() => {}) + } + }) + + test('--inspect', async () => { + const port = await findPort() + let output = '' + let errOutput = '' + const { child, exit } = await launchDevServer( + ['dev', next.testDir, '--port', String(port), '--inspect'], + { + onStdout(msg) { + output += stripAnsi(msg) + }, + onStderr(msg) { + errOutput += stripAnsi(msg) + }, + } + ) + try { + await retry(() => { + expect(output).toMatch(new RegExp(`http://localhost:${port}`)) + }) + await retry(() => { + expect(output).toMatch(/- Debugger port:\s+9229/) + }) + await retry(() => { + expect(errOutput).toMatch(/Debugger listening on/) + }) + expect(errOutput).not.toContain('address already in use') + } finally { + await killApp(child) + await exit.catch(() => {}) + } + }) + + test('--inspect 0', async () => { + const port = await findPort() + let output = '' + let errOutput = '' + const { child, exit } = await launchDevServer( + ['dev', next.testDir, '--port', String(port), '--inspect', '0'], + { + onStdout(msg) { + output += stripAnsi(msg) + }, + onStderr(msg) { + errOutput += stripAnsi(msg) + }, + } + ) + try { + await retry(() => { + expect(output).toMatch(new RegExp(`http://localhost:${port}`)) + }) + await retry(() => { + expect(output).toMatch(/- Debugger port:\s+\d+/) + }) + await retry(() => { + expect(errOutput).toMatch(/Debugger listening on/) + }) + expect(errOutput).not.toContain('address already in use') + } finally { + await killApp(child) + await exit.catch(() => {}) + } + }) + + test('--inspect [port]', async () => { + const port = await findPort() + let output = '' + let errOutput = '' + const { child, exit } = await launchDevServer( + ['dev', next.testDir, '--port', String(port), '--inspect', '9230'], + { + onStdout(msg) { + output += stripAnsi(msg) + }, + onStderr(msg) { + errOutput += stripAnsi(msg) + }, + } + ) + try { + await retry(() => { + expect(output).toMatch(new RegExp(`http://localhost:${port}`)) + }) + await retry(() => { + expect(output).toMatch(/- Debugger port:\s+9230/) + }) + await retry(() => { + expect(errOutput).toMatch(/Debugger listening on/) + }) + expect(errOutput).not.toContain('address already in use') + } finally { + await killApp(child) + await exit.catch(() => {}) + } + }) + + test("NODE_OPTIONS='--inspect=:0'", async () => { + const port = await findPort() + let output = '' + let errOutput = '' + const { child, exit } = await launchDevServer( + ['dev', next.testDir, '--port', String(port)], + { + onStdout(msg) { + output += stripAnsi(msg) + }, + onStderr(msg) { + errOutput += stripAnsi(msg) + }, + env: { NODE_OPTIONS: '--inspect=:0' }, + } + ) + try { + await retry(() => { + expect(output).toMatch(new RegExp(`http://localhost:${port}`)) + }) + await retry(() => { + expect(output).toMatch(/- Debugger port:\s+\d+/) + }) + await retry(() => { + expect(errOutput).toMatch(/Debugger listening on/) + }) + expect(errOutput).not.toContain('address already in use') + expect(errOutput).toContain('Debugger listening on') + console.log(output) + } finally { + await killApp(child) + await exit.catch(() => {}) + } + }) + + test("NODE_OPTIONS='--require=file with spaces to-require-with-node-require-option.js'", async () => { + const port = await findPort() + let output = '' + let errOutput = '' + const { child, exit } = await launchDevServer( + ['dev', next.testDir, '--port', String(port)], + { + cwd: next.testDir, + onStdout(msg) { + output += stripAnsi(msg) + }, + onStderr(msg) { + errOutput += stripAnsi(msg) + }, + env: { + NODE_OPTIONS: + '--require "./file with spaces to-require-with-node-require-option.js"', + }, + } + ) + try { + await retry(() => { + expect(output).toMatch(new RegExp(`http://localhost:${port}`)) + }) + expect(output).toContain( + 'FILE_WITH_SPACES_TO_REQUIRE_WITH_NODE_REQUIRE_OPTION' + ) + expect(errOutput).toBe('') + } finally { + await killApp(child) + await exit.catch(() => {}) + } + }) + + test("NODE_OPTIONS='--require=file with spaces to --require.js'", async () => { + const port = await findPort() + let output = '' + let errOutput = '' + const { child, exit } = await launchDevServer( + ['dev', next.testDir, '--port', String(port)], + { + cwd: next.testDir, + onStdout(msg) { + output += stripAnsi(msg) + }, + onStderr(msg) { + errOutput += stripAnsi(msg) + }, + env: { + NODE_OPTIONS: '--require "./file with spaces to --require.js"', + }, + } + ) + try { + await retry(() => { + expect(output).toMatch(new RegExp(`http://localhost:${port}`)) + }) + expect(output).toContain( + 'FILE_WITH_SPACES_TO_REQUIRE_WITH_NODE_REQUIRE_OPTION' + ) + expect(errOutput).toBe('') + } finally { + await killApp(child) + await exit.catch(() => {}) + } + }) + + test('-p', async () => { + const port = await findPort() + let output = '' + const { child, exit } = await launchDevServer( + ['dev', next.testDir, '-p', String(port)], + { + onStdout(msg) { + output += stripAnsi(msg) + }, + env: { NODE_OPTIONS: '--inspect' }, + } + ) + try { + await retry(() => { + expect(output).toMatch(new RegExp(`http://localhost:${port}`)) + }) + } finally { + await killApp(child) + await exit.catch(() => {}) + } + }) + + test('-p conflict', async () => { + const port = await findPort() + const { stderr, stdout } = await runAndCaptureOutput({ port }) + + expect(stderr).toMatch('already in use') + expect(stdout).not.toMatch(/ready/i) + expect(stdout).not.toMatch('started') + expect(stdout).not.toMatch(`${port}`) + expect(stripAnsi(stdout).trim()).toBeFalsy() + }) + + test('Allow retry if default port is already in use', async () => { + let output = '' + let one: { child: ChildProcess; exit: Promise<any> } | undefined + let two: { child: ChildProcess; exit: Promise<any> } | undefined + + try { + one = await launchDevServer(['dev', next.testDir], {}) + two = await launchDevServer(['dev', next.testDir], { + onStderr(msg) { + output += stripAnsi(msg) + }, + }) + } finally { + if (one) { + await killApp(one.child).catch(console.error) + await one.exit.catch(() => {}) + } + if (two) { + await killApp(two.child).catch(console.error) + await two.exit.catch(() => {}) + } + } + + // Depending on the environment Next may or may not be able to resolve + // the process holding the port; accept both wordings. + expect(output).toMatch( + /⚠ Port 3000 is in use by (an unknown process|process \d+), using available port \d+ instead\./ + ) + }) + + test('-p reserved', async () => { + const TCP_MUX_PORT = 1 + // Don't pre-bind a server to port 1 here: Next.js rejects reserved + // ports during CLI argument parsing, before attempting to bind, so the + // pre-bind step is unnecessary and would also fail on CI where binding + // privileged ports (< 1024) requires root. + let stdout = '' + let stderr = '' + await next.runCommand(['dev', next.testDir, '-p', String(TCP_MUX_PORT)], { + ignoreFail: true, + onStdout: (msg) => { + stdout += msg + }, + onStderr: (msg) => { + stderr += msg + }, + }) + + expect(stdout).toMatch('') + expect(stderr).toMatch( + `Bad port: "${TCP_MUX_PORT}" is reserved for tcpmux` + ) + }) + + test('--hostname', async () => { + const port = await findPort() + let output = '' + const { child, exit } = await launchDevServer( + ['dev', next.testDir, '--hostname', '0.0.0.0', '--port', String(port)], + { + onStdout(msg) { + output += stripAnsi(msg) + }, + } + ) + try { + await retry(() => { + expect(output).toMatch( + new RegExp(`Network:\\s*http://0.0.0.0:${port}`) + ) + }) + await retry(() => { + expect(output).toMatch(new RegExp(`http://localhost:${port}`)) + }) + } finally { + await killApp(child) + await exit.catch(() => {}) + } + }) + + test('-H', async () => { + const port = await findPort() + let output = '' + const { child, exit } = await launchDevServer( + ['dev', next.testDir, '-H', '0.0.0.0', '--port', String(port)], + { + onStdout(msg) { + output += stripAnsi(msg) + }, + } + ) + try { + await retry(() => { + expect(output).toMatch( + new RegExp(`Network:\\s*http://0.0.0.0:${port}`) + ) + }) + await retry(() => { + expect(output).toMatch(new RegExp(`http://localhost:${port}`)) + }) + } finally { + await killApp(child) + await exit.catch(() => {}) + } + }) + + itCI('--experimental-https', async () => { + // only runs on CI as it requires administrator privileges + const port = await findPort() + let output = '' + const { child, exit } = await launchDevServer( + ['dev', next.testDir, '--experimental-https', '--port', String(port)], + { + onStdout(msg) { + output += stripAnsi(msg) + }, + } + ) + try { + await retry(() => { + expect(output).toMatch(/Network:\s*https:\/\//) + }) + expect(output).toMatch(/Local:\s*https:\/\/localhost:(\d+)/) + expect(output).toContain('Certificates created in') + } finally { + await killApp(child) + await exit.catch(() => {}) + } + }) + + test('--experimental-https with provided key/cert', async () => { + const keyFile = path.resolve(__dirname, 'certificates/localhost-key.pem') + const certFile = path.resolve(__dirname, 'certificates/localhost.pem') + const port = await findPort() + let output = '' + const { child, exit } = await launchDevServer( + [ + 'dev', + next.testDir, + '--experimental-https', + '--experimental-https-key', + keyFile, + '--experimental-https-cert', + certFile, + '--port', + String(port), + ], + { + onStdout(msg) { + output += stripAnsi(msg) + }, + } + ) + try { + await retry(() => { + expect(output).toMatch(/https:\/\/localhost:(\d+)/) + }) + } finally { + await killApp(child) + await exit.catch(() => {}) + } + }) + + test('should format IPv6 addresses correctly', async () => { + const port = await findPort() + let output = '' + const { child, exit } = await launchDevServer( + ['dev', next.testDir, '--hostname', '::', '--port', String(port)], + { + onStdout(msg) { + output += stripAnsi(msg) + }, + } + ) + try { + // Only display when hostname is provided + await retry(() => { + expect(output).toMatch( + new RegExp(`Network:\\s*\\http://\\[::\\]:${port}`) + ) + }) + await retry(() => { + expect(output).toMatch(new RegExp(`http://\\[::1\\]:${port}`)) + }) + } finally { + await killApp(child).catch(() => {}) + await exit.catch(() => {}) + } + }) + + test('should warn when unknown argument provided', async () => { + const { stderr } = await next.runCommand(['dev', '--random']) + expect(stderr).toEqual(`error: unknown option '--random'\n`) + }) + test('should not throw UnhandledPromiseRejectionWarning', async () => { + const { stderr } = await next.runCommand(['dev', '--random']) + expect(stderr).not.toContain('UnhandledPromiseRejectionWarning') + }) + + test('should exit when SIGINT is signalled', async () => { + const port = await findPort() + await testExitSignal( + 'SIGINT', + ['dev', next.testDir, '-p', String(port)], + /- Local:/ + ) + }) + test('should exit when SIGTERM is signalled', async () => { + const port = await findPort() + await testExitSignal( + 'SIGTERM', + ['dev', next.testDir, '-p', String(port)], + /- Local:/ + ) + }) + + test('invalid directory', async () => { + const output = await next.runCommand(['dev', 'non-existent']) + expect(output.stderr).toContain( + 'Invalid project directory provided, no such directory' + ) + }) + }) + + describe('export', () => { + test('--help', async () => { + const help = await next.runCommand(['export', '--help']) + expect(help.stderr).toMatch( + `error: unknown option '--help'\n(Did you mean --help?)` + ) + expect(help.code).toBe(1) + }) + + test('run export command', async () => { + const help = await next.runCommand(['export']) + expect(help.stderr).toMatch( + `\`next export\` has been removed in favor of 'output: export' in next.config.js` + ) + expect(help.code).toBe(1) + }) + }) + + describe('info', () => { + function matchInfoOutput(stdout, { nextConfigOutput = '.*' } = {}) { + expect(stdout).toMatch( + new RegExp(` +Operating System: + Platform: .* + Arch: .* + Version: .* + Available memory \\(MB\\): .* + Available CPU cores: .* +Binaries: + Node: .* + npm: .* + Yarn: .* + pnpm: .* +Relevant Packages: + next: .* + eslint-config-next: .* + react: .* + react-dom: .* + typescript: .*${process.env.NEXT_RSPACK ? '\n next-rspack: .*' : ''} +Next.js Config: + output: ${nextConfigOutput} +`) + ) + } + + test('--help', async () => { + const help = await next.runCommand(['info', '--help']) + expect(help.stdout).toMatch( + /Prints relevant details about the current system which can be used to report/ + ) + }) + + test('-h', async () => { + const help = await next.runCommand(['info', '-h']) + expect(help.stdout).toMatch( + /Prints relevant details about the current system which can be used to report/ + ) + }) + + test('should print output', async () => { + const info = await next.runCommand(['info']) + + expect((info.stderr || '').toLowerCase()).not.toContain('error') + matchInfoOutput(info.stdout) + }) + + test('should print output with next.config.mjs', async () => { + let info = { stdout: '', stderr: '' } + const originalPkg = await next.readFile('package.json') + + try { + await next.patchFile( + 'next.config.mjs', + `export default { output: 'standalone' }` + ) + // Merge `type: 'module'` into the existing package.json so we keep + // the `packageManager` field that nextTestSetup generated. Without + // it corepack auto-fetches the latest pnpm, which is incompatible + // with the runner's Node version on CI. + await next.patchFile( + 'package.json', + JSON.stringify({ ...JSON.parse(originalPkg), type: 'module' }) + ) + info = await next.runCommand(['info'], { + cwd: next.testDir, + }) + } finally { + await next.deleteFile('next.config.mjs') + await next.patchFile('package.json', originalPkg) + } + + expect((info.stderr || '').toLowerCase()).not.toContain('error') + matchInfoOutput(info.stdout, { nextConfigOutput: 'standalone' }) + }) + }) +}) + +describe('CLI Usage: duplicate sass dependencies', () => { + const { next, isNextStart, skipped } = nextTestSetup({ + files: join(__dirname, 'duplicate-sass'), + skipStart: true, + dependencies: reactDependencies, + skipDeployment: true, + }) + if (skipped) return + + // The original integration test relied on pre-existing fake `sass` and + // `node-sass` modules in `duplicate-sass/node_modules/`. In e2e mode the + // isolated `pnpm install` moves any pre-existing `node_modules` to + // `.ignored`, so we recreate the fake modules and reference them in + // `package.json` after install, before running `next dev`. + beforeAll(async () => { + if (!isNextStart) return + const pkg = await next.readJSON('package.json') + pkg.dependencies = { + ...pkg.dependencies, + sass: '1.0.0', + 'node-sass': '1.0.0', + } + await next.patchFile('package.json', JSON.stringify(pkg, null, 2)) + for (const name of ['sass', 'node-sass']) { + await next.patchFile( + `node_modules/${name}/package.json`, + JSON.stringify({ name, version: '1.0.0' }) + ) + await next.patchFile( + `node_modules/${name}/index.js`, + 'module.exports = {}\n' + ) + } + }) + ;(isNextStart ? test : test.skip)('duplicate sass deps', async () => { + const port = await findPort() + + let output = '' + let child: ChildProcess | undefined + const exit = next + .runCommand(['dev', next.testDir, '-p', String(port)], { + onStdout(msg) { + output += msg + }, + onStderr(msg) { + output += msg + }, + instance: (p) => { + child = p + }, + }) + .catch(() => {}) + + try { + await retry(() => { + expect(output).toMatch(/both `sass` and `node-sass` installed/) + }) + } finally { + if (child) { + await killApp(child).catch(() => {}) + } + await exit + } + }) +}) diff --git a/test/integration/cli/duplicate-sass/package.json b/test/e2e/cli/duplicate-sass/package.json similarity index 100% rename from test/integration/cli/duplicate-sass/package.json rename to test/e2e/cli/duplicate-sass/package.json diff --git a/test/integration/cli/duplicate-sass/pages/index.js b/test/e2e/cli/duplicate-sass/pages/index.js similarity index 100% rename from test/integration/cli/duplicate-sass/pages/index.js rename to test/e2e/cli/duplicate-sass/pages/index.js diff --git a/test/e2e/client-404/client-404.test.ts b/test/e2e/client-404/client-404.test.ts new file mode 100644 index 000000000000..80cd5726dfd7 --- /dev/null +++ b/test/e2e/client-404/client-404.test.ts @@ -0,0 +1,87 @@ +import { nextTestSetup, isNextStart } from 'e2e-utils' +import { + retry, + getClientBuildManifestLoaderChunkUrlPath, +} from 'next-test-utils' + +describe('Client 404', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + // Assertions don't apply to deploy mode (output differs vs. local Next.js server). + skipDeployment: true, + }) + if (skipped) return + + beforeAll(async () => { + // pre-build the home page so that navigating to it from the + // error page doesn't time out while webpack compiles on demand + await next.render('/') + }) + + describe('should show 404 upon client replacestate', () => { + it('should navigate the page', async () => { + const browser = await next.browser('/asd') + const serverCode = await browser + .waitForElementByCss('#errorStatusCode') + .text() + // In webpack dev mode, compiling the home page (via the + // `next.render('/')` pre-build in beforeAll) can race with the + // `/asd` page's hydration and trigger a Fast Refresh-driven full + // reload, which in turn swallows the click on `#errorGoHome` and + // leaves the browser back on `/asd`. Retry the click until the + // home page actually becomes visible. + await retry(async () => { + if (!(await browser.hasElementByCssSelector('#hellom8'))) { + await browser.waitForElementByCss('#errorGoHome').click() + await browser.waitForElementByCss('#hellom8', 5000) + } + }) + await browser.waitForElementByCss('#hellom8').back() + const clientCode = await browser + .waitForElementByCss('#errorStatusCode') + .text() + + expect({ serverCode, clientCode }).toMatchObject({ + serverCode: '404', + clientCode: '404', + }) + await browser.close() + }) + }) + + it('should hard navigate to URL on failing to load bundle', async () => { + const browser = await next.browser('/invalid-link') + await browser.eval(() => ((window as any).beforeNav = 'hi')) + await browser.elementByCss('#to-nonexistent').click() + await retry(async () => { + expect(await browser.elementByCss('#errorStatusCode').text()).toMatch( + /404/ + ) + }) + expect(await browser.eval(() => (window as any).beforeNav)).not.toBe('hi') + }) + + if (isNextStart) { + it('should hard navigate to URL on failing to load missing bundle', async () => { + const chunk = getClientBuildManifestLoaderChunkUrlPath( + next.testDir, + '/missing' + ) + const browser = await next.browser('/to-missing-link', { + beforePageLoad(page) { + page.route(`**/${chunk}*`, (route) => { + route.abort('internetdisconnected') + }) + }, + }) + await browser.eval(() => ((window as any).beforeNav = 'hi')) + await browser.elementByCss('#to-missing').click() + + await retry(async () => { + expect(await browser.url()).toContain('/missing') + }) + expect(await browser.elementByCss('#missing').text()).toBe('poof') + expect(await browser.eval(() => (window as any).beforeNav)).not.toBe('hi') + }) + } +}) diff --git a/test/integration/jsconfig-baseurl/next.config.js b/test/e2e/client-404/next.config.js similarity index 100% rename from test/integration/jsconfig-baseurl/next.config.js rename to test/e2e/client-404/next.config.js diff --git a/test/integration/client-404/pages/_error.js b/test/e2e/client-404/pages/_error.js similarity index 100% rename from test/integration/client-404/pages/_error.js rename to test/e2e/client-404/pages/_error.js diff --git a/test/integration/client-404/pages/index.js b/test/e2e/client-404/pages/index.js similarity index 100% rename from test/integration/client-404/pages/index.js rename to test/e2e/client-404/pages/index.js diff --git a/test/integration/client-404/pages/invalid-link.js b/test/e2e/client-404/pages/invalid-link.js similarity index 100% rename from test/integration/client-404/pages/invalid-link.js rename to test/e2e/client-404/pages/invalid-link.js diff --git a/test/integration/client-404/pages/missing.js b/test/e2e/client-404/pages/missing.js similarity index 100% rename from test/integration/client-404/pages/missing.js rename to test/e2e/client-404/pages/missing.js diff --git a/test/integration/client-404/pages/to-missing-link.js b/test/e2e/client-404/pages/to-missing-link.js similarity index 100% rename from test/integration/client-404/pages/to-missing-link.js rename to test/e2e/client-404/pages/to-missing-link.js diff --git a/test/e2e/client-shallow-routing/client-shallow-routing.test.ts b/test/e2e/client-shallow-routing/client-shallow-routing.test.ts new file mode 100644 index 000000000000..5357168717a9 --- /dev/null +++ b/test/e2e/client-shallow-routing/client-shallow-routing.test.ts @@ -0,0 +1,87 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('Client Shallow Routing', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should not shallowly navigate back in history when current page was not shallow', async () => { + const browser = await next.browser('/first') + + const props = JSON.parse(await browser.elementByCss('#props').text()) + expect(props.params).toEqual({ slug: 'first' }) + + await browser.elementByCss('#add-query-shallow').click() + await retry(async () => { + const props2 = JSON.parse(await browser.elementByCss('#props').text()) + expect(props2).toEqual(props) + }) + + await browser.elementByCss('#remove-query-shallow').click() + await retry(async () => { + const props3 = JSON.parse(await browser.elementByCss('#props').text()) + expect(props3).toEqual(props) + }) + + await browser.elementByCss('#to-another').click() + await retry(async () => { + const text = await browser.elementByCss('#props').text() + expect(text).toMatch(/another/) + }) + + const props4 = JSON.parse(await browser.elementByCss('#props').text()) + expect(props4.params).toEqual({ slug: 'another' }) + expect(props4.random).not.toBe(props.random) + + await browser.back() + await retry(async () => { + const props5 = JSON.parse(await browser.elementByCss('#props').text()) + expect(props5.params).toEqual({ slug: 'first' }) + expect(props5.random).not.toBe(props4.random) + }) + }) + + it('should not shallowly navigate forwards in history when current page was not shallow', async () => { + const browser = await next.browser('/first') + + const props = JSON.parse(await browser.elementByCss('#props').text()) + expect(props.params).toEqual({ slug: 'first' }) + + await browser.elementByCss('#add-query-shallow').click() + await retry(async () => { + const props2 = JSON.parse(await browser.elementByCss('#props').text()) + expect(props2).toEqual(props) + }) + + await browser.elementByCss('#to-another').click() + await retry(async () => { + const props3text = await browser.elementByCss('#props').text() + expect(props3text).toMatch(/another/) + }) + + const props3 = JSON.parse(await browser.elementByCss('#props').text()) + expect(props3.params).toEqual({ slug: 'another' }) + expect(props3.random).not.toBe(props.random) + + await browser.back() + await retry(async () => { + const props4text = await browser.elementByCss('#props').text() + expect(props4text).toMatch(/first/) + }) + + const props4 = JSON.parse(await browser.elementByCss('#props').text()) + expect(props4.params).toEqual({ slug: 'first' }) + expect(props4.random).not.toBe(props3.random) + + await browser.forward() + await retry(async () => { + const props5text = await browser.elementByCss('#props').text() + expect(props5text).toMatch(/another/) + }) + + const props5 = JSON.parse(await browser.elementByCss('#props').text()) + expect(props5.params).toEqual({ slug: 'another' }) + expect(props5.random).not.toBe(props4.random) + }) +}) diff --git a/test/integration/client-shallow-routing/pages/[slug].js b/test/e2e/client-shallow-routing/pages/[slug].js similarity index 100% rename from test/integration/client-shallow-routing/pages/[slug].js rename to test/e2e/client-shallow-routing/pages/[slug].js diff --git a/test/e2e/config-experimental-warning/config-experimental-warning.test.ts b/test/e2e/config-experimental-warning/config-experimental-warning.test.ts new file mode 100644 index 000000000000..04a7c128b23f --- /dev/null +++ b/test/e2e/config-experimental-warning/config-experimental-warning.test.ts @@ -0,0 +1,269 @@ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +import stripAnsi from 'strip-ansi' + +const experimentalHeader = '- Experiments (use with caution):' +const pageContent = `export default () => 'hi'` + +describe('Config Experimental Warning', () => { + ;(isNextDev ? describe : describe.skip)('development mode', () => { + describe('default config from function', () => { + const { next } = nextTestSetup({ + files: { + 'pages/index.js': pageContent, + 'next.config.js': ` + module.exports = (phase, { defaultConfig }) => { + return { + ...defaultConfig, + experimental: { + ...defaultConfig.experimental, + // We enable this by default in CI + strictRouteTypes: false, + } + } + } + `, + }, + }) + + it('should not show warning with default config from function', async () => { + const output = stripAnsi(next.cliOutput) + expect(output).not.toMatch(experimentalHeader) + }) + }) + + describe('config from object', () => { + const { next } = nextTestSetup({ + files: __dirname, + nextConfig: { + images: {}, + experimental: { + // We enable this by default in CI + strictRouteTypes: false, + }, + }, + }) + + it('should not show warning with config from object', async () => { + const output = stripAnsi(next.cliOutput) + expect(output).not.toMatch(experimentalHeader) + }) + }) + + describe('config with workerThreads from object', () => { + const { next } = nextTestSetup({ + files: __dirname, + nextConfig: { + experimental: { + workerThreads: true, + }, + }, + }) + + it('should show warning with config from object with experimental', async () => { + // Make a request to trigger experimental warnings display + await next.fetch('/') + const output = stripAnsi(next.cliOutput) + expect(output).toMatch(experimentalHeader) + expect(output).toMatch(' ✓ workerThreads') + }) + }) + + describe('config with workerThreads from function', () => { + const { next } = nextTestSetup({ + files: { + 'pages/index.js': pageContent, + 'next.config.js': ` + module.exports = (phase) => ({ + experimental: { + workerThreads: true + } + }) + `, + }, + }) + + it('should show warning with config from function with experimental', async () => { + // Make a request to trigger experimental warnings display + await next.fetch('/') + const output = stripAnsi(next.cliOutput) + expect(output).toMatch(experimentalHeader) + expect(output).toMatch(' ✓ workerThreads') + }) + }) + + describe('config with default value', () => { + const { next } = nextTestSetup({ + files: { + 'pages/index.js': pageContent, + 'next.config.js': ` + module.exports = (phase) => ({ + experimental: { + workerThreads: false, + // We enable this by default in CI + strictRouteTypes: false, + } + }) + `, + }, + }) + + it('should not show warning with default value', async () => { + const output = stripAnsi(next.cliOutput) + expect(output).not.toContain(experimentalHeader) + expect(output).not.toContain('workerThreads') + }) + }) + + describe('config with prerenderEarlyExit false', () => { + const { next } = nextTestSetup({ + files: __dirname, + nextConfig: { + experimental: { + prerenderEarlyExit: false, + }, + }, + }) + + it('should show warning with a symbol indicating that a default true value is set to false', async () => { + // Make a request to trigger experimental warnings display + await next.fetch('/') + const output = stripAnsi(next.cliOutput) + expect(output).toMatch(experimentalHeader) + expect(output).toMatch(' ⨯ prerenderEarlyExit') + }) + }) + + describe('config with cpus', () => { + const { next } = nextTestSetup({ + files: __dirname, + nextConfig: { + experimental: { + cpus: 2, + }, + }, + }) + + it('should show the configured value for numerical features', async () => { + // Make a request to trigger experimental warnings display + await next.fetch('/') + const output = stripAnsi(next.cliOutput) + expect(output).toMatch(experimentalHeader) + expect(output).toMatch(' · cpus: 2') + }) + }) + + // TODO: the incremental option has been removed, update to another string feature + describe.skip('config with ppr incremental', () => { + const { next } = nextTestSetup({ + files: __dirname, + nextConfig: { + experimental: { + ppr: 'incremental', + }, + }, + }) + + it('should show the configured value for string features', async () => { + const output = stripAnsi(next.cliOutput) + expect(output).toMatch(experimentalHeader) + expect(output).toMatch(' · ppr: "incremental"') + }) + }) + + describe('config with multiple experimental keys', () => { + const { next } = nextTestSetup({ + files: __dirname, + nextConfig: { + experimental: { + workerThreads: true, + scrollRestoration: true, + }, + }, + }) + + it('should show warning with config from object with experimental and multiple keys', async () => { + // Make a request to trigger experimental warnings display + await next.fetch('/') + const output = stripAnsi(next.cliOutput) + expect(output).toContain(experimentalHeader) + expect(output).toContain(' ✓ workerThreads') + expect(output).toContain(' ✓ scrollRestoration') + }) + }) + }) + ;(isNextStart ? describe : describe.skip)('production mode', () => { + describe('next start output', () => { + const { next } = nextTestSetup({ + files: __dirname, + skipStart: true, + nextConfig: { + experimental: { + workerThreads: true, + scrollRestoration: true, + parallelServerCompiles: true, + cpus: 2, + }, + }, + }) + + it('should not show next app info in next start', async () => { + await next.build() + const startOffset = next.cliOutput.length + await next.start() + const output = stripAnsi(next.cliOutput.slice(startOffset)) + expect(output).not.toMatch(experimentalHeader) + }) + }) + + describe('next build output', () => { + const { next } = nextTestSetup({ + files: __dirname, + skipStart: true, + nextConfig: { + experimental: { + workerThreads: true, + scrollRestoration: true, + parallelServerCompiles: true, + prerenderEarlyExit: false, + cpus: 2, + }, + }, + }) + + it('should show next app info with all experimental features in next build', async () => { + await next.build() + const output = stripAnsi(next.cliOutput) + expect(output).toMatch(experimentalHeader) + expect(output).toMatch(' · cpus: 2') + expect(output).toMatch(' ✓ workerThreads') + expect(output).toMatch(' ✓ scrollRestoration') + expect(output).toMatch(' ⨯ prerenderEarlyExit') + expect(output).toMatch(' ✓ parallelServerCompiles') + }) + }) + + describe('unrecognized experimental features', () => { + const { next } = nextTestSetup({ + files: __dirname, + skipStart: true, + nextConfig: { + experimental: { + // @ts-expect-error - This is an intentional test + appDir: true, + }, + }, + }) + + it('should show unrecognized experimental features in warning but not in start log experiments section', async () => { + await next.build() + const startOffset = next.cliOutput.length + await next.start() + const startOutput = stripAnsi(next.cliOutput.slice(startOffset)) + expect(startOutput).not.toContain(experimentalHeader) + expect(stripAnsi(next.cliOutput)).toContain( + `Unrecognized key(s) in object: 'appDir' at "experimental"` + ) + }) + }) + }) +}) diff --git a/test/integration/config-experimental-warning/pages/index.js b/test/e2e/config-experimental-warning/pages/index.js similarity index 100% rename from test/integration/config-experimental-warning/pages/index.js rename to test/e2e/config-experimental-warning/pages/index.js diff --git a/test/e2e/conflicting-public-file-page/conflicting-public-file-page.test.ts b/test/e2e/conflicting-public-file-page/conflicting-public-file-page.test.ts new file mode 100644 index 000000000000..c43c6d4f1e8e --- /dev/null +++ b/test/e2e/conflicting-public-file-page/conflicting-public-file-page.test.ts @@ -0,0 +1,39 @@ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' + +describe('Errors on conflict between public file and page file', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + if (skipped) return + + if (isNextDev) { + it('should show conflict error during development', async () => { + await next.start() + + const regex = /A conflicting public file and page file was found for path/ + + const conflicts = ['/another/conflict', '/hello'] + for (const conflict of conflicts) { + const html = await next.render(conflict) + expect(html).toMatch(regex) + } + + expect(next.cliOutput).toMatch(regex) + }) + } + + if (isNextStart) { + it('should show conflict error during build', async () => { + const { cliOutput } = await next.build() + const conflicts = ['/another/conflict', '/another', '/hello'] + + expect(cliOutput).toMatch(/Conflicting public and page files were found/) + + for (const conflict of conflicts) { + expect(cliOutput.indexOf(conflict) > 0).toBe(true) + } + }) + } +}) diff --git a/test/integration/conflicting-public-file-page/pages/another/conflict.js b/test/e2e/conflicting-public-file-page/pages/another/conflict.js similarity index 100% rename from test/integration/conflicting-public-file-page/pages/another/conflict.js rename to test/e2e/conflicting-public-file-page/pages/another/conflict.js diff --git a/test/integration/conflicting-public-file-page/pages/another/index.js b/test/e2e/conflicting-public-file-page/pages/another/index.js similarity index 100% rename from test/integration/conflicting-public-file-page/pages/another/index.js rename to test/e2e/conflicting-public-file-page/pages/another/index.js diff --git a/test/integration/conflicting-public-file-page/pages/hello.js b/test/e2e/conflicting-public-file-page/pages/hello.js similarity index 100% rename from test/integration/conflicting-public-file-page/pages/hello.js rename to test/e2e/conflicting-public-file-page/pages/hello.js diff --git a/test/integration/conflicting-public-file-page/public/another/conflict b/test/e2e/conflicting-public-file-page/public/another/conflict similarity index 100% rename from test/integration/conflicting-public-file-page/public/another/conflict rename to test/e2e/conflicting-public-file-page/public/another/conflict diff --git a/test/integration/conflicting-public-file-page/public/another/index b/test/e2e/conflicting-public-file-page/public/another/index similarity index 100% rename from test/integration/conflicting-public-file-page/public/another/index rename to test/e2e/conflicting-public-file-page/public/another/index diff --git a/test/integration/conflicting-public-file-page/public/hello b/test/e2e/conflicting-public-file-page/public/hello similarity index 100% rename from test/integration/conflicting-public-file-page/public/hello rename to test/e2e/conflicting-public-file-page/public/hello diff --git a/test/integration/conflicting-public-file-page/public/normal.txt b/test/e2e/conflicting-public-file-page/public/normal.txt similarity index 100% rename from test/integration/conflicting-public-file-page/public/normal.txt rename to test/e2e/conflicting-public-file-page/public/normal.txt diff --git a/test/e2e/cpu-profiling/cpu-profiling-build.test.ts b/test/e2e/cpu-profiling/cpu-profiling-build.test.ts index 47c43880bddb..0f1ac74c9c38 100644 --- a/test/e2e/cpu-profiling/cpu-profiling-build.test.ts +++ b/test/e2e/cpu-profiling/cpu-profiling-build.test.ts @@ -10,6 +10,7 @@ describe('CPU Profiling - next build', () => { skipStart: true, skipDeployment: true, }) + if (skipped) return // CPU profiling only works with local `next build`, not dev or deploy modes if (isNextDev || isNextDeploy || skipped) { diff --git a/test/e2e/css-client-nav/css-client-nav.test.ts b/test/e2e/css-client-nav/css-client-nav.test.ts new file mode 100644 index 000000000000..0215ff6ac07f --- /dev/null +++ b/test/e2e/css-client-nav/css-client-nav.test.ts @@ -0,0 +1,223 @@ +/* eslint-disable jest/no-standalone-expect */ +import http from 'http' +import httpProxy from 'http-proxy' +import cheerio from 'cheerio' +import { findPort } from 'next-test-utils' +import webdriver from 'next-webdriver' +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' + +describe('CSS Module client-side navigation', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + // Calls `next.build()` and uses an in-test proxy in front of the Next + // server; both rely on the local-process model and are not applicable to + // deploy mode. + skipDeployment: true, + }) + if (skipped) return + + let proxyServer: http.Server + let proxyPort: number + let stallCss = false + + beforeAll(async () => { + if (!isNextDev) { + await next.build() + } + await next.start() + + if (!isNextDev) { + proxyPort = await findPort() + + const proxy = httpProxy.createProxyServer({ + target: next.url, + }) + + proxyServer = http.createServer(async (req, res) => { + if ( + stallCss && + req.url && + new URL(req.url, next.url).pathname.endsWith('.css') + ) { + console.log('stalling request for', req.url) + await new Promise((resolve) => setTimeout(resolve, 5 * 1000)) + } + proxy.web(req, res) + }) + + proxy.on('error', (err) => { + console.warn('Failed to proxy', err) + }) + + await new Promise<void>((resolve) => { + proxyServer.listen(proxyPort, () => resolve()) + }) + } + }) + + beforeEach(() => { + stallCss = false + }) + + afterAll(async () => { + if (proxyServer) { + proxyServer.close() + } + }) + + beforeEach(() => { + stallCss = false + }) + ;(isNextStart ? it : it.skip)( + 'should time out and hard navigate for stalled CSS request', + async () => { + stallCss = true + + const browser = await webdriver(proxyPort, '/red') + try { + await browser.eval('window.beforeNav = "hello"') + + const redColor = await browser.eval( + `window.getComputedStyle(document.querySelector('#verify-red')).color` + ) + expect(redColor).toMatchInlineSnapshot(`"rgb(255, 0, 0)"`) + expect(await browser.eval('window.beforeNav')).toBe('hello') + + await browser.elementByCss('#link-blue').click() + + await browser.waitForElementByCss('#verify-blue') + + const blueColor = await browser.eval( + `window.getComputedStyle(document.querySelector('#verify-blue')).color` + ) + expect(blueColor).toMatchInlineSnapshot(`"rgb(0, 0, 255)"`) + + expect(await browser.eval('window.beforeNav')).toBeFalsy() + } finally { + stallCss = false + await browser.close() + } + }, + 20000 + ) + + it('should be able to client-side navigate from red to blue', async () => { + const browser = isNextDev + ? await next.browser('/red') + : await webdriver(proxyPort, '/red') + + try { + await browser.eval(`window.__did_not_ssr = 'make sure this is set'`) + + const redColor = await browser.eval( + `window.getComputedStyle(document.querySelector('#verify-red')).color` + ) + expect(redColor).toMatchInlineSnapshot(`"rgb(255, 0, 0)"`) + + await browser.elementByCss('#link-blue').click() + + await browser.waitForElementByCss('#verify-blue') + + const blueColor = await browser.eval( + `window.getComputedStyle(document.querySelector('#verify-blue')).color` + ) + expect(blueColor).toMatchInlineSnapshot(`"rgb(0, 0, 255)"`) + + expect(await browser.eval(`window.__did_not_ssr`)).toMatchInlineSnapshot( + `"make sure this is set"` + ) + } finally { + await browser.close() + } + }) + + it('should be able to client-side navigate from blue to red', async () => { + if (!isNextDev) { + const content = await next.render('/blue') + const $ = cheerio.load(content) + + const serverCssPreloads = $('link[rel="preload"][as="style"]') + expect(serverCssPreloads.length).toBe(2) + + const serverCssPrefetches = $('link[rel="prefetch"][as="style"]') + expect(serverCssPrefetches.length).toBe(0) + } + + const browser = isNextDev + ? await next.browser('/blue') + : await webdriver(proxyPort, '/blue') + + try { + await browser.eval(`window.__did_not_ssr = 'make sure this is set'`) + + const blueColor = await browser.eval( + `window.getComputedStyle(document.querySelector('#verify-blue')).color` + ) + expect(blueColor).toMatchInlineSnapshot(`"rgb(0, 0, 255)"`) + + await browser.elementByCss('#link-red').click() + + await browser.waitForElementByCss('#verify-red') + + const redColor = await browser.eval( + `window.getComputedStyle(document.querySelector('#verify-red')).color` + ) + expect(redColor).toMatchInlineSnapshot(`"rgb(255, 0, 0)"`) + + expect(await browser.eval(`window.__did_not_ssr`)).toMatchInlineSnapshot( + `"make sure this is set"` + ) + } finally { + await browser.close() + } + }) + + it('should be able to client-side navigate from none to red', async () => { + const browser = isNextDev + ? await next.browser('/none') + : await webdriver(proxyPort, '/none') + + try { + await browser.eval(`window.__did_not_ssr = 'make sure this is set'`) + + await browser.elementByCss('#link-red').click() + await browser.waitForElementByCss('#verify-red') + + const redColor = await browser.eval( + `window.getComputedStyle(document.querySelector('#verify-red')).color` + ) + expect(redColor).toMatchInlineSnapshot(`"rgb(255, 0, 0)"`) + + expect(await browser.eval(`window.__did_not_ssr`)).toMatchInlineSnapshot( + `"make sure this is set"` + ) + } finally { + await browser.close() + } + }) + + it('should be able to client-side navigate from none to blue', async () => { + const browser = isNextDev + ? await next.browser('/none') + : await webdriver(proxyPort, '/none') + + try { + await browser.eval(`window.__did_not_ssr = 'make sure this is set'`) + + await browser.elementByCss('#link-blue').click() + await browser.waitForElementByCss('#verify-blue') + + const blueColor = await browser.eval( + `window.getComputedStyle(document.querySelector('#verify-blue')).color` + ) + expect(blueColor).toMatchInlineSnapshot(`"rgb(0, 0, 255)"`) + + expect(await browser.eval(`window.__did_not_ssr`)).toMatchInlineSnapshot( + `"make sure this is set"` + ) + } finally { + await browser.close() + } + }) +}) diff --git a/test/integration/css-fixtures/next.config.js b/test/e2e/css-client-nav/next.config.js similarity index 70% rename from test/integration/css-fixtures/next.config.js rename to test/e2e/css-client-nav/next.config.js index 52231fd92e5b..6f00d70b4947 100644 --- a/test/integration/css-fixtures/next.config.js +++ b/test/e2e/css-client-nav/next.config.js @@ -1,6 +1,5 @@ module.exports = { onDemandEntries: { - // Make sure entries are not getting disposed. maxInactiveAge: 1000 * 60 * 60, }, productionBrowserSourceMaps: true, diff --git a/test/integration/css-features/fixtures/inline-comments/pages/_app.js b/test/e2e/css-client-nav/pages/_app.js similarity index 100% rename from test/integration/css-features/fixtures/inline-comments/pages/_app.js rename to test/e2e/css-client-nav/pages/_app.js diff --git a/test/integration/css-fixtures/multi-module/pages/blue.js b/test/e2e/css-client-nav/pages/blue.js similarity index 100% rename from test/integration/css-fixtures/multi-module/pages/blue.js rename to test/e2e/css-client-nav/pages/blue.js diff --git a/test/integration/css-fixtures/multi-module/pages/blue.module.css b/test/e2e/css-client-nav/pages/blue.module.css similarity index 100% rename from test/integration/css-fixtures/multi-module/pages/blue.module.css rename to test/e2e/css-client-nav/pages/blue.module.css diff --git a/test/integration/css-fixtures/multi-module/pages/global.css b/test/e2e/css-client-nav/pages/global.css similarity index 100% rename from test/integration/css-fixtures/multi-module/pages/global.css rename to test/e2e/css-client-nav/pages/global.css diff --git a/test/integration/css-fixtures/multi-module/pages/none.js b/test/e2e/css-client-nav/pages/none.js similarity index 100% rename from test/integration/css-fixtures/multi-module/pages/none.js rename to test/e2e/css-client-nav/pages/none.js diff --git a/test/integration/css-fixtures/multi-module/pages/red.js b/test/e2e/css-client-nav/pages/red.js similarity index 100% rename from test/integration/css-fixtures/multi-module/pages/red.js rename to test/e2e/css-client-nav/pages/red.js diff --git a/test/integration/css-fixtures/multi-module/pages/red.module.css b/test/e2e/css-client-nav/pages/red.module.css similarity index 100% rename from test/integration/css-fixtures/multi-module/pages/red.module.css rename to test/e2e/css-client-nav/pages/red.module.css diff --git a/test/e2e/css-features/css-and-styled-jsx.test.ts b/test/e2e/css-features/css-and-styled-jsx.test.ts new file mode 100644 index 000000000000..8318020b58d2 --- /dev/null +++ b/test/e2e/css-features/css-and-styled-jsx.test.ts @@ -0,0 +1,17 @@ +import { nextTestSetup } from 'e2e-utils' +import { join } from 'path' + +describe('Ordering with styled-jsx', () => { + const { next } = nextTestSetup({ + files: join(__dirname, 'fixtures/with-styled-jsx'), + }) + + it('should have the correct color (css ordering)', async () => { + const browser = await next.browser('/') + + const currentColor = await browser.eval( + `window.getComputedStyle(document.querySelector('.my-text')).color` + ) + expect(currentColor).toMatchInlineSnapshot(`"rgb(0, 128, 0)"`) + }) +}) diff --git a/test/e2e/css-features/css-modules-ordering.test.ts b/test/e2e/css-features/css-modules-ordering.test.ts new file mode 100644 index 000000000000..7c95f9ea94e5 --- /dev/null +++ b/test/e2e/css-features/css-modules-ordering.test.ts @@ -0,0 +1,656 @@ +/* eslint-disable jest/no-standalone-expect */ +/* eslint-disable jest/no-identical-title */ +import cheerio from 'cheerio' +import { isNextDev, isNextStart, nextTestSetup } from 'e2e-utils' +import type { Playwright } from 'next-webdriver' +import { retry } from 'next-test-utils' +import path from 'path' + +// https://github.com/vercel/next.js/issues/12343 +describe('Basic CSS Modules Ordering', () => { + ;(process.env.IS_TURBOPACK_TEST ? describe.skip : describe)( + 'useLightningcss(true)', + () => { + const { next } = nextTestSetup({ + files: path.join(__dirname, 'fixtures', 'next-issue-12343'), + nextConfig: { + experimental: { + useLightningcss: true, + }, + }, + skipDeployment: true, + }) + + async function checkGreenButton(browser: Playwright) { + await browser.elementByCss('#link-other') + const titleColor = await browser.eval(() => { + const el = document.querySelector('#link-other') + return el ? window.getComputedStyle(el).backgroundColor : '' + }) + expect(titleColor).toBe('rgb(0, 255, 0)') + } + + async function checkPinkButton(browser: Playwright) { + await browser.elementByCss('#link-index') + const titleColor = await browser.eval(() => { + const el = document.querySelector('#link-index') + return el ? window.getComputedStyle(el).backgroundColor : '' + }) + expect(titleColor).toBe('rgb(255, 105, 180)') + } + + it('should have correct color on index page (on load)', async () => { + const browser = await next.browser('/') + try { + await checkGreenButton(browser) + } finally { + await browser.close() + } + }) + + it('should have correct color on index page (on hover)', async () => { + const browser = await next.browser('/') + try { + await checkGreenButton(browser) + await browser.elementByCss('#link-other').moveTo() + await retry(async () => { + await checkGreenButton(browser) + }) + } finally { + await browser.close() + } + }) + + it('should have correct color on index page (on nav)', async () => { + const browser = await next.browser('/') + try { + await checkGreenButton(browser) + await browser.elementByCss('#link-other').click() + + await browser.elementByCss('#link-index') + await checkPinkButton(browser) + + await browser.elementByCss('#link-index').click() + await checkGreenButton(browser) + } finally { + await browser.close() + } + }) + } + ) + ;(process.env.IS_TURBOPACK_TEST ? describe.skip : describe)( + 'useLightningcss(false)', + () => { + const { next } = nextTestSetup({ + files: path.join(__dirname, 'fixtures', 'next-issue-12343'), + nextConfig: { + experimental: { + useLightningcss: false, + }, + }, + skipDeployment: true, + }) + + async function checkGreenButton(browser: Playwright) { + await browser.elementByCss('#link-other') + const titleColor = await browser.eval(() => { + const el = document.querySelector('#link-other') + return el ? window.getComputedStyle(el).backgroundColor : '' + }) + expect(titleColor).toBe('rgb(0, 255, 0)') + } + + async function checkPinkButton(browser: Playwright) { + await browser.elementByCss('#link-index') + const titleColor = await browser.eval(() => { + const el = document.querySelector('#link-index') + return el ? window.getComputedStyle(el).backgroundColor : '' + }) + expect(titleColor).toBe('rgb(255, 105, 180)') + } + + it('should have correct color on index page (on load)', async () => { + const browser = await next.browser('/') + try { + await checkGreenButton(browser) + } finally { + await browser.close() + } + }) + + it('should have correct color on index page (on hover)', async () => { + const browser = await next.browser('/') + try { + await checkGreenButton(browser) + await browser.elementByCss('#link-other').moveTo() + await retry(async () => { + await checkGreenButton(browser) + }) + } finally { + await browser.close() + } + }) + + it('should have correct color on index page (on nav)', async () => { + const browser = await next.browser('/') + try { + await checkGreenButton(browser) + await browser.elementByCss('#link-other').click() + + await browser.elementByCss('#link-index') + await checkPinkButton(browser) + + await browser.elementByCss('#link-index').click() + await checkGreenButton(browser) + } finally { + await browser.close() + } + }) + } + ) +}) + +describe('Ordering with Global CSS and Modules', () => { + describe('useLightningcss(true)', () => { + const { next } = nextTestSetup({ + files: path.join(__dirname, 'fixtures', 'global-and-module-ordering'), + nextConfig: { + experimental: { + useLightningcss: true, + }, + }, + skipDeployment: true, + }) + + ;(isNextDev ? it : it.skip)( + 'should not execute scripts in any order', + async () => { + const content = await next.render('/') + const $ = cheerio.load(content) + + let asyncCount = 0 + let totalCount = 0 + for (const script of $('script').toArray()) { + ++totalCount + if ('async' in script.attribs) { + ++asyncCount + } + } + + expect(asyncCount).toBe(0) + expect(totalCount).not.toBe(0) + } + ) + + it('should have the correct color (css ordering)', async () => { + const browser = await next.browser('/') + + const currentColor = await browser.eval(() => { + const el = document.querySelector('#blueText') + return el ? window.getComputedStyle(el).color : '' + }) + expect(currentColor).toMatchInlineSnapshot(`"rgb(0, 0, 255)"`) + await browser.close() + }) + ;(isNextDev ? it : it.skip)( + 'should have the correct color (css ordering) during hot reloads', + async () => { + const browser = await next.browser('/') + + try { + const blueColor = await browser.eval(() => { + const el = document.querySelector('#blueText') + return el ? window.getComputedStyle(el).color : '' + }) + expect(blueColor).toMatchInlineSnapshot(`"rgb(0, 0, 255)"`) + + const yellowColor = await browser.eval(() => { + const el = document.querySelector('#yellowText') + return el ? window.getComputedStyle(el).color : '' + }) + expect(yellowColor).toMatchInlineSnapshot(`"rgb(255, 255, 0)"`) + + await next.patchFile( + 'pages/index.module.css', + (c) => (c ?? '').replace('color: yellow;', 'color: rgb(1, 1, 1);'), + async () => { + await retry(async () => { + const c = await browser.eval(() => { + const el = document.querySelector('#yellowText') + return el ? window.getComputedStyle(el).color : '' + }) + expect(c).toBe('rgb(1, 1, 1)') + }) + await retry(async () => { + const c = await browser.eval(() => { + const el = document.querySelector('#blueText') + return el ? window.getComputedStyle(el).color : '' + }) + expect(c).toBe('rgb(0, 0, 255)') + }) + } + ) + } finally { + await browser.close() + } + } + ) + ;(isNextStart ? it : it.skip)('should have compiled successfully', () => { + expect(next.cliOutput).toMatch(/Compiled successfully/) + }) + }) + + describe('useLightningcss(false)', () => { + const { next } = nextTestSetup({ + files: path.join(__dirname, 'fixtures', 'global-and-module-ordering'), + nextConfig: { + experimental: { + useLightningcss: false, + }, + }, + skipDeployment: true, + }) + + ;(isNextDev ? it : it.skip)( + 'should not execute scripts in any order', + async () => { + const content = await next.render('/') + const $ = cheerio.load(content) + + let asyncCount = 0 + let totalCount = 0 + for (const script of $('script').toArray()) { + ++totalCount + if ('async' in script.attribs) { + ++asyncCount + } + } + + expect(asyncCount).toBe(0) + expect(totalCount).not.toBe(0) + } + ) + + it('should have the correct color (css ordering)', async () => { + const browser = await next.browser('/') + + const currentColor = await browser.eval(() => { + const el = document.querySelector('#blueText') + return el ? window.getComputedStyle(el).color : '' + }) + expect(currentColor).toMatchInlineSnapshot(`"rgb(0, 0, 255)"`) + await browser.close() + }) + ;(isNextDev ? it : it.skip)( + 'should have the correct color (css ordering) during hot reloads', + async () => { + const browser = await next.browser('/') + + try { + const blueColor = await browser.eval(() => { + const el = document.querySelector('#blueText') + return el ? window.getComputedStyle(el).color : '' + }) + expect(blueColor).toMatchInlineSnapshot(`"rgb(0, 0, 255)"`) + + const yellowColor = await browser.eval(() => { + const el = document.querySelector('#yellowText') + return el ? window.getComputedStyle(el).color : '' + }) + expect(yellowColor).toMatchInlineSnapshot(`"rgb(255, 255, 0)"`) + + await next.patchFile( + 'pages/index.module.css', + (c) => (c ?? '').replace('color: yellow;', 'color: rgb(1, 1, 1);'), + async () => { + await retry(async () => { + const c = await browser.eval(() => { + const el = document.querySelector('#yellowText') + return el ? window.getComputedStyle(el).color : '' + }) + expect(c).toBe('rgb(1, 1, 1)') + }) + await retry(async () => { + const c = await browser.eval(() => { + const el = document.querySelector('#blueText') + return el ? window.getComputedStyle(el).color : '' + }) + expect(c).toBe('rgb(0, 0, 255)') + }) + } + ) + } finally { + await browser.close() + } + } + ) + ;(isNextStart ? it : it.skip)('should have compiled successfully', () => { + expect(next.cliOutput).toMatch(/Compiled successfully/) + }) + }) +}) + +// https://github.com/vercel/next.js/issues/12445 +// This feature is not supported in Turbopack +describe('CSS Modules Composes Ordering', () => { + ;(process.env.IS_TURBOPACK_TEST ? describe.skip : describe)( + 'useLightningcss(true)', + () => { + const { next } = nextTestSetup({ + files: path.join(__dirname, 'fixtures', 'composes-ordering'), + nextConfig: { + experimental: { + useLightningcss: true, + }, + }, + skipDeployment: true, + }) + + async function checkBlackTitle(browser: Playwright) { + await browser.elementByCss('#black-title') + const titleColor = await browser.eval(() => { + const el = document.querySelector('#black-title') + return el ? window.getComputedStyle(el).color : '' + }) + expect(titleColor).toBe('rgb(17, 17, 17)') + } + + async function checkRedTitle(browser: Playwright) { + await browser.elementByCss('#red-title') + const titleColor = await browser.eval(() => { + const el = document.querySelector('#red-title') + return el ? window.getComputedStyle(el).color : '' + }) + expect(titleColor).toBe('rgb(255, 0, 0)') + } + + it('should have correct color on index page (on load)', async () => { + const browser = await next.browser('/') + try { + await checkBlackTitle(browser) + } finally { + await browser.close() + } + }) + + it('should have correct color on index page (on hover)', async () => { + const browser = await next.browser('/') + try { + await checkBlackTitle(browser) + await browser.elementByCss('#link-other').moveTo() + await retry(async () => { + await checkBlackTitle(browser) + }) + } finally { + await browser.close() + } + }) + ;(isNextStart ? it : it.skip)( + 'should not change color on hover', + async () => { + const browser = await next.browser('/') + try { + await checkBlackTitle(browser) + await browser.elementByCss('#link-other').moveTo() + await retry(async () => { + await checkBlackTitle(browser) + }) + } finally { + await browser.close() + } + } + ) + ;(isNextStart ? it : it.skip)( + 'should have correct CSS injection order', + async () => { + const browser = await next.browser('/') + try { + await checkBlackTitle(browser) + + const prevSiblingHref = await browser.eval(() => { + const el = document.querySelector( + 'link[rel=stylesheet][data-n-p]' + )?.previousSibling as Element | null + return el?.getAttribute('href') ?? null + }) + const currentPageHref = await browser.eval(() => { + return document + .querySelector('link[rel=stylesheet][data-n-p]') + ?.getAttribute('href') + }) + expect(prevSiblingHref).toBeDefined() + expect(prevSiblingHref).toBe(currentPageHref) + + await browser.elementByCss('#link-other').click() + await checkRedTitle(browser) + + const newPrevSibling = await browser.eval(() => { + const el = document.querySelector('style[data-n-href]') + ?.previousSibling as Element | null + return el?.getAttribute('data-n-css') ?? null + }) + const newPageHref = await browser.eval(() => { + return document + .querySelector('style[data-n-href]') + ?.getAttribute('data-n-href') + }) + expect(newPrevSibling).toBe('') + expect(newPageHref).toBeDefined() + expect(newPageHref).not.toBe(currentPageHref) + + await browser.elementByCss('#link-index').click() + await checkBlackTitle(browser) + + const newPrevSibling2 = await browser.eval(() => { + const el = document.querySelector('style[data-n-href]') + ?.previousSibling as Element | null + return el?.getAttribute('data-n-css') ?? null + }) + const newPageHref2 = await browser.eval(() => { + return document + .querySelector('style[data-n-href]') + ?.getAttribute('data-n-href') + }) + expect(newPrevSibling2).toBe('') + expect(newPageHref2).toBeDefined() + expect(newPageHref2).toBe(currentPageHref) + } finally { + await browser.close() + } + } + ) + + it('should have correct color on index page (on nav from index)', async () => { + const browser = await next.browser('/') + try { + await checkBlackTitle(browser) + await browser.elementByCss('#link-other').click() + + await browser.elementByCss('#link-index') + await checkRedTitle(browser) + + await browser.elementByCss('#link-index').click() + await checkBlackTitle(browser) + } finally { + await browser.close() + } + }) + + it('should have correct color on index page (on nav from other)', async () => { + const browser = await next.browser('/other') + try { + await checkRedTitle(browser) + await browser.elementByCss('#link-index').click() + + await browser.elementByCss('#link-other') + await checkBlackTitle(browser) + + await browser.elementByCss('#link-other').click() + await checkRedTitle(browser) + } finally { + await browser.close() + } + }) + } + ) + ;(process.env.IS_TURBOPACK_TEST ? describe.skip : describe)( + 'useLightningcss(false)', + () => { + const { next } = nextTestSetup({ + files: path.join(__dirname, 'fixtures', 'composes-ordering'), + nextConfig: { + experimental: { + useLightningcss: false, + }, + }, + skipDeployment: true, + }) + + async function checkBlackTitle(browser: Playwright) { + await browser.elementByCss('#black-title') + const titleColor = await browser.eval(() => { + const el = document.querySelector('#black-title') + return el ? window.getComputedStyle(el).color : '' + }) + expect(titleColor).toBe('rgb(17, 17, 17)') + } + + async function checkRedTitle(browser: Playwright) { + await browser.elementByCss('#red-title') + const titleColor = await browser.eval(() => { + const el = document.querySelector('#red-title') + return el ? window.getComputedStyle(el).color : '' + }) + expect(titleColor).toBe('rgb(255, 0, 0)') + } + + it('should have correct color on index page (on load)', async () => { + const browser = await next.browser('/') + try { + await checkBlackTitle(browser) + } finally { + await browser.close() + } + }) + + it('should have correct color on index page (on hover)', async () => { + const browser = await next.browser('/') + try { + await checkBlackTitle(browser) + await browser.elementByCss('#link-other').moveTo() + await retry(async () => { + await checkBlackTitle(browser) + }) + } finally { + await browser.close() + } + }) + ;(isNextStart ? it : it.skip)( + 'should not change color on hover', + async () => { + const browser = await next.browser('/') + try { + await checkBlackTitle(browser) + await browser.elementByCss('#link-other').moveTo() + await retry(async () => { + await checkBlackTitle(browser) + }) + } finally { + await browser.close() + } + } + ) + ;(isNextStart ? it : it.skip)( + 'should have correct CSS injection order', + async () => { + const browser = await next.browser('/') + try { + await checkBlackTitle(browser) + + const prevSiblingHref = await browser.eval(() => { + const el = document.querySelector( + 'link[rel=stylesheet][data-n-p]' + )?.previousSibling as Element | null + return el?.getAttribute('href') ?? null + }) + const currentPageHref = await browser.eval(() => { + return document + .querySelector('link[rel=stylesheet][data-n-p]') + ?.getAttribute('href') + }) + expect(prevSiblingHref).toBeDefined() + expect(prevSiblingHref).toBe(currentPageHref) + + await browser.elementByCss('#link-other').click() + await checkRedTitle(browser) + + const newPrevSibling = await browser.eval(() => { + const el = document.querySelector('style[data-n-href]') + ?.previousSibling as Element | null + return el?.getAttribute('data-n-css') ?? null + }) + const newPageHref = await browser.eval(() => { + return document + .querySelector('style[data-n-href]') + ?.getAttribute('data-n-href') + }) + expect(newPrevSibling).toBe('') + expect(newPageHref).toBeDefined() + expect(newPageHref).not.toBe(currentPageHref) + + await browser.elementByCss('#link-index').click() + await checkBlackTitle(browser) + + const newPrevSibling2 = await browser.eval(() => { + const el = document.querySelector('style[data-n-href]') + ?.previousSibling as Element | null + return el?.getAttribute('data-n-css') ?? null + }) + const newPageHref2 = await browser.eval(() => { + return document + .querySelector('style[data-n-href]') + ?.getAttribute('data-n-href') + }) + expect(newPrevSibling2).toBe('') + expect(newPageHref2).toBeDefined() + expect(newPageHref2).toBe(currentPageHref) + } finally { + await browser.close() + } + } + ) + + it('should have correct color on index page (on nav from index)', async () => { + const browser = await next.browser('/') + try { + await checkBlackTitle(browser) + await browser.elementByCss('#link-other').click() + + await browser.elementByCss('#link-index') + await checkRedTitle(browser) + + await browser.elementByCss('#link-index').click() + await checkBlackTitle(browser) + } finally { + await browser.close() + } + }) + + it('should have correct color on index page (on nav from other)', async () => { + const browser = await next.browser('/other') + try { + await checkRedTitle(browser) + await browser.elementByCss('#link-index').click() + + await browser.elementByCss('#link-other') + await checkBlackTitle(browser) + + await browser.elementByCss('#link-other').click() + await checkRedTitle(browser) + } finally { + await browser.close() + } + }) + } + ) +}) diff --git a/test/integration/css-fixtures/hydrate-without-deps/.gitignore b/test/e2e/css-features/fixtures/composes-ordering/.gitignore similarity index 100% rename from test/integration/css-fixtures/hydrate-without-deps/.gitignore rename to test/e2e/css-features/fixtures/composes-ordering/.gitignore diff --git a/test/integration/css-fixtures/composes-ordering/pages/common.module.css b/test/e2e/css-features/fixtures/composes-ordering/pages/common.module.css similarity index 100% rename from test/integration/css-fixtures/composes-ordering/pages/common.module.css rename to test/e2e/css-features/fixtures/composes-ordering/pages/common.module.css diff --git a/test/integration/css-fixtures/composes-ordering/pages/index.js b/test/e2e/css-features/fixtures/composes-ordering/pages/index.js similarity index 100% rename from test/integration/css-fixtures/composes-ordering/pages/index.js rename to test/e2e/css-features/fixtures/composes-ordering/pages/index.js diff --git a/test/integration/css-fixtures/composes-ordering/pages/index.module.css b/test/e2e/css-features/fixtures/composes-ordering/pages/index.module.css similarity index 100% rename from test/integration/css-fixtures/composes-ordering/pages/index.module.css rename to test/e2e/css-features/fixtures/composes-ordering/pages/index.module.css diff --git a/test/integration/css-fixtures/composes-ordering/pages/other.js b/test/e2e/css-features/fixtures/composes-ordering/pages/other.js similarity index 100% rename from test/integration/css-fixtures/composes-ordering/pages/other.js rename to test/e2e/css-features/fixtures/composes-ordering/pages/other.js diff --git a/test/integration/css-fixtures/composes-ordering/pages/other.module.css b/test/e2e/css-features/fixtures/composes-ordering/pages/other.module.css similarity index 100% rename from test/integration/css-fixtures/composes-ordering/pages/other.module.css rename to test/e2e/css-features/fixtures/composes-ordering/pages/other.module.css diff --git a/test/integration/css-fixtures/multi-global-reversed/.gitignore b/test/e2e/css-features/fixtures/global-and-module-ordering/.gitignore similarity index 100% rename from test/integration/css-fixtures/multi-global-reversed/.gitignore rename to test/e2e/css-features/fixtures/global-and-module-ordering/.gitignore diff --git a/test/integration/css-fixtures/bad-custom-configuration-arr-1/pages/_app.js b/test/e2e/css-features/fixtures/global-and-module-ordering/pages/_app.js similarity index 100% rename from test/integration/css-fixtures/bad-custom-configuration-arr-1/pages/_app.js rename to test/e2e/css-features/fixtures/global-and-module-ordering/pages/_app.js diff --git a/test/integration/css-fixtures/global-and-module-ordering/pages/index.js b/test/e2e/css-features/fixtures/global-and-module-ordering/pages/index.js similarity index 100% rename from test/integration/css-fixtures/global-and-module-ordering/pages/index.js rename to test/e2e/css-features/fixtures/global-and-module-ordering/pages/index.js diff --git a/test/integration/css-fixtures/global-and-module-ordering/pages/index.module.css b/test/e2e/css-features/fixtures/global-and-module-ordering/pages/index.module.css similarity index 100% rename from test/integration/css-fixtures/global-and-module-ordering/pages/index.module.css rename to test/e2e/css-features/fixtures/global-and-module-ordering/pages/index.module.css diff --git a/test/integration/css-fixtures/global-and-module-ordering/pages/index2.module.css b/test/e2e/css-features/fixtures/global-and-module-ordering/pages/index2.module.css similarity index 100% rename from test/integration/css-fixtures/global-and-module-ordering/pages/index2.module.css rename to test/e2e/css-features/fixtures/global-and-module-ordering/pages/index2.module.css diff --git a/test/integration/css-fixtures/global-and-module-ordering/styles/global.css b/test/e2e/css-features/fixtures/global-and-module-ordering/styles/global.css similarity index 100% rename from test/integration/css-fixtures/global-and-module-ordering/styles/global.css rename to test/e2e/css-features/fixtures/global-and-module-ordering/styles/global.css diff --git a/test/integration/css-fixtures/multi-global/.gitignore b/test/e2e/css-features/fixtures/multi-page/.gitignore similarity index 100% rename from test/integration/css-fixtures/multi-global/.gitignore rename to test/e2e/css-features/fixtures/multi-page/.gitignore diff --git a/test/integration/css-fixtures/multi-page/pages/_app.js b/test/e2e/css-features/fixtures/multi-page/pages/_app.js similarity index 100% rename from test/integration/css-fixtures/multi-page/pages/_app.js rename to test/e2e/css-features/fixtures/multi-page/pages/_app.js diff --git a/test/e2e/css-features/fixtures/multi-page/pages/page1.js b/test/e2e/css-features/fixtures/multi-page/pages/page1.js new file mode 100644 index 000000000000..bbb3756ceaed --- /dev/null +++ b/test/e2e/css-features/fixtures/multi-page/pages/page1.js @@ -0,0 +1,12 @@ +import Link from 'next/link' +export default function Page1() { + return ( + <> + <div className="red-text">This text should be red.</div> + <br /> + <input key={'' + Math.random()} id="text-input" type="text" /> + <br /> + <Link href="/page2">Switch page</Link> + </> + ) +} diff --git a/test/e2e/css-features/fixtures/multi-page/pages/page2.js b/test/e2e/css-features/fixtures/multi-page/pages/page2.js new file mode 100644 index 000000000000..10f2c5792255 --- /dev/null +++ b/test/e2e/css-features/fixtures/multi-page/pages/page2.js @@ -0,0 +1,12 @@ +import Link from 'next/link' +export default function Page2() { + return ( + <> + <div className="blue-text">This text should be blue.</div> + <br /> + <input key={'' + Math.random()} id="text-input" type="text" /> + <br /> + <Link href="/page1">Switch page</Link> + </> + ) +} diff --git a/test/integration/css-fixtures/multi-global/styles/global1.css b/test/e2e/css-features/fixtures/multi-page/styles/global1.css similarity index 100% rename from test/integration/css-fixtures/multi-global/styles/global1.css rename to test/e2e/css-features/fixtures/multi-page/styles/global1.css diff --git a/test/integration/css-fixtures/multi-global/styles/global2.css b/test/e2e/css-features/fixtures/multi-page/styles/global2.css similarity index 100% rename from test/integration/css-fixtures/multi-global/styles/global2.css rename to test/e2e/css-features/fixtures/multi-page/styles/global2.css diff --git a/test/integration/css-fixtures/multi-page/.gitignore b/test/e2e/css-features/fixtures/next-issue-12343/.gitignore similarity index 100% rename from test/integration/css-fixtures/multi-page/.gitignore rename to test/e2e/css-features/fixtures/next-issue-12343/.gitignore diff --git a/test/integration/css-fixtures/next-issue-12343/components/button.jsx b/test/e2e/css-features/fixtures/next-issue-12343/components/button.jsx similarity index 100% rename from test/integration/css-fixtures/next-issue-12343/components/button.jsx rename to test/e2e/css-features/fixtures/next-issue-12343/components/button.jsx diff --git a/test/integration/css-fixtures/next-issue-12343/components/button.module.css b/test/e2e/css-features/fixtures/next-issue-12343/components/button.module.css similarity index 100% rename from test/integration/css-fixtures/next-issue-12343/components/button.module.css rename to test/e2e/css-features/fixtures/next-issue-12343/components/button.module.css diff --git a/test/integration/css-fixtures/next-issue-12343/pages/another-page.js b/test/e2e/css-features/fixtures/next-issue-12343/pages/another-page.js similarity index 100% rename from test/integration/css-fixtures/next-issue-12343/pages/another-page.js rename to test/e2e/css-features/fixtures/next-issue-12343/pages/another-page.js diff --git a/test/integration/css-fixtures/next-issue-12343/pages/homepage.module.css b/test/e2e/css-features/fixtures/next-issue-12343/pages/homepage.module.css similarity index 100% rename from test/integration/css-fixtures/next-issue-12343/pages/homepage.module.css rename to test/e2e/css-features/fixtures/next-issue-12343/pages/homepage.module.css diff --git a/test/integration/css-fixtures/next-issue-12343/pages/index.js b/test/e2e/css-features/fixtures/next-issue-12343/pages/index.js similarity index 100% rename from test/integration/css-fixtures/next-issue-12343/pages/index.js rename to test/e2e/css-features/fixtures/next-issue-12343/pages/index.js diff --git a/test/integration/css-fixtures/nested-global/.gitignore b/test/e2e/css-features/fixtures/transition-react/.gitignore similarity index 100% rename from test/integration/css-fixtures/nested-global/.gitignore rename to test/e2e/css-features/fixtures/transition-react/.gitignore diff --git a/test/e2e/css-features/fixtures/transition-react/pages/index.js b/test/e2e/css-features/fixtures/transition-react/pages/index.js new file mode 100644 index 000000000000..65fe8dae95fb --- /dev/null +++ b/test/e2e/css-features/fixtures/transition-react/pages/index.js @@ -0,0 +1,11 @@ +import Link from 'next/link' + +export default function Home() { + return ( + <main> + <Link href="/other" prefetch={false} id="link-other"> + other + </Link> + </main> + ) +} diff --git a/test/e2e/css-features/fixtures/transition-react/pages/other.js b/test/e2e/css-features/fixtures/transition-react/pages/other.js new file mode 100644 index 000000000000..dbee7e291bf3 --- /dev/null +++ b/test/e2e/css-features/fixtures/transition-react/pages/other.js @@ -0,0 +1,34 @@ +import React from 'react' +import Link from 'next/link' +import css from './other.module.css' + +export default class Other extends React.Component { + ref = React.createRef() + + constructor(props) { + super(props) + this.state = { + color: null, + } + } + + componentDidMount() { + this.setState({ + color: window.getComputedStyle(this.ref.current).color, + }) + } + + render() { + return ( + <main> + <Link href="/" prefetch={false} id="link-index"> + index page + </Link> + <br /> + <h1 id="red-title" className={css.root} ref={this.ref}> + {this.state.color} + </h1> + </main> + ) + } +} diff --git a/test/e2e/css-features/fixtures/transition-react/pages/other.module.css b/test/e2e/css-features/fixtures/transition-react/pages/other.module.css new file mode 100644 index 000000000000..d810e45683b8 --- /dev/null +++ b/test/e2e/css-features/fixtures/transition-react/pages/other.module.css @@ -0,0 +1,3 @@ +.root { + color: red; +} diff --git a/test/integration/css-fixtures/bad-custom-configuration-arr-2/pages/_app.js b/test/e2e/css-features/fixtures/with-styled-jsx/pages/_app.js similarity index 100% rename from test/integration/css-fixtures/bad-custom-configuration-arr-2/pages/_app.js rename to test/e2e/css-features/fixtures/with-styled-jsx/pages/_app.js diff --git a/test/integration/css-fixtures/with-styled-jsx/pages/index.js b/test/e2e/css-features/fixtures/with-styled-jsx/pages/index.js similarity index 100% rename from test/integration/css-fixtures/with-styled-jsx/pages/index.js rename to test/e2e/css-features/fixtures/with-styled-jsx/pages/index.js diff --git a/test/integration/css-fixtures/with-styled-jsx/styles/global.css b/test/e2e/css-features/fixtures/with-styled-jsx/styles/global.css similarity index 100% rename from test/integration/css-fixtures/with-styled-jsx/styles/global.css rename to test/e2e/css-features/fixtures/with-styled-jsx/styles/global.css diff --git a/test/e2e/custom-error-page-exception/custom-error-page-exception.test.ts b/test/e2e/custom-error-page-exception/custom-error-page-exception.test.ts new file mode 100644 index 000000000000..1f84d5af8e92 --- /dev/null +++ b/test/e2e/custom-error-page-exception/custom-error-page-exception.test.ts @@ -0,0 +1,22 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +// TODO: re-enable with React 18 +describe.skip('Custom error page exception', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should handle errors from _error render', async () => { + const navSel = '#nav' + const browser = await next.browser('/') + await browser.waitForElementByCss(navSel).elementByCss(navSel).click() + + await retry(async () => { + const html = await browser.eval('document.documentElement.innerHTML') + expect(html).toMatch( + /Application error: a client-side exception has occurred/ + ) + }) + }) +}) diff --git a/test/integration/custom-error-page-exception/pages/_error.js b/test/e2e/custom-error-page-exception/pages/_error.js similarity index 100% rename from test/integration/custom-error-page-exception/pages/_error.js rename to test/e2e/custom-error-page-exception/pages/_error.js diff --git a/test/integration/custom-error-page-exception/pages/index.js b/test/e2e/custom-error-page-exception/pages/index.js similarity index 100% rename from test/integration/custom-error-page-exception/pages/index.js rename to test/e2e/custom-error-page-exception/pages/index.js diff --git a/test/e2e/custom-error/custom-error.test.ts b/test/e2e/custom-error/custom-error.test.ts new file mode 100644 index 000000000000..7931b383f4e7 --- /dev/null +++ b/test/e2e/custom-error/custom-error.test.ts @@ -0,0 +1,73 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +const customErrNo404Match = + /You have added a custom \/_error page without a custom \/404 page/ + +describe('Custom _error', () => { + const { next, isNextDev, isNextStart } = nextTestSetup({ + files: __dirname, + dependencies: { + react: '19.3.0-canary-fef12a01-20260413', + 'react-dom': '19.3.0-canary-fef12a01-20260413', + }, + skipDeployment: true, + }) + + if (isNextDev) { + it('should not warn with /_error and /404 when rendering error first', async () => { + const outputIndex = next.cliOutput.length + await next.patchFile('pages/404.js', 'export default <h1>') + try { + await retry(async () => { + const html = await next.render('/404') + expect(html).toContain("Expected '\\u003c/', got '\\u003ceof\\u003e'") + expect(next.cliOutput.slice(outputIndex)).not.toMatch( + customErrNo404Match + ) + }) + } finally { + await next.deleteFile('pages/404.js') + } + }) + + it('should not warn with /_error and /404', async () => { + const outputIndex = next.cliOutput.length + await next.patchFile( + 'pages/404.js', + `export default () => 'not found...'` + ) + try { + await retry(async () => { + const html = await next.render('/404') + expect(html).toContain('not found...') + expect(next.cliOutput.slice(outputIndex)).not.toMatch( + customErrNo404Match + ) + }) + } finally { + await next.deleteFile('pages/404.js') + } + }) + + it('should warn on custom /_error without custom /404', async () => { + await retry(async () => { + const html = await next.render('/404') + expect(next.cliOutput).toMatch(customErrNo404Match) + expect(html).toContain('An error 404 occurred on server') + }) + }) + } + + if (isNextStart) { + it('should not contain /_error in build output', async () => { + expect(next.cliOutput).toMatch(/ƒ .*?\/404/) + expect(next.cliOutput).not.toMatch(/ƒ .*?\/_error/) + }) + + it('renders custom _error successfully', async () => { + const html = await next.render('/') + expect(html).toMatch(/Custom error/) + }) + } +}) diff --git a/test/integration/custom-error/pages/_error.js b/test/e2e/custom-error/pages/_error.js similarity index 100% rename from test/integration/custom-error/pages/_error.js rename to test/e2e/custom-error/pages/_error.js diff --git a/test/integration/custom-error/pages/index.js b/test/e2e/custom-error/pages/index.js similarity index 100% rename from test/integration/custom-error/pages/index.js rename to test/e2e/custom-error/pages/index.js diff --git a/test/e2e/custom-page-extension/custom-page-extension.test.ts b/test/e2e/custom-page-extension/custom-page-extension.test.ts new file mode 100644 index 000000000000..a859b4d24c9f --- /dev/null +++ b/test/e2e/custom-page-extension/custom-page-extension.test.ts @@ -0,0 +1,19 @@ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +;((isNextDev && process.env.TURBOPACK_BUILD) || + (isNextStart && process.env.TURBOPACK_DEV) + ? describe.skip + : describe)('Custom page extension', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should work with normal page', async () => { + const html = await next.render('/blog') + expect(html).toContain('Blog - CPE') + }) + + it('should work dynamic page', async () => { + const html = await next.render('/blog/nextjs') + expect(html).toContain('Post - nextjs') + }) +}) diff --git a/test/integration/custom-page-extension/next.config.js b/test/e2e/custom-page-extension/next.config.js similarity index 100% rename from test/integration/custom-page-extension/next.config.js rename to test/e2e/custom-page-extension/next.config.js diff --git a/test/integration/custom-page-extension/pages/blog/[pid].page.js b/test/e2e/custom-page-extension/pages/blog/[pid].page.js similarity index 100% rename from test/integration/custom-page-extension/pages/blog/[pid].page.js rename to test/e2e/custom-page-extension/pages/blog/[pid].page.js diff --git a/test/integration/custom-page-extension/pages/blog/index.page.js b/test/e2e/custom-page-extension/pages/blog/index.page.js similarity index 100% rename from test/integration/custom-page-extension/pages/blog/index.page.js rename to test/e2e/custom-page-extension/pages/blog/index.page.js diff --git a/test/e2e/custom-routes-catchall/custom-routes-catchall.test.ts b/test/e2e/custom-routes-catchall/custom-routes-catchall.test.ts new file mode 100644 index 000000000000..9c3f4d7711c8 --- /dev/null +++ b/test/e2e/custom-routes-catchall/custom-routes-catchall.test.ts @@ -0,0 +1,30 @@ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +;((isNextDev && process.env.TURBOPACK_BUILD) || + (isNextStart && process.env.TURBOPACK_DEV) + ? describe.skip + : describe)('Custom routes', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should rewrite and render page correctly', async () => { + const html = await next.render('/docs/hello') + expect(html).toMatch(/hello world/) + }) + + it('should rewrite to /_next/static correctly', async () => { + const bundlePath = `/docs/_next/static/${next.buildId}/_buildManifest.js` + const data = await next.render(bundlePath) + expect(data).toContain('/hello') + }) + + it('should rewrite to public/static correctly', async () => { + const data = await next.render('/docs/static/data.json') + expect(data).toContain('some data...') + }) + + it('should rewrite to public file correctly', async () => { + const data = await next.render('/docs/another.txt') + expect(data).toContain('some text') + }) +}) diff --git a/test/integration/custom-routes-catchall/next.config.js b/test/e2e/custom-routes-catchall/next.config.js similarity index 100% rename from test/integration/custom-routes-catchall/next.config.js rename to test/e2e/custom-routes-catchall/next.config.js diff --git a/test/integration/custom-routes-catchall/pages/hello.js b/test/e2e/custom-routes-catchall/pages/hello.js similarity index 100% rename from test/integration/custom-routes-catchall/pages/hello.js rename to test/e2e/custom-routes-catchall/pages/hello.js diff --git a/test/integration/custom-routes-catchall/public/another.txt b/test/e2e/custom-routes-catchall/public/another.txt similarity index 100% rename from test/integration/custom-routes-catchall/public/another.txt rename to test/e2e/custom-routes-catchall/public/another.txt diff --git a/test/integration/custom-routes-catchall/public/static/data.json b/test/e2e/custom-routes-catchall/public/static/data.json similarity index 100% rename from test/integration/custom-routes-catchall/public/static/data.json rename to test/e2e/custom-routes-catchall/public/static/data.json diff --git a/test/e2e/custom-routes-i18n-index-redirect/custom-routes-i18n-index-redirect.test.ts b/test/e2e/custom-routes-i18n-index-redirect/custom-routes-i18n-index-redirect.test.ts new file mode 100644 index 000000000000..8c9f6ef7b1cc --- /dev/null +++ b/test/e2e/custom-routes-i18n-index-redirect/custom-routes-i18n-index-redirect.test.ts @@ -0,0 +1,36 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('Custom routes i18n with index redirect', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + // Assertions don't apply to deploy mode (output differs vs. local Next.js server). + skipDeployment: true, + }) + if (skipped) return + + it('should respond to default locale redirects correctly for index redirect', async () => { + for (const [path, dest] of [ + ['/', '/destination'], + ['/en', '/destination'], + ['/fr', '/fr/destination'], + ]) { + const res = await next.fetch(path, { + redirect: 'manual', + }) + + expect(res.status).toBe(dest ? 307 : 404) + + if (dest) { + const text = await res.text() + expect(text).toEqual(dest) + if (dest.startsWith('/')) { + const parsed = new URL(res.headers.get('location')) + expect(parsed.pathname).toBe(dest) + expect(parsed.search).toBe('') + } else { + expect(res.headers.get('location')).toBe(dest) + } + } + } + }) +}) diff --git a/test/integration/custom-routes-i18n-index-redirect/next.config.js b/test/e2e/custom-routes-i18n-index-redirect/next.config.js similarity index 100% rename from test/integration/custom-routes-i18n-index-redirect/next.config.js rename to test/e2e/custom-routes-i18n-index-redirect/next.config.js diff --git a/test/integration/custom-routes-i18n-index-redirect/pages/index.js b/test/e2e/custom-routes-i18n-index-redirect/pages/index.js similarity index 100% rename from test/integration/custom-routes-i18n-index-redirect/pages/index.js rename to test/e2e/custom-routes-i18n-index-redirect/pages/index.js diff --git a/test/integration/custom-routes-i18n/test/index.test.ts b/test/e2e/custom-routes-i18n/custom-routes-i18n.test.ts similarity index 61% rename from test/integration/custom-routes-i18n/test/index.test.ts rename to test/e2e/custom-routes-i18n/custom-routes-i18n.test.ts index 23e76336a6b3..89a228d322a6 100644 --- a/test/integration/custom-routes-i18n/test/index.test.ts +++ b/test/e2e/custom-routes-i18n/custom-routes-i18n.test.ts @@ -1,28 +1,47 @@ -/* eslint-env jest */ - import http from 'http' -import { join } from 'path' import cheerio from 'cheerio' -import webdriver from 'next-webdriver' -import { - launchApp, - killApp, - findPort, - nextBuild, - nextStart, - File, - fetchViaHTTP, - check, -} from 'next-test-utils' - -const appDir = join(__dirname, '..') -const nextConfig = new File(join(appDir, 'next.config.js')) -let server -let externalPort -let appPort -let app - -const runTests = () => { +import { findPort, retry } from 'next-test-utils' +import { nextTestSetup, isNextDev } from 'e2e-utils' + +describe('Custom routes i18n', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + if (skipped) return + + let server: http.Server + let externalPort: number + + beforeAll(async () => { + externalPort = await findPort() + server = http.createServer((req, res) => { + res.statusCode = 200 + res.end( + `<p id='data'>${JSON.stringify({ + url: req.url, + })}</p>` + ) + }) + await new Promise<void>((res, rej) => { + server.listen(externalPort, (err?: Error) => (err ? rej(err) : res())) + }) + + await next.patchFile('next.config.js', (content) => + content.replace(/__EXTERNAL_PORT__/g, String(externalPort)) + ) + + if (!isNextDev) { + await next.build() + } + await next.start() + }) + + afterAll(() => { + server.close() + }) + it('should respond to default locale redirects correctly', async () => { for (const [path, dest] of [ ['/redirect-1', '/destination-1'], @@ -31,7 +50,7 @@ const runTests = () => { ['/nl-NL/redirect-2', '/destination-2'], ['/fr/redirect-2', false], ] as const) { - const res = await fetchViaHTTP(appPort, path, undefined, { + const res = await next.fetch(path, { redirect: 'manual', }) @@ -41,7 +60,7 @@ const runTests = () => { const text = await res.text() expect(text).toEqual(dest) if (dest.startsWith('/')) { - const parsed = new URL(res.headers.get('location')) + const parsed = new URL(res.headers.get('location')!) expect(parsed.pathname).toBe(dest) expect(parsed.search).toBe('') } else { @@ -53,7 +72,7 @@ const runTests = () => { it('should rewrite index routes correctly', async () => { for (const path of ['/', '/fr', '/nl-NL']) { - const res = await fetchViaHTTP(appPort, path, undefined, { + const res = await next.fetch(path, { redirect: 'manual', }) expect(res.status).toBe(200) @@ -73,7 +92,7 @@ const runTests = () => { ['/nl-NL/catch-all/hello', '/hello'], ['/fr/catch-all/hello', '/fr/hello'], ]) { - const res = await fetchViaHTTP(appPort, path, undefined, { + const res = await next.fetch(path, { redirect: 'manual', }) expect(res.status).toBe(200) @@ -86,13 +105,13 @@ const runTests = () => { it('should navigate on the client with rewrites correctly', async () => { for (const locale of ['', '/nl-NL', '/fr']) { - const browser = await webdriver(appPort, `${locale}/links`) + const browser = await next.browser(`${locale}/links`) const expectedIndex = locale === '/fr' ? `fr` : '' await browser.elementByCss('#to-about').click() - await check(async () => { + await retry(async () => { const data = JSON.parse( cheerio .load(await browser.eval('document.documentElement.innerHTML'))( @@ -100,11 +119,8 @@ const runTests = () => { ) .text() ) - console.log(data) - return data.url === `${expectedIndex ? '/fr' : ''}/about` - ? 'success' - : 'fail' - }, 'success') + expect(data.url).toBe(`${expectedIndex ? '/fr' : ''}/about`) + }) await browser .back() @@ -112,7 +128,7 @@ const runTests = () => { .elementByCss('#to-catch-all') .click() - await check(async () => { + await retry(async () => { const data = JSON.parse( cheerio .load(await browser.eval('document.documentElement.innerHTML'))( @@ -120,11 +136,8 @@ const runTests = () => { ) .text() ) - console.log(data) - return data.url === `${expectedIndex ? '/fr' : ''}/hello` - ? 'success' - : 'fail' - }, 'success') + expect(data.url).toBe(`${expectedIndex ? '/fr' : ''}/hello`) + }) await browser.back().waitForElementByCss('#links') @@ -132,61 +145,21 @@ const runTests = () => { await browser.elementByCss('#to-index').click() - await check(() => browser.eval('window.location.pathname'), locale || '/') + await retry(async () => { + expect(await browser.eval('window.location.pathname')).toBe( + locale || '/' + ) + }) expect(await browser.eval('window.beforeNav')).toBe(1) await browser.elementByCss('#to-links').click() - await check( - () => browser.eval('window.location.pathname'), - `${locale}/links` - ) + await retry(async () => { + expect(await browser.eval('window.location.pathname')).toBe( + `${locale}/links` + ) + }) expect(await browser.eval('window.beforeNav')).toBe(1) } }) -} - -describe('Custom routes i18n', () => { - beforeAll(async () => { - externalPort = await findPort() - server = http.createServer((req, res) => { - res.statusCode = 200 - res.end( - `<p id='data'>${JSON.stringify({ - url: req.url, - })}</p>` - ) - }) - await new Promise<void>((res, rej) => { - server.listen(externalPort, (err) => (err ? rej(err) : res())) - }) - nextConfig.replace(/__EXTERNAL_PORT__/g, '' + externalPort) - }) - afterAll(async () => { - server.close() - nextConfig.restore() - }) - ;(process.env.TURBOPACK_BUILD ? describe.skip : describe)( - 'development mode', - () => { - beforeAll(async () => { - appPort = await findPort() - app = await launchApp(appDir, appPort) - }) - afterAll(() => killApp(app)) - runTests() - } - ) - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( - 'production mode', - () => { - beforeAll(async () => { - await nextBuild(appDir) - appPort = await findPort() - app = await nextStart(appDir, appPort) - }) - afterAll(() => killApp(app)) - runTests() - } - ) }) diff --git a/test/integration/custom-routes-i18n/next.config.js b/test/e2e/custom-routes-i18n/next.config.js similarity index 100% rename from test/integration/custom-routes-i18n/next.config.js rename to test/e2e/custom-routes-i18n/next.config.js diff --git a/test/integration/custom-routes-i18n/pages/links.js b/test/e2e/custom-routes-i18n/pages/links.js similarity index 100% rename from test/integration/custom-routes-i18n/pages/links.js rename to test/e2e/custom-routes-i18n/pages/links.js diff --git a/test/integration/custom-routes/test/index.test.ts b/test/e2e/custom-routes/custom-routes.test.ts similarity index 57% rename from test/integration/custom-routes/test/index.test.ts rename to test/e2e/custom-routes/custom-routes.test.ts index fe2da621c0df..8d8fe6f7713b 100644 --- a/test/integration/custom-routes/test/index.test.ts +++ b/test/e2e/custom-routes/custom-routes.test.ts @@ -1,44 +1,83 @@ -/* eslint-env jest */ - import http from 'http' import stripAnsi from 'strip-ansi' -import fs from 'fs-extra' -import { join } from 'path' -import WebSocket from 'ws' import cheerio from 'cheerio' -import webdriver from 'next-webdriver' +import WebSocket from 'ws' import { waitForNoRedbox, - launchApp, - killApp, findPort, - nextBuild, - nextStart, - fetchViaHTTP, - File, - renderViaHTTP, getBrowserBodyText, waitFor, normalizeRegEx, - check, normalizeManifest, + retry, } from 'next-test-utils' +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' + +describe('Custom routes', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + disableAutoSkewProtection: true, + skipDeployment: true, + }) + if (skipped) return + + let externalServerPort: number + let externalServer: http.Server + let externalServerHits: Set<string> + let buildId: string + let buildCliOutput: string = '' + + beforeEach(() => { + externalServerHits = new Set() + }) + + beforeAll(async () => { + externalServerPort = await findPort() + externalServer = http.createServer((req, res) => { + externalServerHits.add(req.url) + const nextHost = req.headers['x-forwarded-host'] + const externalHost = req.headers['host'] + res.end(`hi ${nextHost} from ${externalHost}`) + }) + const wsServer = new WebSocket.Server({ noServer: true }) + + externalServer.on('upgrade', (req, socket, head) => { + externalServerHits.add(req.url) + wsServer.handleUpgrade(req, socket, head, (client) => { + client.send('hello world') + }) + }) + await new Promise<void>((resolve, reject) => { + const onError = (error: Error) => { + reject(error) + } + externalServer.once('error', onError) + externalServer.listen(externalServerPort, () => { + externalServer.off('error', onError) + resolve() + }) + }) + + await next.patchFile('next.config.js', (content) => + content.replace(/__EXTERNAL_PORT__/g, String(externalServerPort)) + ) + + if (!isNextDev) { + const result = await next.build({ args: ['-d'] }) + buildCliOutput = result.cliOutput || '' + buildId = (await next.readFile('.next/BUILD_ID')).trim() + await next.start({ skipBuild: true }) + } else { + buildId = 'development' + await next.start() + } + }) + + afterAll(() => { + externalServer.close() + }) -let appDir = join(__dirname, '..') -const nextConfig = new File(join(appDir, 'next.config.js')) -const nextConfigPath = join(appDir, 'next.config.js') -let externalServerHits = new Set<string>() -let nextConfigRestoreContent -let nextConfigContent -let externalServerPort -let externalServer -let stdout = '' -let stderr = '' -let buildId -let appPort -let app - -const runTests = (isDev = false) => { it.each([ { path: '/to-ANOTHER', @@ -63,9 +102,7 @@ const runTests = (isDev = false) => { ])( 'should honor caseSensitiveRoutes config for $path', async ({ path, status, content }) => { - const res = await fetchViaHTTP(appPort, path, undefined, { - redirect: 'manual', - }) + const res = await next.fetch(path, { redirect: 'manual' }) if (status) { expect(res.status).toBe(status) @@ -80,7 +117,7 @@ const runTests = (isDev = false) => { it('should successfully rewrite a WebSocket request', async () => { const messages = [] const ws = await new Promise<WebSocket>((resolve, reject) => { - let socket = new WebSocket(`ws://localhost:${appPort}/to-websocket`) + let socket = new WebSocket(`ws://localhost:${next.appPort}/to-websocket`) socket.on('message', (data) => { messages.push(data.toString()) }) @@ -92,10 +129,9 @@ const runTests = (isDev = false) => { }) }) - await check( - () => (messages.length > 0 ? 'success' : JSON.stringify(messages)), - 'success' - ) + await retry(() => { + expect(messages.length).toBeGreaterThan(0) + }) ws.close() expect([...externalServerHits]).toEqual(['/_next/hmr?page=/about']) }) @@ -105,7 +141,7 @@ const runTests = (isDev = false) => { try { const ws = await new Promise<WebSocket>((resolve, reject) => { let socket = new WebSocket( - `ws://localhost:${appPort}/websocket-to-page` + `ws://localhost:${next.appPort}/websocket-to-page` ) socket.on('message', (data) => { messages.push(data.toString()) @@ -121,16 +157,15 @@ const runTests = (isDev = false) => { } catch (err) { messages.push(err) } - expect(stderr).not.toContain('unhandledRejection') + expect(next.cliOutput).not.toContain('unhandledRejection') }) it('should not rewrite for _next/data route when a match is found', async () => { - const initial = await fetchViaHTTP(appPort, '/overridden/first') + const initial = await next.fetch('/overridden/first') expect(initial.status).toBe(200) expect(await initial.text()).toContain('this page is overridden') - const nextData = await fetchViaHTTP( - appPort, + const nextData = await next.fetch( `/_next/data/${buildId}/overridden/first.json` ) expect(nextData.status).toBe(200) @@ -160,14 +195,9 @@ const runTests = (isDev = false) => { }, ]) { const { post } = expected - const res = await fetchViaHTTP( - appPort, - '/has-rewrite-8', - `?post=${post}`, - { - redirect: 'manual', - } - ) + const res = await next.fetch(`/has-rewrite-8?post=${post}`, { + redirect: 'manual', + }) expect(res.status).toBe(200) @@ -181,7 +211,7 @@ const runTests = (isDev = false) => { }) it('should handle external beforeFiles rewrite correctly', async () => { - const res = await fetchViaHTTP(appPort, '/overridden') + const res = await next.fetch('/overridden') const html = await res.text() if (res.status !== 200) { @@ -190,16 +220,17 @@ const runTests = (isDev = false) => { expect(res.status).toBe(200) expect(html).toContain('Example Domain') - const browser = await webdriver(appPort, '/nav') + const browser = await next.browser('/nav') await browser.elementByCss('#to-before-files-overridden').click() - await check( - () => browser.eval('document.documentElement.innerHTML'), - /Example Domain/ - ) + await retry(async () => { + expect(await browser.eval('document.documentElement.innerHTML')).toMatch( + /Example Domain/ + ) + }) }) it('should handle beforeFiles rewrite to dynamic route correctly', async () => { - const res = await fetchViaHTTP(appPort, '/nfl') + const res = await next.fetch('/nfl') const html = await res.text() if (res.status !== 200) { @@ -208,13 +239,14 @@ const runTests = (isDev = false) => { expect(res.status).toBe(200) expect(html).toContain('/_sport/[slug]') - const browser = await webdriver(appPort, '/nav') + const browser = await next.browser('/nav') await browser.eval('window.beforeNav = 1') await browser.elementByCss('#to-before-files-dynamic').click() - await check( - () => browser.eval('document.documentElement.innerHTML'), - /_sport\/\[slug\]/ - ) + await retry(async () => { + expect(await browser.eval('document.documentElement.innerHTML')).toMatch( + /_sport\/\[slug\]/ + ) + }) expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ slug: 'nfl', }) @@ -225,7 +257,7 @@ const runTests = (isDev = false) => { }) it('should handle beforeFiles rewrite to partly dynamic route correctly', async () => { - const res = await fetchViaHTTP(appPort, '/nfl') + const res = await next.fetch('/nfl') const html = await res.text() if (res.status !== 200) { @@ -234,13 +266,14 @@ const runTests = (isDev = false) => { expect(res.status).toBe(200) expect(html).toContain('/_sport/[slug]') - const browser = await webdriver(appPort, '/nav') + const browser = await next.browser('/nav') await browser.eval('window.beforeNav = 1') await browser.elementByCss('#to-before-files-dynamic-again').click() - await check( - () => browser.eval('document.documentElement.innerHTML'), - /_sport\/\[slug\]\/test/ - ) + await retry(async () => { + expect(await browser.eval('document.documentElement.innerHTML')).toMatch( + /_sport\/\[slug\]\/test/ + ) + }) expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ slug: 'nfl', }) @@ -251,8 +284,7 @@ const runTests = (isDev = false) => { }) it('should support long URLs for rewrites', async () => { - const res = await fetchViaHTTP( - appPort, + const res = await next.fetch( '/catchall-rewrite/a9btBxtHQALZ6cxfuj18X6OLGNSkJVzrOXz41HG4QwciZfn7ggRZzPx21dWqGiTBAqFRiWvVNm5ko2lpyso5jtVaXg88dC1jKfqI2qmIcdeyJat8xamrIh2LWnrYRrsBcoKfQU65KHod8DPANuzPS3fkVYWlmov05GQbc82HwR1exOvPVKUKb5gBRWiN0WOh7hN4QyezIuq3dJINAptFQ6m2bNGjYACBRk4MOSHdcQG58oq5Ch7luuqrl9EcbWSa' ) @@ -262,7 +294,7 @@ const runTests = (isDev = false) => { }) it('should resolveHref correctly navigating through history', async () => { - const browser = await webdriver(appPort, '/') + const browser = await next.browser('/') await browser.eval('window.beforeNav = 1') expect(await browser.eval('document.documentElement.innerHTML')).toContain( @@ -292,13 +324,13 @@ const runTests = (isDev = false) => { }) expect(await browser.eval('window.beforeNav')).toBe(1) - if (isDev) { + if (isNextDev) { await waitForNoRedbox(browser) } }) it('should continue in beforeFiles rewrites', async () => { - const res = await fetchViaHTTP(appPort, '/old-blog/about') + const res = await next.fetch('/old-blog/about') expect(res.status).toBe(200) const html = await res.text() @@ -306,7 +338,7 @@ const runTests = (isDev = false) => { expect($('#hello').text()).toContain('Hello') - const browser = await webdriver(appPort, '/nav') + const browser = await next.browser('/nav') await browser.eval('window.beforeNav = 1') await browser @@ -317,7 +349,7 @@ const runTests = (isDev = false) => { }) it('should not hang when proxy rewrite fails', async () => { - const res = await fetchViaHTTP(appPort, '/to-nowhere', undefined, { + const res = await next.fetch('/to-nowhere', { timeout: 5000, }) @@ -325,11 +357,12 @@ const runTests = (isDev = false) => { }) it('should parse params correctly for rewrite to auto-export dynamic page', async () => { - const browser = await webdriver(appPort, '/rewriting-to-auto-export') - await check( - () => browser.eval(() => document.documentElement.innerHTML), - /auto-export.*?hello/ - ) + const browser = await next.browser('/rewriting-to-auto-export') + await retry(async () => { + expect( + await browser.eval(() => document.documentElement.innerHTML) + ).toMatch(/auto-export.*?hello/) + }) expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ rewrite: '1', slug: 'hello', @@ -337,8 +370,7 @@ const runTests = (isDev = false) => { }) it('should provide params correctly for rewrite to auto-export non-dynamic page', async () => { - const browser = await webdriver( - appPort, + const browser = await next.browser( '/rewriting-to-another-auto-export/first' ) @@ -352,17 +384,17 @@ const runTests = (isDev = false) => { }) it('should handle one-to-one rewrite successfully', async () => { - const html = await renderViaHTTP(appPort, '/first') + const html = await next.render('/first') expect(html).toMatch(/hello/) }) it('should handle chained rewrites successfully', async () => { - const html = await renderViaHTTP(appPort, '/') + const html = await next.render('/') expect(html).toMatch(/multi-rewrites/) }) it('should handle param like headers properly', async () => { - const res = await fetchViaHTTP(appPort, '/my-other-header/my-path') + const res = await next.fetch('/my-other-header/my-path') expect(res.headers.get('x-path')).toBe('my-path') expect(res.headers.get('somemy-path')).toBe('hi') expect(res.headers.get('x-test')).toBe('some:value*') @@ -380,7 +412,7 @@ const runTests = (isDev = false) => { }) it('should not match dynamic route immediately after applying header', async () => { - const res = await fetchViaHTTP(appPort, '/blog/post-321') + const res = await next.fetch('/blog/post-321') expect(res.headers.get('x-something')).toBe('applied-everywhere') const $ = cheerio.load(await res.text()) @@ -388,25 +420,19 @@ const runTests = (isDev = false) => { }) it('should handle chained redirects successfully', async () => { - const res1 = await fetchViaHTTP(appPort, '/redir-chain1', undefined, { - redirect: 'manual', - }) + const res1 = await next.fetch('/redir-chain1', { redirect: 'manual' }) const res1location = new URL(res1.headers.get('location'), res1.url) .pathname expect(res1.status).toBe(301) expect(res1location).toBe('/redir-chain2') - const res2 = await fetchViaHTTP(appPort, res1location, undefined, { - redirect: 'manual', - }) + const res2 = await next.fetch(res1location, { redirect: 'manual' }) const res2location = new URL(res2.headers.get('location'), res2.url) .pathname expect(res2.status).toBe(302) expect(res2location).toBe('/redir-chain3') - const res3 = await fetchViaHTTP(appPort, res2location, undefined, { - redirect: 'manual', - }) + const res3 = await next.fetch(res2location, { redirect: 'manual' }) const res3location = new URL(res3.headers.get('location'), res3.url) .pathname expect(res3.status).toBe(303) @@ -414,44 +440,32 @@ const runTests = (isDev = false) => { }) it('should not match redirect for /_next', async () => { - const res = await fetchViaHTTP( - appPort, - '/_next/has-redirect-5', - undefined, - { - headers: { - 'x-test-next': 'true', - }, - redirect: 'manual', - } - ) + const res = await next.fetch('/_next/has-redirect-5', { + headers: { + 'x-test-next': 'true', + }, + redirect: 'manual', + }) expect(res.status).toBe(404) - const res2 = await fetchViaHTTP( - appPort, - '/another/has-redirect-5', - undefined, - { - headers: { - 'x-test-next': 'true', - }, - redirect: 'manual', - } - ) + const res2 = await next.fetch('/another/has-redirect-5', { + headers: { + 'x-test-next': 'true', + }, + redirect: 'manual', + }) expect(res2.status).toBe(307) }) it('should redirect successfully with permanent: false', async () => { - const res = await fetchViaHTTP(appPort, '/redirect1', undefined, { - redirect: 'manual', - }) + const res = await next.fetch('/redirect1', { redirect: 'manual' }) const { pathname } = new URL(res.headers.get('location'), res.url) expect(res.status).toBe(307) expect(pathname).toBe('/') }) it('should redirect with params successfully', async () => { - const res = await fetchViaHTTP(appPort, '/hello/123/another', undefined, { + const res = await next.fetch('/hello/123/another', { redirect: 'manual', }) const { pathname } = new URL(res.headers.get('location'), res.url) @@ -460,14 +474,9 @@ const runTests = (isDev = false) => { }) it('should redirect with hash successfully', async () => { - const res = await fetchViaHTTP( - appPort, - '/docs/router-status/500', - undefined, - { - redirect: 'manual', - } - ) + const res = await next.fetch('/docs/router-status/500', { + redirect: 'manual', + }) const { pathname, hash, search } = new URL( res.headers.get('location'), res.url @@ -479,9 +488,7 @@ const runTests = (isDev = false) => { }) it('should redirect successfully with provided statusCode', async () => { - const res = await fetchViaHTTP(appPort, '/redirect2', undefined, { - redirect: 'manual', - }) + const res = await next.fetch('/redirect2', { redirect: 'manual' }) const { pathname, search } = new URL(res.headers.get('location'), res.url) expect(res.status).toBe(301) expect(pathname).toBe('/') @@ -489,14 +496,9 @@ const runTests = (isDev = false) => { }) it('should redirect successfully with catchall', async () => { - const res = await fetchViaHTTP( - appPort, - '/catchall-redirect/hello/world', - undefined, - { - redirect: 'manual', - } - ) + const res = await next.fetch('/catchall-redirect/hello/world', { + redirect: 'manual', + }) const { pathname, search } = new URL(res.headers.get('location'), res.url) expect(res.status).toBe(307) expect(pathname).toBe('/somewhere') @@ -504,28 +506,28 @@ const runTests = (isDev = false) => { }) it('should server static files through a rewrite', async () => { - const text = await renderViaHTTP(appPort, '/hello-world') + const text = await next.render('/hello-world') expect(text).toBe('hello world!') }) it('should rewrite with params successfully', async () => { - const html = await renderViaHTTP(appPort, '/test/hello') + const html = await next.render('/test/hello') expect(html).toMatch(/Hello/) }) it('should not append params when one is used in destination path', async () => { - const html = await renderViaHTTP(appPort, '/test/with-params?a=b') + const html = await next.render('/test/with-params?a=b') const $ = cheerio.load(html) expect(JSON.parse($('p').text())).toEqual({ a: 'b' }) }) it('should double redirect successfully', async () => { - const html = await renderViaHTTP(appPort, '/docs/github') + const html = await next.render('/docs/github') expect(html).toMatch(/hi there/) }) it('should allow params in query for rewrite', async () => { - const html = await renderViaHTTP(appPort, '/query-rewrite/hello/world?a=b') + const html = await next.render('/query-rewrite/hello/world?a=b') const $ = cheerio.load(html) expect(JSON.parse($('#__NEXT_DATA__').html()).query).toEqual({ first: 'hello', @@ -537,10 +539,7 @@ const runTests = (isDev = false) => { }) it('should have correct params for catchall rewrite', async () => { - const html = await renderViaHTTP( - appPort, - '/catchall-rewrite/hello/world?a=b' - ) + const html = await next.render('/catchall-rewrite/hello/world?a=b') const $ = cheerio.load(html) expect(JSON.parse($('#__NEXT_DATA__').html()).query).toEqual({ a: 'b', @@ -549,8 +548,7 @@ const runTests = (isDev = false) => { }) it('should have correct encoding for params with catchall rewrite', async () => { - const html = await renderViaHTTP( - appPort, + const html = await next.render( '/catchall-rewrite/hello%20world%3Fw%3D24%26focalpoint%3Dcenter?a=b' ) const $ = cheerio.load(html) @@ -561,7 +559,7 @@ const runTests = (isDev = false) => { }) it('should have correct query for catchall rewrite', async () => { - const html = await renderViaHTTP(appPort, '/catchall-query/hello/world?a=b') + const html = await next.render('/catchall-query/hello/world?a=b') const $ = cheerio.load(html) expect(JSON.parse($('#__NEXT_DATA__').html()).query).toEqual({ a: 'b', @@ -571,20 +569,15 @@ const runTests = (isDev = false) => { }) it('should have correct header for catchall rewrite', async () => { - const res = await fetchViaHTTP(appPort, '/catchall-header/hello/world?a=b') + const res = await next.fetch('/catchall-header/hello/world?a=b') const headerValue = res.headers.get('x-value') expect(headerValue).toBe('hello/world') }) it('should allow params in query for redirect', async () => { - const res = await fetchViaHTTP( - appPort, - '/query-redirect/hello/world?a=b', - undefined, - { - redirect: 'manual', - } - ) + const res = await next.fetch('/query-redirect/hello/world?a=b', { + redirect: 'manual', + }) const { pathname, searchParams } = new URL( res.headers.get('location'), res.url @@ -599,13 +592,9 @@ const runTests = (isDev = false) => { }) it('should have correctly encoded params in query for redirect', async () => { - const res = await fetchViaHTTP( - appPort, + const res = await next.fetch( '/query-redirect/hello%20world%3Fw%3D24%26focalpoint%3Dcenter/world?a=b', - undefined, - { - redirect: 'manual', - } + { redirect: 'manual' } ) const { pathname, searchParams } = new URL( res.headers.get('location'), @@ -614,7 +603,6 @@ const runTests = (isDev = false) => { expect(res.status).toBe(307) expect(pathname).toBe('/with-params') expect(Object.fromEntries(searchParams)).toEqual({ - // this should be decoded since new URL decodes query values first: 'hello world?w=24&focalpoint=center', second: 'world', a: 'b', @@ -622,20 +610,14 @@ const runTests = (isDev = false) => { }) it('should overwrite param values correctly', async () => { - const html = await renderViaHTTP(appPort, '/test-overwrite/first/second') + const html = await next.render('/test-overwrite/first/second') expect(html).toMatch(/this-should-be-the-value/) expect(html).not.toMatch(/first/) expect(html).toMatch(/second/) }) it('should handle query for rewrite correctly', async () => { - // query merge order lowest priority to highest - // 1. initial URL query values - // 2. path segment values - // 3. destination specified query values - - const html = await renderViaHTTP( - appPort, + const html = await next.render( '/query-rewrite/first/second?section=overridden&name=overridden&first=overridden&second=overridden&keep=me' ) @@ -649,24 +631,20 @@ const runTests = (isDev = false) => { }) }) - // current routes order do not allow rewrites to override page - // but allow redirects to it('should not allow rewrite to override page file', async () => { - const html = await renderViaHTTP(appPort, '/nav') + const html = await next.render('/nav') expect(html).toContain('to-hello') }) it('show allow redirect to override the page', async () => { - const res = await fetchViaHTTP(appPort, '/redirect-override', undefined, { - redirect: 'manual', - }) + const res = await next.fetch('/redirect-override', { redirect: 'manual' }) const { pathname } = new URL(res.headers.get('location') || '', res.url) expect(res.status).toBe(307) expect(pathname).toBe('/thank-you-next') }) it('should work successfully on the client', async () => { - const browser = await webdriver(appPort, '/nav') + const browser = await next.browser('/nav') await browser.elementByCss('#to-hello').click() await browser.waitForElementByCss('#hello') @@ -693,7 +671,7 @@ const runTests = (isDev = false) => { }) it('should work with rewrite when manually specifying href/as', async () => { - const browser = await webdriver(appPort, '/nav') + const browser = await next.browser('/nav') await browser.eval('window.beforeNav = 1') await browser .elementByCss('#to-params-manual') @@ -709,7 +687,7 @@ const runTests = (isDev = false) => { }) it('should work with rewrite when only specifying href', async () => { - const browser = await webdriver(appPort, '/nav') + const browser = await next.browser('/nav') await browser.eval('window.beforeNav = 1') await browser .elementByCss('#to-params') @@ -725,7 +703,7 @@ const runTests = (isDev = false) => { }) it('should work with rewrite when only specifying href and ends in dynamic route', async () => { - const browser = await webdriver(appPort, '/nav') + const browser = await next.browser('/nav') await browser.eval('window.beforeNav = 1') await browser .elementByCss('#to-rewritten-dynamic') @@ -739,81 +717,78 @@ const runTests = (isDev = false) => { }) it('should match a page after a rewrite', async () => { - const html = await renderViaHTTP(appPort, '/to-hello') + const html = await next.render('/to-hello') expect(html).toContain('Hello') }) it('should match dynamic route after rewrite', async () => { - const html = await renderViaHTTP(appPort, '/blog/post-1') + const html = await next.render('/blog/post-1') expect(html).toMatch(/post:.*?post-2/) }) it('should match public file after rewrite', async () => { - const data = await renderViaHTTP(appPort, '/blog/data.json') + const data = await next.render('/blog/data.json') expect(JSON.parse(data)).toEqual({ hello: 'world' }) }) it('should match /_next file after rewrite', async () => { - await renderViaHTTP(appPort, '/hello') - const data = await renderViaHTTP( - appPort, + await next.render('/hello') + const data = await next.render( `/hidden/_next/static/${buildId}/_buildManifest.js` ) expect(data).toContain('/hello') }) it('should allow redirecting to external resource', async () => { - const res = await fetchViaHTTP(appPort, '/to-external', undefined, { - redirect: 'manual', - }) + const res = await next.fetch('/to-external', { redirect: 'manual' }) const location = res.headers.get('location') expect(res.status).toBe(307) expect(location).toBe('https://google.com/') }) it('should apply headers for exact match', async () => { - const res = await fetchViaHTTP(appPort, '/add-header') + const res = await next.fetch('/add-header') expect(res.headers.get('x-custom-header')).toBe('hello world') expect(res.headers.get('x-another-header')).toBe('hello again') }) it('should apply headers for multi match', async () => { - const res = await fetchViaHTTP(appPort, '/my-headers/first') + const res = await next.fetch('/my-headers/first') expect(res.headers.get('x-first-header')).toBe('first') expect(res.headers.get('x-second-header')).toBe('second') }) it('should apply params for header key/values', async () => { - const res = await fetchViaHTTP(appPort, '/my-other-header/first') + const res = await next.fetch('/my-other-header/first') expect(res.headers.get('x-path')).toBe('first') expect(res.headers.get('somefirst')).toBe('hi') }) it('should support URL for header key/values', async () => { - const res = await fetchViaHTTP(appPort, '/without-params/url') + const res = await next.fetch('/without-params/url') expect(res.headers.get('x-origin')).toBe('https://example.com') }) it('should apply params header key/values with URL', async () => { - const res = await fetchViaHTTP(appPort, '/with-params/url/first') + const res = await next.fetch('/with-params/url/first') expect(res.headers.get('x-url')).toBe('https://example.com/first') }) it('should apply params header key/values with URL that has port', async () => { - const res = await fetchViaHTTP(appPort, '/with-params/url2/first') + const res = await next.fetch('/with-params/url2/first') expect(res.headers.get('x-url')).toBe( 'https://example.com:8080?hello=first' ) }) it('should support named pattern for header key/values', async () => { - const res = await fetchViaHTTP(appPort, '/named-pattern/hello') + const res = await next.fetch('/named-pattern/hello') expect(res.headers.get('x-something')).toBe('value=hello') expect(res.headers.get('path-hello')).toBe('end') }) it('should support proxying to external resource', async () => { - const res = await fetchViaHTTP(appPort, '/proxy-me/first?keep=me&and=me') + const res = await next.fetch('/proxy-me/first?keep=me&and=me') expect(res.status).toBe(200) expect( [...externalServerHits].map((u) => { @@ -832,13 +807,13 @@ const runTests = (isDev = false) => { }, }, ]) - const nextHost = `localhost:${appPort}` + const nextHost = `localhost:${next.appPort}` const externalHost = `localhost:${externalServerPort}` expect(await res.text()).toContain(`hi ${nextHost} from ${externalHost}`) }) it('should support unnamed parameters correctly', async () => { - const res = await fetchViaHTTP(appPort, '/unnamed/first/final', undefined, { + const res = await next.fetch('/unnamed/first/final', { redirect: 'manual', }) const { pathname } = new URL(res.headers.get('location') || '', res.url) @@ -847,36 +822,24 @@ const runTests = (isDev = false) => { }) it('should support named like unnamed parameters correctly', async () => { - const res = await fetchViaHTTP( - appPort, - '/named-like-unnamed/first', - undefined, - { - redirect: 'manual', - } - ) + const res = await next.fetch('/named-like-unnamed/first', { + redirect: 'manual', + }) const { pathname } = new URL(res.headers.get('location') || '', res.url) expect(res.status).toBe(307) expect(pathname).toBe('/first') }) it('should add refresh header for 308 redirect', async () => { - const res = await fetchViaHTTP(appPort, '/redirect4', undefined, { - redirect: 'manual', - }) + const res = await next.fetch('/redirect4', { redirect: 'manual' }) expect(res.status).toBe(308) expect(res.headers.get('refresh')).toBe(`0;url=/`) }) it('should have correctly encoded query in location and refresh headers', async () => { - const res = await fetchViaHTTP( - appPort, - // Query unencoded is ?テスト=あ + const res = await next.fetch( '/redirect4?%E3%83%86%E3%82%B9%E3%83%88=%E3%81%82', - undefined, - { - redirect: 'manual', - } + { redirect: 'manual' } ) expect(res.status).toBe(308) @@ -889,32 +852,28 @@ const runTests = (isDev = false) => { }) it('should handle basic api rewrite successfully', async () => { - const data = await renderViaHTTP(appPort, '/api-hello') + const data = await next.render('/api-hello') expect(JSON.parse(data)).toEqual({ query: {} }) }) it('should handle api rewrite with un-named param successfully', async () => { - const data = await renderViaHTTP(appPort, '/api-hello-regex/hello/world') + const data = await next.render('/api-hello-regex/hello/world') expect(JSON.parse(data)).toEqual({ query: { name: 'hello/world', first: 'hello/world' }, }) }) it('should handle api rewrite with param successfully', async () => { - const data = await renderViaHTTP(appPort, '/api-hello-param/hello') + const data = await next.render('/api-hello-param/hello') expect(JSON.parse(data)).toEqual({ query: { name: 'hello', hello: 'hello' }, }) }) it('should handle encoded value in the pathname correctly', async () => { - const res = await fetchViaHTTP( - appPort, + const res = await next.fetch( '/redirect/me/to-about/' + encodeURI('\\google.com'), - undefined, - { - redirect: 'manual', - } + { redirect: 'manual' } ) const { pathname, hostname, searchParams } = new URL( @@ -928,8 +887,7 @@ const runTests = (isDev = false) => { }) it('should handle unnamed parameters with multi-match successfully', async () => { - const html = await renderViaHTTP( - appPort, + const html = await next.render( '/unnamed-params/nested/first/second/hello/world' ) const params = JSON.parse(cheerio.load(html)('p').text()) @@ -937,28 +895,18 @@ const runTests = (isDev = false) => { }) it('should handle named regex parameters with multi-match successfully', async () => { - const res = await fetchViaHTTP( - appPort, - '/docs/integrations/v2-some/thing', - undefined, - { - redirect: 'manual', - } - ) + const res = await next.fetch('/docs/integrations/v2-some/thing', { + redirect: 'manual', + }) const { pathname } = new URL(res.headers.get('location') || '', res.url) expect(res.status).toBe(307) expect(pathname).toBe('/integrations/-some/thing') }) it('should redirect with URL in query correctly', async () => { - const res = await fetchViaHTTP( - appPort, - '/to-external-with-query', - undefined, - { - redirect: 'manual', - } - ) + const res = await next.fetch('/to-external-with-query', { + redirect: 'manual', + }) expect(res.status).toBe(307) expect(res.headers.get('location')).toBe( @@ -967,14 +915,9 @@ const runTests = (isDev = false) => { }) it('should redirect with URL in query correctly non-encoded', async () => { - const res = await fetchViaHTTP( - appPort, - '/to-external-with-query', - undefined, - { - redirect: 'manual', - } - ) + const res = await next.fetch('/to-external-with-query', { + redirect: 'manual', + }) expect(res.status).toBe(307) expect(res.headers.get('location')).toBe( @@ -983,7 +926,7 @@ const runTests = (isDev = false) => { }) it('should match missing header headers correctly', async () => { - const res = await fetchViaHTTP(appPort, '/missing-headers-1', undefined, { + const res = await next.fetch('/missing-headers-1', { headers: { 'x-my-header': 'hello world!!', }, @@ -991,27 +934,25 @@ const runTests = (isDev = false) => { expect(res.status).toBe(404) - const res2 = await fetchViaHTTP(appPort, '/missing-headers-1', undefined, { + const res2 = await next.fetch('/missing-headers-1', { redirect: 'manual', }) expect(res2.headers.get('x-new-header')).toBe('new-value') }) it('should match missing query headers correctly', async () => { - const res = await fetchViaHTTP(appPort, '/missing-headers-2', { - 'my-query': 'hellooo', - }) + const res = await next.fetch('/missing-headers-2?my-query=hellooo') expect(res.status).toBe(404) - const res2 = await fetchViaHTTP(appPort, '/missing-headers-2', undefined, { + const res2 = await next.fetch('/missing-headers-2', { redirect: 'manual', }) expect(res2.headers.get('x-new-header')).toBe('new-value') }) it('should match missing cookie headers correctly', async () => { - const res = await fetchViaHTTP(appPort, '/missing-headers-3', undefined, { + const res = await next.fetch('/missing-headers-3', { headers: { cookie: 'loggedIn=true', }, @@ -1020,14 +961,14 @@ const runTests = (isDev = false) => { expect(res.status).toBe(404) - const res2 = await fetchViaHTTP(appPort, '/missing-headers-3', undefined, { + const res2 = await next.fetch('/missing-headers-3', { redirect: 'manual', }) expect(res2.headers.get('x-new-header')).toBe('new-value') }) it('should match missing header redirect correctly', async () => { - const res = await fetchViaHTTP(appPort, '/missing-rewrite-1', undefined, { + const res = await next.fetch('/missing-rewrite-1', { headers: { 'x-my-header': 'hello world!!', }, @@ -1035,7 +976,7 @@ const runTests = (isDev = false) => { expect(res.status).toBe(404) - const res2 = await fetchViaHTTP(appPort, '/missing-redirect-1', undefined, { + const res2 = await next.fetch('/missing-redirect-1', { redirect: 'manual', }) expect(res2.status).toBe(307) @@ -1044,13 +985,11 @@ const runTests = (isDev = false) => { }) it('should match missing query redirect correctly', async () => { - const res = await fetchViaHTTP(appPort, '/missing-redirect-2', { - 'my-query': 'hellooo', - }) + const res = await next.fetch('/missing-redirect-2?my-query=hellooo') expect(res.status).toBe(404) - const res2 = await fetchViaHTTP(appPort, '/missing-redirect-2', undefined, { + const res2 = await next.fetch('/missing-redirect-2', { redirect: 'manual', }) expect(res2.status).toBe(307) @@ -1059,7 +998,7 @@ const runTests = (isDev = false) => { }) it('should match missing cookie redirect correctly', async () => { - const res = await fetchViaHTTP(appPort, '/missing-redirect-3', undefined, { + const res = await next.fetch('/missing-redirect-3', { headers: { cookie: 'loggedIn=true', }, @@ -1068,7 +1007,7 @@ const runTests = (isDev = false) => { expect(res.status).toBe(404) - const res2 = await fetchViaHTTP(appPort, '/missing-redirect-3', undefined, { + const res2 = await next.fetch('/missing-redirect-3', { redirect: 'manual', }) expect(res2.status).toBe(307) @@ -1078,7 +1017,7 @@ const runTests = (isDev = false) => { }) it('should match missing header rewrite correctly', async () => { - const res = await fetchViaHTTP(appPort, '/missing-rewrite-1', undefined, { + const res = await next.fetch('/missing-rewrite-1', { headers: { 'x-my-header': 'hello world!!', }, @@ -1086,7 +1025,7 @@ const runTests = (isDev = false) => { expect(res.status).toBe(404) - const res2 = await fetchViaHTTP(appPort, '/missing-rewrite-1') + const res2 = await next.fetch('/missing-rewrite-1') const $2 = cheerio.load(await res2.text()) expect(res2.status).toBe(200) @@ -1094,20 +1033,18 @@ const runTests = (isDev = false) => { }) it('should match missing query rewrite correctly', async () => { - const res = await fetchViaHTTP(appPort, '/missing-rewrite-2', { - 'my-query': 'hellooo', - }) + const res = await next.fetch('/missing-rewrite-2?my-query=hellooo') expect(res.status).toBe(404) - const res2 = await fetchViaHTTP(appPort, '/missing-rewrite-2') + const res2 = await next.fetch('/missing-rewrite-2') const $2 = cheerio.load(await res2.text()) expect(res2.status).toBe(200) expect(JSON.parse($2('#query').text())).toEqual({}) }) it('should match missing cookie rewrite correctly', async () => { - const res = await fetchViaHTTP(appPort, '/missing-rewrite-3', undefined, { + const res = await next.fetch('/missing-rewrite-3', { headers: { cookie: 'loggedIn=true', }, @@ -1115,7 +1052,7 @@ const runTests = (isDev = false) => { expect(res.status).toBe(404) - const res2 = await fetchViaHTTP(appPort, '/missing-rewrite-3') + const res2 = await next.fetch('/missing-rewrite-3') const $2 = cheerio.load(await res2.text()) expect(JSON.parse($2('#query').text())).toEqual({ authorized: '1', @@ -1124,7 +1061,7 @@ const runTests = (isDev = false) => { }) it('should match has header rewrite correctly', async () => { - const res = await fetchViaHTTP(appPort, '/has-rewrite-1', undefined, { + const res = await next.fetch('/has-rewrite-1', { headers: { 'x-my-header': 'hello world!!', }, @@ -1137,14 +1074,12 @@ const runTests = (isDev = false) => { myHeader: 'hello world!!', }) - const res2 = await fetchViaHTTP(appPort, '/has-rewrite-1') + const res2 = await next.fetch('/has-rewrite-1') expect(res2.status).toBe(404) }) it('should match has query rewrite correctly', async () => { - const res = await fetchViaHTTP(appPort, '/has-rewrite-2', { - 'my-query': 'hellooo', - }) + const res = await next.fetch('/has-rewrite-2?my-query=hellooo') expect(res.status).toBe(200) const $ = cheerio.load(await res.text()) @@ -1155,12 +1090,12 @@ const runTests = (isDev = false) => { value: 'hellooo', }) - const res2 = await fetchViaHTTP(appPort, '/has-rewrite-2') + const res2 = await next.fetch('/has-rewrite-2') expect(res2.status).toBe(404) }) it('should match has cookie rewrite correctly', async () => { - const res = await fetchViaHTTP(appPort, '/has-rewrite-3', undefined, { + const res = await next.fetch('/has-rewrite-3', { headers: { cookie: 'loggedIn=true', }, @@ -1174,15 +1109,15 @@ const runTests = (isDev = false) => { authorized: '1', }) - const res2 = await fetchViaHTTP(appPort, '/has-rewrite-3') + const res2 = await next.fetch('/has-rewrite-3') expect(res2.status).toBe(404) }) it('should match has host rewrite correctly', async () => { - const res1 = await fetchViaHTTP(appPort, '/has-rewrite-4') + const res1 = await next.fetch('/has-rewrite-4') expect(res1.status).toBe(404) - const res = await fetchViaHTTP(appPort, '/has-rewrite-4', undefined, { + const res = await next.fetch('/has-rewrite-4', { headers: { host: 'example.com', }, @@ -1195,17 +1130,15 @@ const runTests = (isDev = false) => { host: '1', }) - const res2 = await fetchViaHTTP(appPort, '/has-rewrite-4') + const res2 = await next.fetch('/has-rewrite-4') expect(res2.status).toBe(404) }) it('should pass has segment for rewrite correctly', async () => { - const res1 = await fetchViaHTTP(appPort, '/has-rewrite-5') + const res1 = await next.fetch('/has-rewrite-5') expect(res1.status).toBe(404) - const res = await fetchViaHTTP(appPort, '/has-rewrite-5', { - hasParam: 'with-params', - }) + const res = await next.fetch('/has-rewrite-5?hasParam=with-params') expect(res.status).toBe(200) const $ = cheerio.load(await res.text()) @@ -1216,10 +1149,10 @@ const runTests = (isDev = false) => { }) it('should not pass non captured has value for rewrite correctly', async () => { - const res1 = await fetchViaHTTP(appPort, '/has-rewrite-6') + const res1 = await next.fetch('/has-rewrite-6') expect(res1.status).toBe(404) - const res = await fetchViaHTTP(appPort, '/has-rewrite-6', undefined, { + const res = await next.fetch('/has-rewrite-6', { headers: { hasParam: 'with-params', }, @@ -1231,12 +1164,10 @@ const runTests = (isDev = false) => { }) it('should pass captured has value for rewrite correctly', async () => { - const res1 = await fetchViaHTTP(appPort, '/has-rewrite-7') + const res1 = await next.fetch('/has-rewrite-7') expect(res1.status).toBe(404) - const res = await fetchViaHTTP(appPort, '/has-rewrite-7', { - hasParam: 'with-params', - }) + const res = await next.fetch('/has-rewrite-7?hasParam=with-params') expect(res.status).toBe(200) const $ = cheerio.load(await res.text()) @@ -1247,12 +1178,12 @@ const runTests = (isDev = false) => { }) it('should match has rewrite correctly before files', async () => { - const res1 = await fetchViaHTTP(appPort, '/hello') + const res1 = await next.fetch('/hello') expect(res1.status).toBe(200) const $1 = cheerio.load(await res1.text()) expect($1('#hello').text()).toBe('Hello') - const res = await fetchViaHTTP(appPort, '/hello', { overrideMe: '1' }) + const res = await next.fetch('/hello?overrideMe=1') expect(res.status).toBe(200) const $ = cheerio.load(await res.text()) @@ -1262,7 +1193,7 @@ const runTests = (isDev = false) => { overridden: '1', }) - const browser = await webdriver(appPort, '/nav') + const browser = await next.browser('/nav') await browser.eval('window.beforeNav = 1') await browser.elementByCss('#to-overridden').click() await browser.waitForElementByCss('#query') @@ -1278,7 +1209,7 @@ const runTests = (isDev = false) => { }) it('should match has header redirect correctly', async () => { - const res = await fetchViaHTTP(appPort, '/has-redirect-1', undefined, { + const res = await next.fetch('/has-redirect-1', { headers: { 'x-my-header': 'hello world!!', }, @@ -1293,23 +1224,14 @@ const runTests = (isDev = false) => { myHeader: 'hello world!!', }) - const res2 = await fetchViaHTTP(appPort, '/has-redirect-1', undefined, { - redirect: 'manual', - }) + const res2 = await next.fetch('/has-redirect-1', { redirect: 'manual' }) expect(res2.status).toBe(404) }) it('should match has query redirect correctly', async () => { - const res = await fetchViaHTTP( - appPort, - '/has-redirect-2', - { - 'my-query': 'hellooo', - }, - { - redirect: 'manual', - } - ) + const res = await next.fetch('/has-redirect-2?my-query=hellooo', { + redirect: 'manual', + }) expect(res.status).toBe(307) const parsed = new URL(res.headers.get('location'), res.url) @@ -1320,14 +1242,12 @@ const runTests = (isDev = false) => { 'my-query': 'hellooo', }) - const res2 = await fetchViaHTTP(appPort, '/has-redirect-2', undefined, { - redirect: 'manual', - }) + const res2 = await next.fetch('/has-redirect-2', { redirect: 'manual' }) expect(res2.status).toBe(404) }) it('should match has cookie redirect correctly', async () => { - const res = await fetchViaHTTP(appPort, '/has-redirect-3', undefined, { + const res = await next.fetch('/has-redirect-3', { headers: { cookie: 'loggedIn=true', }, @@ -1342,19 +1262,15 @@ const runTests = (isDev = false) => { authorized: '1', }) - const res2 = await fetchViaHTTP(appPort, '/has-redirect-3', undefined, { - redirect: 'manual', - }) + const res2 = await next.fetch('/has-redirect-3', { redirect: 'manual' }) expect(res2.status).toBe(404) }) it('should match has host redirect correctly', async () => { - const res1 = await fetchViaHTTP(appPort, '/has-redirect-4', undefined, { - redirect: 'manual', - }) + const res1 = await next.fetch('/has-redirect-4', { redirect: 'manual' }) expect(res1.status).toBe(404) - const res = await fetchViaHTTP(appPort, '/has-redirect-4', undefined, { + const res = await next.fetch('/has-redirect-4', { headers: { host: 'example.com', }, @@ -1371,12 +1287,10 @@ const runTests = (isDev = false) => { }) it('should match has host redirect and insert in destination correctly', async () => { - const res1 = await fetchViaHTTP(appPort, '/has-redirect-6', undefined, { - redirect: 'manual', - }) + const res1 = await next.fetch('/has-redirect-6', { redirect: 'manual' }) expect(res1.status).toBe(404) - const res = await fetchViaHTTP(appPort, '/has-redirect-6', undefined, { + const res = await next.fetch('/has-redirect-6', { headers: { host: 'hello-test.example.com', }, @@ -1395,14 +1309,9 @@ const runTests = (isDev = false) => { }) it('should match has query redirect with duplicate query key', async () => { - const res = await fetchViaHTTP( - appPort, - '/has-redirect-7', - '?hello=world&hello=another', - { - redirect: 'manual', - } - ) + const res = await next.fetch('/has-redirect-7?hello=world&hello=another', { + redirect: 'manual', + }) expect(res.status).toBe(307) const parsed = new URL(res.headers.get('location'), res.url) @@ -1419,7 +1328,7 @@ const runTests = (isDev = false) => { }) it('should match has header for header correctly', async () => { - const res = await fetchViaHTTP(appPort, '/has-header-1', undefined, { + const res = await next.fetch('/has-header-1', { headers: { 'x-my-header': 'hello world!!', }, @@ -1428,34 +1337,23 @@ const runTests = (isDev = false) => { expect(res.headers.get('x-another')).toBe('header') - const res2 = await fetchViaHTTP(appPort, '/has-header-1', undefined, { - redirect: 'manual', - }) + const res2 = await next.fetch('/has-header-1', { redirect: 'manual' }) expect(res2.headers.get('x-another')).toBe(null) }) it('should match has query for header correctly', async () => { - const res = await fetchViaHTTP( - appPort, - '/has-header-2', - { - 'my-query': 'hellooo', - }, - { - redirect: 'manual', - } - ) + const res = await next.fetch('/has-header-2?my-query=hellooo', { + redirect: 'manual', + }) expect(res.headers.get('x-added')).toBe('value') - const res2 = await fetchViaHTTP(appPort, '/has-header-2', undefined, { - redirect: 'manual', - }) + const res2 = await next.fetch('/has-header-2', { redirect: 'manual' }) expect(res2.headers.get('x-another')).toBe(null) }) it('should match has cookie for header correctly', async () => { - const res = await fetchViaHTTP(appPort, '/has-header-3', undefined, { + const res = await next.fetch('/has-header-3', { headers: { cookie: 'loggedIn=true', }, @@ -1464,14 +1362,12 @@ const runTests = (isDev = false) => { expect(res.headers.get('x-is-user')).toBe('yuuuup') - const res2 = await fetchViaHTTP(appPort, '/has-header-3', undefined, { - redirect: 'manual', - }) + const res2 = await next.fetch('/has-header-3', { redirect: 'manual' }) expect(res2.headers.get('x-is-user')).toBe(null) }) it('should match has host for header correctly', async () => { - const res = await fetchViaHTTP(appPort, '/has-header-4', undefined, { + const res = await next.fetch('/has-header-4', { headers: { host: 'example.com', }, @@ -1480,16 +1376,14 @@ const runTests = (isDev = false) => { expect(res.headers.get('x-is-host')).toBe('yuuuup') - const res2 = await fetchViaHTTP(appPort, '/has-header-4', undefined, { - redirect: 'manual', - }) + const res2 = await next.fetch('/has-header-4', { redirect: 'manual' }) expect(res2.headers.get('x-is-host')).toBe(null) }) - if (!isDev) { + if (!isNextDev) { it('should output routes-manifest successfully', async () => { - const manifest = await fs.readJSON( - join(appDir, '.next/routes-manifest.json') + const manifest = JSON.parse( + await next.readFile('.next/routes-manifest.json') ) for (const route of [ @@ -1506,12 +1400,13 @@ const runTests = (isDev = false) => { route.dataRouteRegex = normalizeRegEx(route.dataRouteRegex) } - expect( - normalizeManifest(manifest, [ - [buildId, 'BUILD_ID'], - [`${externalServerPort}`, 'EXTERNAL_SERVER_PORT'], - ]) - ).toMatchInlineSnapshot(` + const normalizedManifest = normalizeManifest(manifest, [ + [buildId, 'BUILD_ID'], + [`${externalServerPort}`, 'EXTERNAL_SERVER_PORT'], + ]) + + if (process.env.__NEXT_CACHE_COMPONENTS === 'true') { + expect(normalizedManifest).toMatchInlineSnapshot(` { "appType": "pages", "basePath": "", @@ -1866,6 +1761,13 @@ const runTests = (isDev = false) => { ], "onMatchHeaders": [], "pages404": true, + "ppr": { + "chain": { + "headers": { + "next-resume": "1", + }, + }, + }, "redirects": [ { "destination": "/:path+", @@ -2442,10 +2344,10 @@ const runTests = (isDev = false) => { "fallback": [], }, "rsc": { - "clientParamParsing": false, + "clientParamParsing": true, "contentTypeHeader": "text/x-component", "didPostponeHeader": "x-nextjs-postponed", - "dynamicRSCPrerender": false, + "dynamicRSCPrerender": true, "header": "rsc", "prefetchHeader": "next-router-prefetch", "prefetchSegmentDirSuffix": ".segments", @@ -2519,297 +2421,1254 @@ const runTests = (isDev = false) => { "version": 3, } `) - }) - - it('should have redirects/rewrites in build output with debug flag', async () => { - const manifest = await fs.readJSON( - join(appDir, '.next/routes-manifest.json') - ) - const cleanStdout = stripAnsi(stdout) - expect(cleanStdout).toContain('Redirects') - expect(cleanStdout).toContain('Rewrites') - expect(cleanStdout).toContain('Headers') - expect(cleanStdout).toMatch(/source.*?/i) - expect(cleanStdout).toMatch(/destination.*?/i) - - for (const route of [ - ...manifest.redirects, - ...manifest.rewrites.beforeFiles, - ...manifest.rewrites.afterFiles, - ...manifest.rewrites.fallback, - ]) { - expect(cleanStdout).toContain(route.source) - expect(cleanStdout).toContain(route.destination) - } - - for (const route of manifest.headers) { - expect(cleanStdout).toContain(route.source) - - for (const header of route.headers) { - expect(cleanStdout).toContain(header.key) - expect(cleanStdout).toContain(header.value) - } - } - }) - } -} - -describe('Custom routes', () => { - beforeEach(() => { - externalServerHits = new Set() - }) - beforeAll(async () => { - externalServerPort = await findPort() - externalServer = http.createServer((req, res) => { - externalServerHits.add(req.url) - const nextHost = req.headers['x-forwarded-host'] - const externalHost = req.headers['host'] - res.end(`hi ${nextHost} from ${externalHost}`) - }) - const wsServer = new WebSocket.Server({ noServer: true }) - - externalServer.on('upgrade', (req, socket, head) => { - externalServerHits.add(req.url) - wsServer.handleUpgrade(req, socket, head, (client) => { - client.send('hello world') - }) - }) - await new Promise<void>((resolve, reject) => { - externalServer.listen(externalServerPort, (error) => { - if (error) return reject(error) - resolve() - }) - }) - nextConfigRestoreContent = await fs.readFile(nextConfigPath, 'utf8') - await fs.writeFile( - nextConfigPath, - nextConfigRestoreContent.replace(/__EXTERNAL_PORT__/g, externalServerPort) - ) - }) - afterAll(async () => { - externalServer.close() - await fs.writeFile(nextConfigPath, nextConfigRestoreContent) - }) - ;(process.env.TURBOPACK_BUILD ? describe.skip : describe)( - 'development mode', - () => { - let nextConfigContent - - beforeAll(async () => { - // ensure cache with rewrites disabled doesn't persist - // after enabling rewrites - await fs.remove(join(appDir, '.next')) - nextConfigContent = await fs.readFile(nextConfigPath, 'utf8') - await fs.writeFile( - nextConfigPath, - nextConfigContent.replace('// no-rewrites comment', 'return []') - ) + } else { + expect(normalizedManifest).toMatchInlineSnapshot(` + { + "appType": "pages", + "basePath": "", + "caseSensitive": true, + "dataRoutes": [ + { + "dataRouteRegex": "^\\/_next\\/data\\/BUILD_ID\\/blog\\-catchall\\/(.+?)\\.json$", + "namedDataRouteRegex": "^/_next/data/BUILD_ID/blog\\-catchall/(?<nxtPslug>.+?)\\.json$", + "page": "/blog-catchall/[...slug]", + "routeKeys": { + "nxtPslug": "nxtPslug", + }, + }, + { + "dataRouteRegex": "^\\/_next\\/data\\/BUILD_ID\\/overridden\\/([^\\/]+?)\\.json$", + "namedDataRouteRegex": "^/_next/data/BUILD_ID/overridden/(?<nxtPslug>[^/]+?)\\.json$", + "page": "/overridden/[slug]", + "routeKeys": { + "nxtPslug": "nxtPslug", + }, + }, + ], + "dynamicRoutes": [ + { + "namedRegex": "^/_sport/(?<nxtPslug>[^/]+?)(?:/)?$", + "page": "/_sport/[slug]", + "regex": "^\\/_sport\\/([^\\/]+?)(?:\\/)?$", + "routeKeys": { + "nxtPslug": "nxtPslug", + }, + }, + { + "namedRegex": "^/_sport/(?<nxtPslug>[^/]+?)/test(?:/)?$", + "page": "/_sport/[slug]/test", + "regex": "^\\/_sport\\/([^\\/]+?)\\/test(?:\\/)?$", + "routeKeys": { + "nxtPslug": "nxtPslug", + }, + }, + { + "namedRegex": "^/another/(?<nxtPid>[^/]+?)(?:/)?$", + "page": "/another/[id]", + "regex": "^\\/another\\/([^\\/]+?)(?:\\/)?$", + "routeKeys": { + "nxtPid": "nxtPid", + }, + }, + { + "namedRegex": "^/api/dynamic/(?<nxtPslug>[^/]+?)(?:/)?$", + "page": "/api/dynamic/[slug]", + "regex": "^\\/api\\/dynamic\\/([^\\/]+?)(?:\\/)?$", + "routeKeys": { + "nxtPslug": "nxtPslug", + }, + }, + { + "namedRegex": "^/auto\\-export/(?<nxtPslug>[^/]+?)(?:/)?$", + "page": "/auto-export/[slug]", + "regex": "^\\/auto\\-export\\/([^\\/]+?)(?:\\/)?$", + "routeKeys": { + "nxtPslug": "nxtPslug", + }, + }, + { + "namedRegex": "^/blog/(?<nxtPpost>[^/]+?)(?:/)?$", + "page": "/blog/[post]", + "regex": "^\\/blog\\/([^\\/]+?)(?:\\/)?$", + "routeKeys": { + "nxtPpost": "nxtPpost", + }, + }, + { + "namedRegex": "^/blog\\-catchall/(?<nxtPslug>.+?)(?:/)?$", + "page": "/blog-catchall/[...slug]", + "regex": "^\\/blog\\-catchall\\/(.+?)(?:\\/)?$", + "routeKeys": { + "nxtPslug": "nxtPslug", + }, + }, + { + "namedRegex": "^/overridden/(?<nxtPslug>[^/]+?)(?:/)?$", + "page": "/overridden/[slug]", + "regex": "^\\/overridden\\/([^\\/]+?)(?:\\/)?$", + "routeKeys": { + "nxtPslug": "nxtPslug", + }, + }, + ], + "headers": [ + { + "headers": [ + { + "key": "x-new-header", + "value": "new-value", + }, + ], + "missing": [ + { + "key": "x-my-header", + "type": "header", + "value": "(?<myHeader>.*)", + }, + ], + "regex": "^\\/missing-headers-1(?:\\/)?$", + "source": "/missing-headers-1", + }, + { + "headers": [ + { + "key": "x-new-header", + "value": "new-value", + }, + ], + "missing": [ + { + "key": "my-query", + "type": "query", + }, + ], + "regex": "^\\/missing-headers-2(?:\\/)?$", + "source": "/missing-headers-2", + }, + { + "headers": [ + { + "key": "x-new-header", + "value": "new-value", + }, + ], + "missing": [ + { + "key": "loggedIn", + "type": "cookie", + "value": "(?<loggedIn>true)", + }, + ], + "regex": "^\\/missing-headers-3(?:\\/)?$", + "source": "/missing-headers-3", + }, + { + "headers": [ + { + "key": "x-custom-header", + "value": "hello world", + }, + { + "key": "x-another-header", + "value": "hello again", + }, + ], + "regex": "^\\/add-header(?:\\/)?$", + "source": "/add-header", + }, + { + "headers": [ + { + "key": "x-first-header", + "value": "first", + }, + { + "key": "x-second-header", + "value": "second", + }, + ], + "regex": "^\\/my-headers(?:\\/(.*))(?:\\/)?$", + "source": "/my-headers/(.*)", + }, + { + "headers": [ + { + "key": "x-path", + "value": ":path", + }, + { + "key": "some:path", + "value": "hi", + }, + { + "key": "x-test", + "value": "some:value*", + }, + { + "key": "x-test-2", + "value": "value*", + }, + { + "key": "x-test-3", + "value": ":value?", + }, + { + "key": "x-test-4", + "value": ":value+", + }, + { + "key": "x-test-5", + "value": "something https:", + }, + { + "key": "x-test-6", + "value": ":hello(world)", + }, + { + "key": "x-test-7", + "value": "hello(world)", + }, + { + "key": "x-test-8", + "value": "hello{1,}", + }, + { + "key": "x-test-9", + "value": ":hello{1,2}", + }, + { + "key": "content-security-policy", + "value": "default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com/:path", + }, + ], + "regex": "^\\/my-other-header(?:\\/([^\\/]+?))(?:\\/)?$", + "source": "/my-other-header/:path", + }, + { + "headers": [ + { + "key": "x-origin", + "value": "https://example.com", + }, + ], + "regex": "^\\/without-params\\/url(?:\\/)?$", + "source": "/without-params/url", + }, + { + "headers": [ + { + "key": "x-url", + "value": "https://example.com/:path*", + }, + ], + "regex": "^\\/with-params\\/url(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$", + "source": "/with-params/url/:path*", + }, + { + "headers": [ + { + "key": "x-url", + "value": "https://example.com:8080?hello=:path*", + }, + ], + "regex": "^\\/with-params\\/url2(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$", + "source": "/with-params/url2/:path*", + }, + { + "headers": [ + { + "key": "x-something", + "value": "applied-everywhere", + }, + ], + "regex": "^(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$", + "source": "/:path*", + }, + { + "headers": [ + { + "key": "x-something", + "value": "value=:path", + }, + { + "key": "path-:path", + "value": "end", + }, + ], + "regex": "^\\/named-pattern(?:\\/(.*))(?:\\/)?$", + "source": "/named-pattern/:path(.*)", + }, + { + "headers": [ + { + "key": "x-value", + "value": ":path*", + }, + ], + "regex": "^\\/catchall-header(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$", + "source": "/catchall-header/:path*", + }, + { + "has": [ + { + "key": "x-my-header", + "type": "header", + "value": "(?<myHeader>.*)", + }, + ], + "headers": [ + { + "key": "x-another", + "value": "header", + }, + ], + "regex": "^\\/has-header-1(?:\\/)?$", + "source": "/has-header-1", + }, + { + "has": [ + { + "key": "my-query", + "type": "query", + }, + ], + "headers": [ + { + "key": "x-added", + "value": "value", + }, + ], + "regex": "^\\/has-header-2(?:\\/)?$", + "source": "/has-header-2", + }, + { + "has": [ + { + "key": "loggedIn", + "type": "cookie", + "value": "true", + }, + ], + "headers": [ + { + "key": "x-is-user", + "value": "yuuuup", + }, + ], + "regex": "^\\/has-header-3(?:\\/)?$", + "source": "/has-header-3", + }, + { + "has": [ + { + "type": "host", + "value": "example.com", + }, + ], + "headers": [ + { + "key": "x-is-host", + "value": "yuuuup", + }, + ], + "regex": "^\\/has-header-4(?:\\/)?$", + "source": "/has-header-4", + }, + ], + "onMatchHeaders": [], + "pages404": true, + "redirects": [ + { + "destination": "/:path+", + "internal": true, + "priority": true, + "regex": "^(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))\\/$", + "source": "/:path+/", + "statusCode": 308, + }, + { + "destination": "/with-params", + "missing": [ + { + "key": "x-my-header", + "type": "header", + "value": "(?<myHeader>.*)", + }, + ], + "regex": "^(?!\\/_next)\\/missing-redirect-1(?:\\/)?$", + "source": "/missing-redirect-1", + "statusCode": 307, + }, + { + "destination": "/with-params", + "missing": [ + { + "key": "my-query", + "type": "query", + }, + ], + "regex": "^(?!\\/_next)\\/missing-redirect-2(?:\\/)?$", + "source": "/missing-redirect-2", + "statusCode": 307, + }, + { + "destination": "/with-params?authorized=1", + "missing": [ + { + "key": "loggedIn", + "type": "cookie", + "value": "(?<loggedIn>true)", + }, + ], + "regex": "^(?!\\/_next)\\/missing-redirect-3(?:\\/)?$", + "source": "/missing-redirect-3", + "statusCode": 307, + }, + { + "destination": "/:lang/about", + "regex": "^(?!\\/_next)\\/redirect\\/me\\/to-about(?:\\/([^\\/]+?))(?:\\/)?$", + "source": "/redirect/me/to-about/:lang", + "statusCode": 307, + }, + { + "destination": "/docs/v2/network/status-codes#:code", + "regex": "^(?!\\/_next)\\/docs\\/router-status(?:\\/([^\\/]+?))(?:\\/)?$", + "source": "/docs/router-status/:code", + "statusCode": 301, + }, + { + "destination": "/docs/v2/advanced/now-for-github", + "regex": "^(?!\\/_next)\\/docs\\/github(?:\\/)?$", + "source": "/docs/github", + "statusCode": 301, + }, + { + "destination": "/docs/v2/more/:all", + "regex": "^(?!\\/_next)\\/docs\\/v2\\/advanced(?:\\/(.*))(?:\\/)?$", + "source": "/docs/v2/advanced/:all(.*)", + "statusCode": 301, + }, + { + "destination": "/blog/:id", + "regex": "^(?!\\/_next)\\/hello(?:\\/([^\\/]+?))\\/another(?:\\/)?$", + "source": "/hello/:id/another", + "statusCode": 307, + }, + { + "destination": "/", + "regex": "^(?!\\/_next)\\/redirect1(?:\\/)?$", + "source": "/redirect1", + "statusCode": 307, + }, + { + "destination": "/", + "regex": "^(?!\\/_next)\\/redirect2(?:\\/)?$", + "source": "/redirect2", + "statusCode": 301, + }, + { + "destination": "/another", + "regex": "^(?!\\/_next)\\/redirect3(?:\\/)?$", + "source": "/redirect3", + "statusCode": 302, + }, + { + "destination": "/", + "regex": "^(?!\\/_next)\\/redirect4(?:\\/)?$", + "source": "/redirect4", + "statusCode": 308, + }, + { + "destination": "/redir-chain2", + "regex": "^(?!\\/_next)\\/redir-chain1(?:\\/)?$", + "source": "/redir-chain1", + "statusCode": 301, + }, + { + "destination": "/redir-chain3", + "regex": "^(?!\\/_next)\\/redir-chain2(?:\\/)?$", + "source": "/redir-chain2", + "statusCode": 302, + }, + { + "destination": "/", + "regex": "^(?!\\/_next)\\/redir-chain3(?:\\/)?$", + "source": "/redir-chain3", + "statusCode": 303, + }, + { + "destination": "https://google.com", + "regex": "^(?!\\/_next)\\/to-external(?:\\/)?$", + "source": "/to-external", + "statusCode": 307, + }, + { + "destination": "/with-params?first=:section&second=:name", + "regex": "^(?!\\/_next)\\/query-redirect(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))(?:\\/)?$", + "source": "/query-redirect/:section/:name", + "statusCode": 307, + }, + { + "destination": "/got-unnamed", + "regex": "^(?!\\/_next)\\/unnamed(?:\\/(first|second))(?:\\/(.*))(?:\\/)?$", + "source": "/unnamed/(first|second)/(.*)", + "statusCode": 307, + }, + { + "destination": "/:0", + "regex": "^(?!\\/_next)\\/named-like-unnamed(?:\\/([^\\/]+?))(?:\\/)?$", + "source": "/named-like-unnamed/:0", + "statusCode": 307, + }, + { + "destination": "/thank-you-next", + "regex": "^(?!\\/_next)\\/redirect-override(?:\\/)?$", + "source": "/redirect-override", + "statusCode": 307, + }, + { + "destination": "/:first/:second", + "regex": "^(?!\\/_next)\\/docs(?:\\/(integrations|now-cli))\\/v2(.*)(?:\\/)?$", + "source": "/docs/:first(integrations|now-cli)/v2:second(.*)", + "statusCode": 307, + }, + { + "destination": "/somewhere", + "regex": "^(?!\\/_next)\\/catchall-redirect(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$", + "source": "/catchall-redirect/:path*", + "statusCode": 307, + }, + { + "destination": "https://authserver.example.com/set-password?returnUrl=https%3A%2F%2Fwww.example.com/login", + "regex": "^(?!\\/_next)\\/to-external-with-query(?:\\/)?$", + "source": "/to-external-with-query", + "statusCode": 307, + }, + { + "destination": "https://authserver.example.com/set-password?returnUrl=https://www.example.com/login", + "regex": "^(?!\\/_next)\\/to-external-with-query-2(?:\\/)?$", + "source": "/to-external-with-query-2", + "statusCode": 307, + }, + { + "destination": "/another?myHeader=:myHeader", + "has": [ + { + "key": "x-my-header", + "type": "header", + "value": "(?<myHeader>.*)", + }, + ], + "regex": "^(?!\\/_next)\\/has-redirect-1(?:\\/)?$", + "source": "/has-redirect-1", + "statusCode": 307, + }, + { + "destination": "/another?value=:myquery", + "has": [ + { + "key": "my-query", + "type": "query", + }, + ], + "regex": "^(?!\\/_next)\\/has-redirect-2(?:\\/)?$", + "source": "/has-redirect-2", + "statusCode": 307, + }, + { + "destination": "/another?authorized=1", + "has": [ + { + "key": "loggedIn", + "type": "cookie", + "value": "true", + }, + ], + "regex": "^(?!\\/_next)\\/has-redirect-3(?:\\/)?$", + "source": "/has-redirect-3", + "statusCode": 307, + }, + { + "destination": "/another?host=1", + "has": [ + { + "type": "host", + "value": "example.com", + }, + ], + "regex": "^(?!\\/_next)\\/has-redirect-4(?:\\/)?$", + "source": "/has-redirect-4", + "statusCode": 307, + }, + { + "destination": "/somewhere", + "has": [ + { + "key": "x-test-next", + "type": "header", + }, + ], + "regex": "^(?!\\/_next)(?:\\/([^\\/]+?))\\/has-redirect-5(?:\\/)?$", + "source": "/:path/has-redirect-5", + "statusCode": 307, + }, + { + "destination": "https://:subdomain.example.com/some-path/end?a=b", + "has": [ + { + "type": "host", + "value": "(?<subdomain>.*)-test.example.com", + }, + ], + "regex": "^(?!\\/_next)\\/has-redirect-6(?:\\/)?$", + "source": "/has-redirect-6", + "statusCode": 307, + }, + { + "destination": "/somewhere?value=:hello", + "has": [ + { + "key": "hello", + "type": "query", + "value": "(?<hello>.*)", + }, + ], + "regex": "^(?!\\/_next)\\/has-redirect-7(?:\\/)?$", + "source": "/has-redirect-7", + "statusCode": 307, + }, + ], + "rewriteHeaders": { + "pathHeader": "x-nextjs-rewritten-path", + "queryHeader": "x-nextjs-rewritten-query", + }, + "rewrites": { + "afterFiles": [ + { + "destination": "http://localhost:EXTERNAL_SERVER_PORT/_next/hmr?page=/about", + "regex": "^\\/to-websocket(?:\\/)?$", + "source": "/to-websocket", + }, + { + "destination": "/hello", + "regex": "^\\/websocket-to-page(?:\\/)?$", + "source": "/websocket-to-page", + }, + { + "destination": "http://localhost:12233", + "regex": "^\\/to-nowhere(?:\\/)?$", + "source": "/to-nowhere", + }, + { + "destination": "/auto-export/hello?rewrite=1", + "regex": "^\\/rewriting-to-auto-export(?:\\/)?$", + "source": "/rewriting-to-auto-export", + }, + { + "destination": "/auto-export/another?rewrite=1", + "regex": "^\\/rewriting-to-another-auto-export(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$", + "source": "/rewriting-to-another-auto-export/:path*", + }, + { + "destination": "/another/one", + "regex": "^\\/to-another(?:\\/)?$", + "source": "/to-another", + }, + { + "destination": "/404", + "regex": "^\\/nav(?:\\/)?$", + "source": "/nav", + }, + { + "destination": "/static/hello.txt", + "regex": "^\\/hello-world(?:\\/)?$", + "source": "/hello-world", + }, + { + "destination": "/another", + "regex": "^\\/(?:\\/)?$", + "source": "/", + }, + { + "destination": "/multi-rewrites", + "regex": "^\\/another(?:\\/)?$", + "source": "/another", + }, + { + "destination": "/hello", + "regex": "^\\/first(?:\\/)?$", + "source": "/first", + }, + { + "destination": "/hello-again", + "regex": "^\\/second(?:\\/)?$", + "source": "/second", + }, + { + "destination": "/hello", + "regex": "^\\/to-hello(?:\\/)?$", + "source": "/to-hello", + }, + { + "destination": "/blog/post-2", + "regex": "^\\/blog\\/post-1(?:\\/)?$", + "source": "/blog/post-1", + }, + { + "destination": "/:path", + "regex": "^\\/test(?:\\/([^\\/]+?))(?:\\/)?$", + "source": "/test/:path", + }, + { + "destination": "/params/this-should-be-the-value", + "regex": "^\\/test-overwrite(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))(?:\\/)?$", + "source": "/test-overwrite/:something/:another", + }, + { + "destination": "/with-params", + "regex": "^\\/params(?:\\/([^\\/]+?))(?:\\/)?$", + "source": "/params/:something", + }, + { + "destination": "/with-params?first=:section&second=:name", + "regex": "^\\/query-rewrite(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))(?:\\/)?$", + "source": "/query-rewrite/:section/:name", + }, + { + "destination": "/_next/:path*", + "regex": "^\\/hidden\\/_next(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$", + "source": "/hidden/_next/:path*", + }, + { + "destination": "http://localhost:EXTERNAL_SERVER_PORT/:path*", + "regex": "^\\/proxy-me(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$", + "source": "/proxy-me/:path*", + }, + { + "destination": "/api/hello", + "regex": "^\\/api-hello(?:\\/)?$", + "source": "/api-hello", + }, + { + "destination": "/api/hello?name=:first*", + "regex": "^\\/api-hello-regex(?:\\/(.*))(?:\\/)?$", + "source": "/api-hello-regex/:first(.*)", + }, + { + "destination": "/api/hello?hello=:name", + "regex": "^\\/api-hello-param(?:\\/([^\\/]+?))(?:\\/)?$", + "source": "/api-hello-param/:name", + }, + { + "destination": "/api/dynamic/:name?hello=:name", + "regex": "^\\/api-dynamic-param(?:\\/([^\\/]+?))(?:\\/)?$", + "source": "/api-dynamic-param/:name", + }, + { + "destination": "/with-params", + "regex": "^(?:\\/([^\\/]+?))\\/post-321(?:\\/)?$", + "source": "/:path/post-321", + }, + { + "destination": "/with-params", + "regex": "^\\/unnamed-params\\/nested(?:\\/(.*))(?:\\/([^\\/]+?))(?:\\/(.*))(?:\\/)?$", + "source": "/unnamed-params/nested/(.*)/:test/(.*)", + }, + { + "destination": "/with-params", + "regex": "^\\/catchall-rewrite(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$", + "source": "/catchall-rewrite/:path*", + }, + { + "destination": "/with-params?another=:path*", + "regex": "^\\/catchall-query(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$", + "source": "/catchall-query/:path*", + }, + { + "destination": "/with-params?myHeader=:myHeader", + "has": [ + { + "key": "x-my-header", + "type": "header", + "value": "(?<myHeader>.*)", + }, + ], + "regex": "^\\/has-rewrite-1(?:\\/)?$", + "source": "/has-rewrite-1", + }, + { + "destination": "/with-params?value=:myquery", + "has": [ + { + "key": "my-query", + "type": "query", + }, + ], + "regex": "^\\/has-rewrite-2(?:\\/)?$", + "source": "/has-rewrite-2", + }, + { + "destination": "/with-params?authorized=1", + "has": [ + { + "key": "loggedIn", + "type": "cookie", + "value": "(?<loggedIn>true)", + }, + ], + "regex": "^\\/has-rewrite-3(?:\\/)?$", + "source": "/has-rewrite-3", + }, + { + "destination": "/with-params?host=1", + "has": [ + { + "type": "host", + "value": "example.com", + }, + ], + "regex": "^\\/has-rewrite-4(?:\\/)?$", + "source": "/has-rewrite-4", + }, + { + "destination": "/:hasParam", + "has": [ + { + "key": "hasParam", + "type": "query", + }, + ], + "regex": "^\\/has-rewrite-5(?:\\/)?$", + "source": "/has-rewrite-5", + }, + { + "destination": "/with-params", + "has": [ + { + "key": "hasParam", + "type": "header", + "value": "with-params", + }, + ], + "regex": "^\\/has-rewrite-6(?:\\/)?$", + "source": "/has-rewrite-6", + }, + { + "destination": "/with-params?idk=:idk", + "has": [ + { + "key": "hasParam", + "type": "query", + "value": "(?<idk>with-params|hello)", + }, + ], + "regex": "^\\/has-rewrite-7(?:\\/)?$", + "source": "/has-rewrite-7", + }, + { + "destination": "/blog-catchall/:post", + "has": [ + { + "key": "post", + "type": "query", + }, + ], + "regex": "^\\/has-rewrite-8(?:\\/)?$", + "source": "/has-rewrite-8", + }, + { + "destination": "/with-params", + "missing": [ + { + "key": "x-my-header", + "type": "header", + "value": "(?<myHeader>.*)", + }, + ], + "regex": "^\\/missing-rewrite-1(?:\\/)?$", + "source": "/missing-rewrite-1", + }, + { + "destination": "/with-params", + "missing": [ + { + "key": "my-query", + "type": "query", + }, + ], + "regex": "^\\/missing-rewrite-2(?:\\/)?$", + "source": "/missing-rewrite-2", + }, + { + "destination": "/with-params?authorized=1", + "missing": [ + { + "key": "loggedIn", + "type": "cookie", + "value": "(?<loggedIn>true)", + }, + ], + "regex": "^\\/missing-rewrite-3(?:\\/)?$", + "source": "/missing-rewrite-3", + }, + { + "destination": "/hello", + "regex": "^\\/blog\\/about(?:\\/)?$", + "source": "/blog/about", + }, + { + "destination": "/overridden", + "regex": "^\\/overridden(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$", + "source": "/overridden/:path*", + }, + ], + "beforeFiles": [ + { + "destination": "/with-params?overridden=1", + "has": [ + { + "key": "overrideMe", + "type": "query", + }, + ], + "regex": "^\\/hello(?:\\/)?$", + "source": "/hello", + }, + { + "destination": "/blog/:path*", + "regex": "^\\/old-blog(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$", + "source": "/old-blog/:path*", + }, + { + "destination": "https://example.vercel.sh", + "regex": "^\\/overridden(?:\\/)?$", + "source": "/overridden", + }, + { + "destination": "/_sport/nfl/:path*", + "regex": "^\\/nfl(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$", + "source": "/nfl/:path*", + }, + ], + "fallback": [], + }, + "rsc": { + "clientParamParsing": false, + "contentTypeHeader": "text/x-component", + "didPostponeHeader": "x-nextjs-postponed", + "dynamicRSCPrerender": false, + "header": "rsc", + "prefetchHeader": "next-router-prefetch", + "prefetchSegmentDirSuffix": ".segments", + "prefetchSegmentHeader": "next-router-segment-prefetch", + "prefetchSegmentSuffix": ".segment.rsc", + "suffix": ".rsc", + "varyHeader": "rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch", + }, + "staticRoutes": [ + { + "namedRegex": "^/api/hello(?:/)?$", + "page": "/api/hello", + "regex": "^/api/hello(?:/)?$", + "routeKeys": {}, + }, + { + "namedRegex": "^/auto\\-export/another(?:/)?$", + "page": "/auto-export/another", + "regex": "^/auto\\-export/another(?:/)?$", + "routeKeys": {}, + }, + { + "namedRegex": "^/docs/v2/more/now\\-for\\-github(?:/)?$", + "page": "/docs/v2/more/now-for-github", + "regex": "^/docs/v2/more/now\\-for\\-github(?:/)?$", + "routeKeys": {}, + }, + { + "namedRegex": "^/hello(?:/)?$", + "page": "/hello", + "regex": "^/hello(?:/)?$", + "routeKeys": {}, + }, + { + "namedRegex": "^/hello\\-again(?:/)?$", + "page": "/hello-again", + "regex": "^/hello\\-again(?:/)?$", + "routeKeys": {}, + }, + { + "namedRegex": "^/multi\\-rewrites(?:/)?$", + "page": "/multi-rewrites", + "regex": "^/multi\\-rewrites(?:/)?$", + "routeKeys": {}, + }, + { + "namedRegex": "^/nav(?:/)?$", + "page": "/nav", + "regex": "^/nav(?:/)?$", + "routeKeys": {}, + }, + { + "namedRegex": "^/overridden(?:/)?$", + "page": "/overridden", + "regex": "^/overridden(?:/)?$", + "routeKeys": {}, + }, + { + "namedRegex": "^/redirect\\-override(?:/)?$", + "page": "/redirect-override", + "regex": "^/redirect\\-override(?:/)?$", + "routeKeys": {}, + }, + { + "namedRegex": "^/with\\-params(?:/)?$", + "page": "/with-params", + "regex": "^/with\\-params(?:/)?$", + "routeKeys": {}, + }, + ], + "version": 3, + } + `) + } + }) - const tempPort = await findPort() - const tempApp = await launchApp(appDir, tempPort) - await renderViaHTTP(tempPort, '/') + it('should have redirects/rewrites in build output with debug flag', async () => { + const manifest = JSON.parse( + await next.readFile('.next/routes-manifest.json') + ) + const cleanOutput = stripAnsi(buildCliOutput) + expect(cleanOutput).toContain('Redirects') + expect(cleanOutput).toContain('Rewrites') + expect(cleanOutput).toContain('Headers') + expect(cleanOutput).toMatch(/source.*?/i) + expect(cleanOutput).toMatch(/destination.*?/i) - await killApp(tempApp) - await fs.writeFile(nextConfigPath, nextConfigContent) + for (const route of [ + ...manifest.redirects, + ...manifest.rewrites.beforeFiles, + ...manifest.rewrites.afterFiles, + ...manifest.rewrites.fallback, + ]) { + expect(cleanOutput).toContain(route.source) + expect(cleanOutput).toContain(route.destination) + } - appPort = await findPort() - app = await launchApp(appDir, appPort) - buildId = 'development' - }) - afterAll(async () => { - await fs.writeFile(nextConfigPath, nextConfigContent) - await killApp(app) - }) - runTests(true) - } - ) + for (const route of manifest.headers) { + expect(cleanOutput).toContain(route.source) - describe('no-op rewrite', () => { - beforeAll(async () => { - appPort = await findPort() - app = await launchApp(appDir, appPort, { - env: { - ADD_NOOP_REWRITE: 'true', - }, - }) + for (const header of route.headers) { + expect(cleanOutput).toContain(header.key) + expect(cleanOutput).toContain(header.value) + } + } }) - afterAll(() => killApp(app)) - it('should not error for no-op rewrite and auto export dynamic route', async () => { - const browser = await webdriver(appPort, '/auto-export/my-slug') - await check( - () => browser.eval(() => document.documentElement.innerHTML), - /auto-export.*?my-slug/ + it('should not show warning for custom routes when not next export', async () => { + expect(next.cliOutput).not.toContain( + `rewrites, redirects, and headers are not applied when exporting your application detected` ) }) + } +}) +describe('Custom routes no-op rewrite', () => { + const { next, isTurbopack, isNextStart, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + env: { + ADD_NOOP_REWRITE: 'true', + }, + skipDeployment: true, }) - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( - 'production mode', - () => { - beforeAll(async () => { - const { stdout: buildStdout, stderr: buildStderr } = await nextBuild( - appDir, - ['-d'], - { - stdout: true, - stderr: true, - disableAutoSkewProtection: true, - } - ) - stdout = buildStdout - stderr = buildStderr - appPort = await findPort() - app = await nextStart(appDir, appPort, { - disableAutoSkewProtection: true, - }) - buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') - }) - afterAll(() => killApp(app)) - runTests() + if (skipped) return + if (isTurbopack && isNextStart) { + it('skipped - not supported in turbopack build mode', () => {}) + return + } - it('should not show warning for custom routes when not next export', async () => { - expect(stderr).not.toContain( - `rewrites, redirects, and headers are not applied when exporting your application detected` - ) - }) - } - ) + let externalServer: http.Server - describe('should load custom routes when only one type is used', () => { - const runSoloTests = (isDev: boolean) => { - const buildAndStart = async () => { - if (isDev) { - appPort = await findPort() - app = await launchApp(appDir, appPort) - } else { - const { code } = await nextBuild(appDir) - if (code !== 0) throw new Error(`failed to build, got code ${code}`) - appPort = await findPort() - app = await nextStart(appDir, appPort) - } + beforeAll(async () => { + const port = await findPort() + externalServer = http.createServer((req, res) => { + res.end('external') + }) + await new Promise<void>((resolve, reject) => { + const onError = (error: Error) => { + reject(error) } + externalServer.once('error', onError) + externalServer.listen(port, () => { + externalServer.off('error', onError) + resolve() + }) + }) - it('should work with just headers', async () => { - nextConfigContent = await fs.readFile(nextConfigPath, 'utf8') - await fs.writeFile( - nextConfigPath, - nextConfigContent.replace(/(async (?:redirects|rewrites))/g, '$1s') - ) - await buildAndStart() - - const res = await fetchViaHTTP(appPort, '/add-header') - - const res2 = await fetchViaHTTP(appPort, '/docs/github', undefined, { - redirect: 'manual', - }) - const res3 = await fetchViaHTTP(appPort, '/hello-world') + await next.patchFile('next.config.js', (content) => + content.replace(/__EXTERNAL_PORT__/g, String(port)) + ) - await fs.writeFile(nextConfigPath, nextConfigContent) - await killApp(app) + if (!isNextDev) { + await next.build() + } + await next.start() + }) - expect(res.headers.get('x-custom-header')).toBe('hello world') - expect(res.headers.get('x-another-header')).toBe('hello again') + afterAll(() => { + externalServer.close() + }) - expect(res2.status).toBe(404) - expect(res3.status).toBe(404) - }) + it('should not error for no-op rewrite and auto export dynamic route', async () => { + const browser = await next.browser('/auto-export/my-slug') + await retry(async () => { + expect( + await browser.eval(() => document.documentElement.innerHTML) + ).toMatch(/auto-export.*?my-slug/) + }) + }) +}) - it('should work with just rewrites', async () => { - nextConfigContent = await fs.readFile(nextConfigPath, 'utf8') - await fs.writeFile( - nextConfigPath, - nextConfigContent.replace(/(async (?:redirects|headers))/g, '$1s') - ) - await buildAndStart() +describe('Custom routes solo types', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + disableAutoSkewProtection: true, + skipDeployment: true, + }) + if (skipped) return - const res = await fetchViaHTTP(appPort, '/add-header') + let externalServer: http.Server + let externalServerPort: number - const res2 = await fetchViaHTTP(appPort, '/docs/github', undefined, { - redirect: 'manual', - }) - const res3 = await fetchViaHTTP(appPort, '/hello-world') + beforeAll(async () => { + externalServerPort = await findPort() + externalServer = http.createServer((req, res) => { + res.end('external') + }) + await new Promise<void>((resolve, reject) => { + const onError = (error: Error) => { + reject(error) + } + externalServer.once('error', onError) + externalServer.listen(externalServerPort, () => { + externalServer.off('error', onError) + resolve() + }) + }) - await fs.writeFile(nextConfigPath, nextConfigContent) - await killApp(app) + await next.patchFile('next.config.js', (content) => + content.replace(/__EXTERNAL_PORT__/g, String(externalServerPort)) + ) + }) - expect(res.headers.get('x-custom-header')).toBeFalsy() - expect(res.headers.get('x-another-header')).toBeFalsy() + afterAll(() => { + externalServer.close() + }) - expect(res2.status).toBe(404) + it('should work with just headers', async () => { + const content = await next.readFile('next.config.js') + await next.patchFile( + 'next.config.js', + content.replace(/(async (?:redirects|rewrites))/g, '$1s') + ) + try { + if (!isNextDev) { + await next.build() + } + await next.start() - expect(res3.status).toBe(200) - expect(await res3.text()).toContain('hello world!') - }) + const res = await next.fetch('/add-header') + const res2 = await next.fetch('/docs/github', { redirect: 'manual' }) + const res3 = await next.fetch('/hello-world') - it('should work with just redirects', async () => { - nextConfigContent = await fs.readFile(nextConfigPath, 'utf8') - await fs.writeFile( - nextConfigPath, - nextConfigContent.replace(/(async (?:rewrites|headers))/g, '$1s') - ) - await buildAndStart() + expect(res.headers.get('x-custom-header')).toBe('hello world') + expect(res.headers.get('x-another-header')).toBe('hello again') - const res = await fetchViaHTTP(appPort, '/add-header') + expect(res2.status).toBe(404) + expect(res3.status).toBe(404) + } finally { + await next.stop() + await next.patchFile('next.config.js', content) + } + }) - const res2 = await fetchViaHTTP(appPort, '/docs/github', undefined, { - redirect: 'manual', - }) - const res3 = await fetchViaHTTP(appPort, '/hello world') + it('should work with just rewrites', async () => { + const content = await next.readFile('next.config.js') + await next.patchFile( + 'next.config.js', + content.replace(/(async (?:redirects|headers))/g, '$1s') + ) + try { + if (!isNextDev) { + await next.build() + } + await next.start() - await fs.writeFile(nextConfigPath, nextConfigContent) - await killApp(app) + const res = await next.fetch('/add-header') + const res2 = await next.fetch('/docs/github', { redirect: 'manual' }) + const res3 = await next.fetch('/hello-world') - expect(res.headers.get('x-custom-header')).toBeFalsy() - expect(res.headers.get('x-another-header')).toBeFalsy() + expect(res.headers.get('x-custom-header')).toBeFalsy() + expect(res.headers.get('x-another-header')).toBeFalsy() - const { pathname } = new URL(res2.headers.get('location'), res.url) - expect(res2.status).toBe(301) - expect(pathname).toBe('/docs/v2/advanced/now-for-github') + expect(res2.status).toBe(404) - expect(res3.status).toBe(404) - }) + expect(res3.status).toBe(200) + expect(await res3.text()).toContain('hello world!') + } finally { + await next.stop() + await next.patchFile('next.config.js', content) } + }) - ;(process.env.TURBOPACK_BUILD ? describe.skip : describe)( - 'development mode', - () => { - runSoloTests(true) - } + it('should work with just redirects', async () => { + const content = await next.readFile('next.config.js') + await next.patchFile( + 'next.config.js', + content.replace(/(async (?:rewrites|headers))/g, '$1s') ) - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( - 'production mode', - () => { - runSoloTests(false) + try { + if (!isNextDev) { + await next.build() } - ) - }) -}) + await next.start() -describe('export', () => { - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( - 'production mode', - () => { - beforeAll(async () => { - nextConfig.replace('// REPLACEME', `output: 'export',`) - const { stdout: buildStdout, stderr: buildStderr } = await nextBuild( - appDir, - ['-d'], - { - stdout: true, - stderr: true, - } - ) + const res = await next.fetch('/add-header') + const res2 = await next.fetch('/docs/github', { redirect: 'manual' }) + const res3 = await next.fetch('/hello world') - stdout = buildStdout - stderr = buildStderr - }) - afterAll(() => nextConfig.restore()) + expect(res.headers.get('x-custom-header')).toBeFalsy() + expect(res.headers.get('x-another-header')).toBeFalsy() - it('should not show warning for custom routes when not next export', async () => { - expect(stderr).not.toContain( - `rewrites, redirects, and headers are not applied when exporting your application detected` - ) - }) + const { pathname } = new URL(res2.headers.get('location'), res.url) + expect(res2.status).toBe(301) + expect(pathname).toBe('/docs/v2/advanced/now-for-github') + + expect(res3.status).toBe(404) + } finally { + await next.stop() + await next.patchFile('next.config.js', content) } - ) + }) +}) +;(isNextStart ? describe : describe.skip)('Custom routes export', () => { + const { next, isNextDeploy } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + if (isNextDeploy) return + + it('should not show warning for custom routes when not next export', async () => { + await next.patchFile('next.config.js', (content) => + content + .replace(/__EXTERNAL_PORT__/g, '12345') + .replace('// REPLACEME', `output: 'export',`) + ) + const { cliOutput } = await next.build() + + expect(cliOutput).not.toContain( + `rewrites, redirects, and headers are not applied when exporting your application detected` + ) + }) }) diff --git a/test/integration/custom-routes/next.config.js b/test/e2e/custom-routes/next.config.js similarity index 100% rename from test/integration/custom-routes/next.config.js rename to test/e2e/custom-routes/next.config.js diff --git a/test/e2e/custom-routes/package.json b/test/e2e/custom-routes/package.json new file mode 100644 index 000000000000..f06143b8967d --- /dev/null +++ b/test/e2e/custom-routes/package.json @@ -0,0 +1,8 @@ +{ + "private": true, + "dependencies": { + "next": "latest", + "react": "latest", + "react-dom": "latest" + } +} diff --git a/test/integration/custom-routes/pages/_sport/[slug]/index.js b/test/e2e/custom-routes/pages/_sport/[slug]/index.js similarity index 100% rename from test/integration/custom-routes/pages/_sport/[slug]/index.js rename to test/e2e/custom-routes/pages/_sport/[slug]/index.js diff --git a/test/integration/custom-routes/pages/_sport/[slug]/test.js b/test/e2e/custom-routes/pages/_sport/[slug]/test.js similarity index 100% rename from test/integration/custom-routes/pages/_sport/[slug]/test.js rename to test/e2e/custom-routes/pages/_sport/[slug]/test.js diff --git a/test/integration/custom-routes/pages/another/[id].js b/test/e2e/custom-routes/pages/another/[id].js similarity index 100% rename from test/integration/custom-routes/pages/another/[id].js rename to test/e2e/custom-routes/pages/another/[id].js diff --git a/test/integration/custom-routes/pages/api/dynamic/[slug].js b/test/e2e/custom-routes/pages/api/dynamic/[slug].js similarity index 100% rename from test/integration/custom-routes/pages/api/dynamic/[slug].js rename to test/e2e/custom-routes/pages/api/dynamic/[slug].js diff --git a/test/integration/custom-routes/pages/api/hello.js b/test/e2e/custom-routes/pages/api/hello.js similarity index 100% rename from test/integration/custom-routes/pages/api/hello.js rename to test/e2e/custom-routes/pages/api/hello.js diff --git a/test/integration/custom-routes/pages/auto-export/[slug].js b/test/e2e/custom-routes/pages/auto-export/[slug].js similarity index 100% rename from test/integration/custom-routes/pages/auto-export/[slug].js rename to test/e2e/custom-routes/pages/auto-export/[slug].js diff --git a/test/integration/custom-routes/pages/auto-export/another.js b/test/e2e/custom-routes/pages/auto-export/another.js similarity index 100% rename from test/integration/custom-routes/pages/auto-export/another.js rename to test/e2e/custom-routes/pages/auto-export/another.js diff --git a/test/integration/custom-routes/pages/blog-catchall/[...slug].js b/test/e2e/custom-routes/pages/blog-catchall/[...slug].js similarity index 100% rename from test/integration/custom-routes/pages/blog-catchall/[...slug].js rename to test/e2e/custom-routes/pages/blog-catchall/[...slug].js diff --git a/test/integration/custom-routes/pages/blog/[post]/index.js b/test/e2e/custom-routes/pages/blog/[post]/index.js similarity index 100% rename from test/integration/custom-routes/pages/blog/[post]/index.js rename to test/e2e/custom-routes/pages/blog/[post]/index.js diff --git a/test/integration/custom-routes/pages/docs/v2/more/now-for-github.js b/test/e2e/custom-routes/pages/docs/v2/more/now-for-github.js similarity index 100% rename from test/integration/custom-routes/pages/docs/v2/more/now-for-github.js rename to test/e2e/custom-routes/pages/docs/v2/more/now-for-github.js diff --git a/test/integration/custom-routes/pages/hello-again.js b/test/e2e/custom-routes/pages/hello-again.js similarity index 100% rename from test/integration/custom-routes/pages/hello-again.js rename to test/e2e/custom-routes/pages/hello-again.js diff --git a/test/integration/custom-routes/pages/hello.js b/test/e2e/custom-routes/pages/hello.js similarity index 100% rename from test/integration/custom-routes/pages/hello.js rename to test/e2e/custom-routes/pages/hello.js diff --git a/test/integration/custom-routes/pages/multi-rewrites.js b/test/e2e/custom-routes/pages/multi-rewrites.js similarity index 100% rename from test/integration/custom-routes/pages/multi-rewrites.js rename to test/e2e/custom-routes/pages/multi-rewrites.js diff --git a/test/integration/custom-routes/pages/nav.js b/test/e2e/custom-routes/pages/nav.js similarity index 100% rename from test/integration/custom-routes/pages/nav.js rename to test/e2e/custom-routes/pages/nav.js diff --git a/test/integration/custom-routes/pages/overridden.js b/test/e2e/custom-routes/pages/overridden.js similarity index 100% rename from test/integration/custom-routes/pages/overridden.js rename to test/e2e/custom-routes/pages/overridden.js diff --git a/test/integration/custom-routes/pages/overridden/[slug].js b/test/e2e/custom-routes/pages/overridden/[slug].js similarity index 100% rename from test/integration/custom-routes/pages/overridden/[slug].js rename to test/e2e/custom-routes/pages/overridden/[slug].js diff --git a/test/integration/custom-routes/pages/redirect-override.js b/test/e2e/custom-routes/pages/redirect-override.js similarity index 100% rename from test/integration/custom-routes/pages/redirect-override.js rename to test/e2e/custom-routes/pages/redirect-override.js diff --git a/test/integration/custom-routes/pages/with-params.js b/test/e2e/custom-routes/pages/with-params.js similarity index 100% rename from test/integration/custom-routes/pages/with-params.js rename to test/e2e/custom-routes/pages/with-params.js diff --git a/test/integration/custom-routes/public/blog/data.json b/test/e2e/custom-routes/public/blog/data.json similarity index 100% rename from test/integration/custom-routes/public/blog/data.json rename to test/e2e/custom-routes/public/blog/data.json diff --git a/test/integration/custom-routes/public/static/hello.txt b/test/e2e/custom-routes/public/static/hello.txt similarity index 100% rename from test/integration/custom-routes/public/static/hello.txt rename to test/e2e/custom-routes/public/static/hello.txt diff --git a/test/e2e/custom-server/custom-server.test.ts b/test/e2e/custom-server/custom-server.test.ts new file mode 100644 index 000000000000..e43b6e8f74bd --- /dev/null +++ b/test/e2e/custom-server/custom-server.test.ts @@ -0,0 +1,347 @@ +/* eslint-disable jest/no-standalone-expect */ +import { nextTestSetup, isNextDev } from 'e2e-utils' +import { retry } from 'next-test-utils' +import cheerio from 'cheerio' +import https from 'https' + +const sharedDeps = { 'get-port': '5.1.1' } +const sharedNodeEnv = isNextDev ? 'development' : 'production' + +describe.each([ + { title: 'HTTP', useHttps: 'false' }, + { title: 'HTTPS', useHttps: 'true' }, +])('Custom Server $title', ({ title, useHttps }) => { + // The HTTPS server presents a self-signed certificate that the test process + // does not trust. Pass a custom agent that skips cert verification on every + // HTTPS request. Setting `process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'` + // does not take effect from inside the Jest VM context. + const agent = + useHttps === 'true' + ? new https.Agent({ rejectUnauthorized: false }) + : undefined + + describe('with dynamic assetPrefix', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + startCommand: 'node server.js', + serverReadyPattern: /- Local:/, + env: { USE_HTTPS: useHttps, NODE_ENV: sharedNodeEnv }, + dependencies: sharedDeps, + skipDeployment: true, + disableAutoSkewProtection: true, + }) + if (skipped) return + + it('should serve internal file from render', async () => { + const html = await next.render('/static/hello.txt', undefined, { agent }) + expect(html).toMatch(/hello world/) + }) + + it('should handle render with undefined query', async () => { + const html = await next.render('/no-query', undefined, { agent }) + expect(html).toMatch(/"query":/) + }) + + it('should set the assetPrefix dynamically', async () => { + const normalUsage = await next.render('/asset', undefined, { agent }) + expect(normalUsage).not.toMatch(/127\.0\.0\.1/) + + const dynamicUsage = await next.render( + '/asset?setAssetPrefix=1', + undefined, + { agent } + ) + expect(dynamicUsage).toMatch(/127\.0\.0\.1/) + }) + + it('should handle null assetPrefix accordingly', async () => { + const normalUsage = await next.render( + '/asset?setEmptyAssetPrefix=1', + undefined, + { agent } + ) + expect(normalUsage).toMatch(/"\/_next/) + }) + + it('should set the assetPrefix to a given request', async () => { + for (let lc = 0; lc < 10; lc++) { + // Make requests sequential to avoid race condition with setAssetPrefix + const normalUsage = await next.render('/asset', undefined, { agent }) + const dynamicUsage = await next.render( + '/asset?setAssetPrefix=1', + undefined, + { agent } + ) + + expect(normalUsage).not.toMatch(/127\.0\.0\.1/) + expect(dynamicUsage).toMatch(/127\.0\.0\.1/) + } + }) + + it('should render nested index', async () => { + const html = await next.render('/dashboard', undefined, { agent }) + expect(html).toMatch(/made it to dashboard/) + }) + + it('should handle custom urls with requests handler', async () => { + const html = await next.render( + '/custom-url-with-request-handler', + undefined, + { agent } + ) + expect(html).toMatch(/made it to dashboard/) + }) + + it.skip('should contain customServer in NEXT_DATA', async () => { + const html = await next.render('/', undefined, { agent }) + const $ = cheerio.load(html) + expect(JSON.parse($('#__NEXT_DATA__').text()).customServer).toBe(true) + }) + + it.each(['/', '/no-query'])( + 'should handle compression for route %s', + async (route) => { + const response = await next.fetch(route, { agent }) + expect(response.headers.get('Content-Encoding')).toBe('gzip') + } + ) + + it('should read the expected url protocol in middleware', async () => { + const path = '/middleware-augmented' + const response = await next.fetch(path, { agent }) + const port = new URL(next.url).port + expect(response.headers.get('x-original-url')).toBe( + `${useHttps === 'true' ? 'https' : 'http'}://localhost:${port}${path}` + ) + }) + }) + ;(isNextDev ? describe.skip : describe)('with generateEtags enabled', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + startCommand: 'node server.js', + serverReadyPattern: /- Local:/, + env: { + USE_HTTPS: useHttps, + GENERATE_ETAGS: 'true', + NODE_ENV: sharedNodeEnv, + }, + dependencies: sharedDeps, + skipDeployment: true, + disableAutoSkewProtection: true, + }) + if (skipped) return + + it('response includes etag header', async () => { + const response = await next.fetch('/', { agent }) + expect(response.headers.get('etag')).toBeTruthy() + }) + }) + + describe('with generateEtags disabled', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + startCommand: 'node server.js', + serverReadyPattern: /- Local:/, + env: { + USE_HTTPS: useHttps, + GENERATE_ETAGS: 'false', + NODE_ENV: sharedNodeEnv, + }, + dependencies: sharedDeps, + skipDeployment: true, + disableAutoSkewProtection: true, + }) + if (skipped) return + + it('response does not include etag header', async () => { + const response = await next.fetch('/', { agent }) + expect(response.headers.get('etag')).toBeNull() + }) + }) + + if (useHttps === 'false') { + ;(isNextDev ? describe : describe.skip)('HMR with custom server', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + startCommand: 'node server.js', + serverReadyPattern: /- Local:/, + env: { USE_HTTPS: useHttps, NODE_ENV: sharedNodeEnv }, + dependencies: sharedDeps, + skipDeployment: true, + disableAutoSkewProtection: true, + }) + if (skipped) return + + it('Should support HMR when rendering with /index pathname', async () => { + const browser = await next.browser('/test-index-hmr') + const text = await browser.elementByCss('#go-asset').text() + const logs = await browser.log() + expect(text).toBe('Asset') + + expect( + logs.some((log) => + log.message.includes( + 'ReactDOM.hydrate is no longer supported in React 18' + ) + ) + ).toBe(false) + + const originalContent = await next.readFile('pages/index.js') + await next.patchFile( + 'pages/index.js', + originalContent.replace('Asset', 'Asset!!') + ) + + try { + await retry(async () => { + expect(await browser.elementByCss('#go-asset').text()).toMatch( + /Asset!!/ + ) + }) + } finally { + await next.patchFile('pages/index.js', originalContent) + } + }) + }) + } + + describe('Error when rendering without starting slash', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + startCommand: 'node server.js', + serverReadyPattern: /- Local:/, + env: { USE_HTTPS: useHttps, NODE_ENV: sharedNodeEnv }, + dependencies: sharedDeps, + skipDeployment: true, + disableAutoSkewProtection: true, + }) + if (skipped) return + ;(isNextDev ? it : it.skip)('should warn in development mode', async () => { + const cliOutputBefore = next.cliOutput.length + const html = await next.render('/no-slash', undefined, { agent }) + expect(html).toContain('made it to dashboard') + await retry(async () => { + expect(next.cliOutput.slice(cliOutputBefore)).toContain( + 'Cannot render page with path "dashboard"' + ) + }) + }) + ;(isNextDev ? it.skip : it)('should warn in production mode', async () => { + const cliOutputBefore = next.cliOutput.length + const html = await next.render('/no-slash', undefined, { agent }) + expect(html).toContain('made it to dashboard') + await retry(async () => { + expect(next.cliOutput.slice(cliOutputBefore)).toContain( + 'Cannot render page with path "dashboard"' + ) + }) + }) + }) + + describe('with a custom fetch polyfill', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + startCommand: 'node server.js', + serverReadyPattern: /- Local:/, + env: { + USE_HTTPS: useHttps, + POLYFILL_FETCH: 'true', + NODE_ENV: sharedNodeEnv, + }, + dependencies: { ...sharedDeps, 'node-fetch': '2.6.7' }, + skipDeployment: true, + disableAutoSkewProtection: true, + }) + if (skipped) return + + it('should serve internal file from render', async () => { + const html = await next.render('/static/hello.txt', undefined, { agent }) + expect(html).toMatch(/hello world/) + }) + }) + + describe('unhandled rejection', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + startCommand: 'node server.js', + serverReadyPattern: /- Local:/, + env: { USE_HTTPS: useHttps, NODE_ENV: sharedNodeEnv }, + dependencies: sharedDeps, + skipDeployment: true, + disableAutoSkewProtection: true, + }) + if (skipped) return + + it('stderr should include error message and stack trace', async () => { + const cliOutputBefore = next.cliOutput.length + await next.fetch('/unhandled-rejection', { agent }) + await retry(async () => { + const newOutput = next.cliOutput.slice(cliOutputBefore) + expect(newOutput).toContain('unhandledRejection') + }) + const newOutput = next.cliOutput.slice(cliOutputBefore) + expect(newOutput).toContain( + 'unhandledRejection: Error: unhandled rejection' + ) + expect(newOutput).toMatch(/server\.js:\d+:\d+/) + }) + }) + + describe('legacy NextCustomServer methods', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + startCommand: 'node server.js', + serverReadyPattern: /- Local:/, + env: { USE_HTTPS: useHttps, NODE_ENV: sharedNodeEnv }, + dependencies: sharedDeps, + skipDeployment: true, + disableAutoSkewProtection: true, + }) + if (skipped) return + + it('NextCustomServer.renderToHTML', async () => { + const rawHTML = await next.render( + '/legacy-methods/render-to-html?q=2', + undefined, + { agent } + ) + const $ = cheerio.load(rawHTML) + const text = $('p').text() + expect(text).toContain('made it to dynamic dashboard') + expect(text).toContain('query param: 1') + }) + + it('NextCustomServer.render404', async () => { + const html = await next.render('/legacy-methods/render404', undefined, { + agent, + }) + expect(html).toContain('made it to 404') + }) + + it('NextCustomServer.renderError', async () => { + const html = await next.render( + '/legacy-methods/render-error', + undefined, + { agent } + ) + if (isNextDev) { + expect(html).toContain('Error: kaboom') + } else { + expect(html).toContain('made it to 500') + } + }) + + it('NextCustomServer.renderErrorToHTML', async () => { + const html = await next.render( + '/legacy-methods/render-error-to-html', + undefined, + { agent } + ) + if (isNextDev) { + expect(html).toContain('Error: kaboom') + } else { + expect(html).toContain('made it to 500') + } + }) + }) +}) diff --git a/test/integration/custom-server/middleware.js b/test/e2e/custom-server/middleware.js similarity index 100% rename from test/integration/custom-server/middleware.js rename to test/e2e/custom-server/middleware.js diff --git a/test/integration/custom-server/next.config.js b/test/e2e/custom-server/next.config.js similarity index 100% rename from test/integration/custom-server/next.config.js rename to test/e2e/custom-server/next.config.js diff --git a/test/integration/custom-server/pages/404.js b/test/e2e/custom-server/pages/404.js similarity index 100% rename from test/integration/custom-server/pages/404.js rename to test/e2e/custom-server/pages/404.js diff --git a/test/integration/custom-server/pages/500.js b/test/e2e/custom-server/pages/500.js similarity index 100% rename from test/integration/custom-server/pages/500.js rename to test/e2e/custom-server/pages/500.js diff --git a/test/e2e/custom-server/pages/asset.js b/test/e2e/custom-server/pages/asset.js new file mode 100644 index 000000000000..b4c749584394 --- /dev/null +++ b/test/e2e/custom-server/pages/asset.js @@ -0,0 +1,5 @@ +export default () => <div id="asset-page">asset page</div> + +export async function getServerSideProps() { + return { props: {} } +} diff --git a/test/integration/custom-server/pages/dashboard/index.js b/test/e2e/custom-server/pages/dashboard/index.js similarity index 100% rename from test/integration/custom-server/pages/dashboard/index.js rename to test/e2e/custom-server/pages/dashboard/index.js diff --git a/test/integration/custom-server/pages/dynamic-dashboard/index.js b/test/e2e/custom-server/pages/dynamic-dashboard/index.js similarity index 100% rename from test/integration/custom-server/pages/dynamic-dashboard/index.js rename to test/e2e/custom-server/pages/dynamic-dashboard/index.js diff --git a/test/integration/custom-server/pages/index.js b/test/e2e/custom-server/pages/index.js similarity index 100% rename from test/integration/custom-server/pages/index.js rename to test/e2e/custom-server/pages/index.js diff --git a/test/integration/custom-server/pages/middleware-augmented.js b/test/e2e/custom-server/pages/middleware-augmented.js similarity index 100% rename from test/integration/custom-server/pages/middleware-augmented.js rename to test/e2e/custom-server/pages/middleware-augmented.js diff --git a/test/integration/custom-server/pages/no-query.js b/test/e2e/custom-server/pages/no-query.js similarity index 100% rename from test/integration/custom-server/pages/no-query.js rename to test/e2e/custom-server/pages/no-query.js diff --git a/test/integration/custom-server/server.js b/test/e2e/custom-server/server.js similarity index 82% rename from test/integration/custom-server/server.js rename to test/e2e/custom-server/server.js index 0725e4ed1237..67718453929d 100644 --- a/test/integration/custom-server/server.js +++ b/test/e2e/custom-server/server.js @@ -17,17 +17,12 @@ const next = require('next') const { join } = require('path') const { parse } = require('url') +const getPort = require('get-port') const dev = process.env.NODE_ENV !== 'production' const dir = __dirname -const port = - (process.env.PORT ? Number.parseInt(process.env.PORT) : undefined) || 3000 -const { createServer } = require( - process.env.USE_HTTPS === 'true' ? 'https' : 'http' -) - -const app = next({ dev, hostname: 'localhost', port, dir }) -const handleNextRequests = app.getRequestHandler() +const useHttps = process.env.USE_HTTPS === 'true' +const { createServer } = require(useHttps ? 'https' : 'http') const httpOptions = { key: readFileSync(join(__dirname, 'ssh/localhost-key.pem')), @@ -38,9 +33,18 @@ process.on('unhandledRejection', (err) => { console.error('unhandledRejection:', err) }) -app.prepare().then(() => { +async function main() { + const envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : 0 + const port = envPort > 0 ? envPort : await getPort() + const hostname = 'localhost' + const protocol = useHttps ? 'https' : 'http' + + const app = next({ dev, hostname, port, dir }) + const handleNextRequests = app.getRequestHandler() + + await app.prepare() + const server = createServer(httpOptions, async (req, res) => { - // let next.js handle assets from /_next/ if (/\/_next\//.test(req.url)) { return handleNextRequests(req, res) } @@ -59,8 +63,6 @@ app.prepare().then(() => { } else if (/setEmptyAssetPrefix/.test(req.url)) { app.setAssetPrefix('') } else { - // This is to support multi-zones support in localhost - // and may be in staging deployments app.setAssetPrefix('') } @@ -141,11 +143,12 @@ app.prepare().then(() => { handleNextRequests(req, res) }) - server.listen(port, (err) => { - if (err) { - throw err - } - - console.log(`> Ready on http://localhost:${port}`) + server.listen(port, () => { + console.log(`- Local: ${protocol}://${hostname}:${port}`) }) +} + +main().catch((err) => { + console.error(err) + process.exit(1) }) diff --git a/test/e2e/custom-server/ssh/ca-key.pem b/test/e2e/custom-server/ssh/ca-key.pem new file mode 100644 index 000000000000..aa780e55aca4 --- /dev/null +++ b/test/e2e/custom-server/ssh/ca-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7H+WvCew8OD4d +sJ6/BmsLag0R/k829yIvm0npIB4PrKlBhTTvJ1t57p86az5XrzuvwLZ+wcZe2XTM +/y0WeIDqJj2vF9TO7suzVKz60XW4ByKTQY99ZgR6KPNelxJtEdtF9Z2ILvLKuywq +7UEp8m+2QFMrPBckAZpvF87Jcc6Agq9q8KX5I0zhi0zwiho/Ly10Uj1cZflJn0hQ +qpMyrO6GYCavNJaytkqvc9KA/dK7UM+d2YkiR4DSy2nrh965IdCl7a7nM37nG4Ho +kAwImAZ8HMuGzxjNT8GPb50ZHZM+6ufvjvpLrCJKoE/i+on40II7OiZUbCqTA5+Z +V7+WqNMjAgMBAAECggEAI3PFH9cY70XWmuhqAxYTtMtoY4bTej3zN6LLq4Pevih/ +vr6ei0rhHWZUu4wy+QjlnYQ87yIGU9UOOIKGB9JX1kTdfe1db8E1TaoWxcRmcbHE +XrLWnTL1WTGl/j9QCeMOsJ/6syD19evlT9K4fFqGyLhCKZhOaA93s2GG14qczaLh +MCpNM+9YINeba2txLm6y4lEPWduJ3krw5zhnXJ+schYhRQGiQs80ZOqpBmpv6Yg/ +yeUa6DUxsizaFatfMMuV8GuwSLCNQ7qeuBIVilIO3CLBWIJBkZvXSVmUR07SbzdW +7oF40qAryS9URR/4oHyyL3b72cGQ/0WHRALTxRkuWQKBgQD55bspKxyq6NUN0DoM +CSpBfjqgQ5oxM0lkdjmHh9WOjehTya6eW+Vt5a0slubu67qRhJlqcbvYNO8DjQT/ +Ne2exXUXsqXYKdFOjJg0byw3cEJgde0mHEtPtVlAIF0/riKYoBJqgGB6vEDTulcJ +vnyTA9U5xeiERbj5v437e0qoOQKBgQC/sbuuQwfRIXfiBBmnT36xnOFn/L91NRyE +ZnNCojZJgzQD3GGgNbb+ShwhYdE8h5N+aaQj+2AgVg8IGsCAmw2KxSEa/GomBwgS +AZLinHMtBzjeUEevQPD2kHQzvRI5sISOkaFWQiLf3bpuQ2qHZ1cLTgIl+Ny5KQD9 +gZVmyuh+OwKBgHnP/B2jQEQnXsNu/vyi8xAdweIhUeYtw9bOslhYZEcq8Vb2dsIG +tZ2OWY3kuQk5qyYH5ui2LATGOMQYV5k35m6AfgVhNepa6AZMDl1w27Lia9itYz4y +iJuAINNbc1j5Py/6xz9W+LCbV1Zd/NNpITxgn+2bhS4E9pf3QfSWMtwxAoGAPVb3 +MnT6jF4ESYt8VgBnDxifPDIxZaUAIhHSchEMBAv62f1sM+LcUpSOWdQ+KvtLJBlT +z7vBb7d6CeLdlImJFM2toUACC3cWiwR75w2fAVqdRO41cgv6zzs8I84thc5JLIHH +OZ3iIPaHkH78GKXlwZVNtK7kmk9XpMcRcpodOT8CgYEA2KfRvA5KxqFYRlfTpw/j +9tvQMaQYI8zcMCb6fUJ5UTAQ6JnIzx0R4a18DSCMR1gho342UOt4g1NBSmx4fzmX ++k3Op3w1TzlxhxFjBYK81MuwDJ9RNn1TmbIvFdG2h6DSNK9xdBoUbhCgES00I8nK +rorO3dTBV0G/njpZovkQHy8= +-----END PRIVATE KEY----- diff --git a/test/e2e/custom-server/ssh/ca.pem b/test/e2e/custom-server/ssh/ca.pem new file mode 100644 index 000000000000..f807c39b0102 --- /dev/null +++ b/test/e2e/custom-server/ssh/ca.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDTzCCAjegAwIBAgIUTZSRcxl11PwNsMVIIsaS9UsoJAEwDQYJKoZIhvcNAQEL +BQAwNjEdMBsGA1UEAwwUTmV4dC5qcyBUZXN0IFJvb3QgQ0ExFTATBgNVBAoMDE5l +eHQuanMgVGVzdDAgFw0yNjA0MTcxNTE0MjdaGA8yMTI2MDMyNDE1MTQyN1owNjEd +MBsGA1UEAwwUTmV4dC5qcyBUZXN0IFJvb3QgQ0ExFTATBgNVBAoMDE5leHQuanMg +VGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALsf5a8J7Dw4Ph2w +nr8GawtqDRH+Tzb3Ii+bSekgHg+sqUGFNO8nW3nunzprPlevO6/Atn7Bxl7ZdMz/ +LRZ4gOomPa8X1M7uy7NUrPrRdbgHIpNBj31mBHoo816XEm0R20X1nYgu8sq7LCrt +QSnyb7ZAUys8FyQBmm8XzslxzoCCr2rwpfkjTOGLTPCKGj8vLXRSPVxl+UmfSFCq +kzKs7oZgJq80lrK2Sq9z0oD90rtQz53ZiSJHgNLLaeuH3rkh0KXtruczfucbgeiQ +DAiYBnwcy4bPGM1PwY9vnRkdkz7q5++O+kusIkqgT+L6ifjQgjs6JlRsKpMDn5lX +v5ao0yMCAwEAAaNTMFEwHQYDVR0OBBYEFL3ikcUt8Rc87b9VhngffSKQqBk3MB8G +A1UdIwQYMBaAFL3ikcUt8Rc87b9VhngffSKQqBk3MA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQELBQADggEBAGYiA/CP4r7Wp61LKiYjgYtqoZ8zGx1pxWiO5QDD +GKP6ZVzZ9CB4tVuxol91hF74p+D/0n4EFFCQTYE9BtEyXuoU1TxN9iZ6pANY/2to +zTP2qSrk/EuSSMfnHKjhDk04Iffmimry7562w2DNA0/2F9YOp77kQMUxtYR3S1u3 +uxtc79NCr5Wrn7TyH2AHXb55yWMheFEWU5i7K6yAhGTjY/SSDg6zcOmib47N/6Um +SQOgbjuJuUYmbBl1qN6zzNNjiOCnyaX3qmH1gFmB17rrDcJi0DWTIszgOS6pH7EE +HXsgB/VZvIAEH/xVI4UxOU4C9J6SDZ0890ZoelDSpAeUUiQ= +-----END CERTIFICATE----- diff --git a/test/e2e/custom-server/ssh/localhost-key.pem b/test/e2e/custom-server/ssh/localhost-key.pem new file mode 100644 index 000000000000..eecf743f6bbf --- /dev/null +++ b/test/e2e/custom-server/ssh/localhost-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC4RjLHONsy01T+ +akh92wD+JORLGQAb0rM2s02vFMMOzGUCyVk4nP4u/lY+iA6LwJpOXC2cst8PdMXc +R3WwyVBolOdep0gImR5mUJZK2y5KSbXSY2YK+S2seC7XZX6IfYflzzq46aBqQKdC +kanGEmCSkKKk9g0/5zd+ckobAxGrVAhP2xi837CtXlR8Du9j1Yp8VsmTElPr9F2L +o9s1NuqiAQwN5VSQ4sg3WjD5YxLeksL2LPfr5uuBZpPeoSVlFxFxjpqvBgmhr0Qm +KQJPk0vtj22304EjIlfQdmiCUBmAglp/7BoszRNZEdNEgFzIR6DN1wIpoA9tyFZV +YuFoZedDAgMBAAECggEAGD8K/7dQRuPoqzXH27YnxMjiOFa4nXCVVRgK8NkIq1dN +3oEoxLAKKpvGNEx0VtjHqjzVtuhlkjk5WY5rgrs9DG20nc82mq8EVs5kCsMZLabS +OOBhn7BWsqVOSy2BvyrT6UiMP6K55pEi5jmX/vBnFZHYsgo6sb2rdG9MUePkUh2v +arnQxWnPM1cf2VjOwVJxanqTUim0tsZeL7jtWHFfvmrk7NBumCaCMp+Qr1Di+hlQ +Uv7jvGc16KpG0GDrpPr6Cc/bII1aRCVLX8dX+IM3/hMxouSqJRwi39Hc3BMZGzZE +3EMZImKoimnj6iZUHj6W/6vueuMXBHDeGQFNwXUMKQKBgQDiMZZWejFaYDFCW+cp +0L57fmpAZ0cM5fbu03cF+5FhEXkIAbFEiR6vm68hairk1Qaz8AJarlb/mvt+A2Nd +O4Tyu+lsZEmEjpRRXN1Y9SyKdq1LPSbJUK6TA6mGxHNRAkMNrR/fH2YnrGdIVTEi +ZHtcofuaz5Zn12mgb1zu5zDLSQKBgQDQjoAHqv2QjH1idzgXhyZa+rK8PcTeXFz8 +2vw6QWiIsepsr1NOc2PRuGrr97to1/mpg3D07bbhyQg3getsV4xSCRrUQey+G/ha +HINRcWI+OibvH0rr5ATTv7sVcv3sgns/MOX81TW1+4hyYc/Q/LyUQqYxvxjDcXZC +7sEYJAOyKwKBgHDMXbZeVUJGegonno2hxXv8PKxFVI9AFvZeihp3q7YGap7tiSMu +ZhkYwCsfsKhQ+3i8FdB25HevJ/9dXS3fQptxziP5lxf9qkUw1ElKap3aNO0yX6dB +Du3Ng7rrOL6LLDkjvqMcG2tpdFAD++60wTgep0Q6hTzflZMmNegFbrXRAoGAU+aP +8QXD/mvWYU9u3GA9eOpUD1CWK5JiwkJiqBj6McvJcDHURMI9DPiw4v8FgPcp9Bzx +y0b6hLi4OoRkrnBF/ha1mWBwGkbsUWwZFCGWEUyZrycan+1aV8lRPR7GsmgHpvNI +Ar/PXMe1K7bXwM58GvT5IRgsoSu7FNAyFEEgz4MCgYBVV8igQWZRkR+lK7KmRU8A +9D8m4UGmvyrHXmJq0WvfVPEI7XNk6P4xIpoSFHmuIWPGie8L919ohMG5pb8UxHyZ +NYgyhbqQmZ8mpn7f3RjwrQT5JpSGFnHtkRw8d4sfYEnx/KaXMv0h5Jc0vsR3M3jU +aAMnx52Djc5JZynoLNwO8A== +-----END PRIVATE KEY----- diff --git a/test/e2e/custom-server/ssh/localhost.pem b/test/e2e/custom-server/ssh/localhost.pem new file mode 100644 index 000000000000..23fc2f9f6459 --- /dev/null +++ b/test/e2e/custom-server/ssh/localhost.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDeTCCAmGgAwIBAgIUR3IsWKEgIlTx+O7xGsp1jKB4d14wDQYJKoZIhvcNAQEL +BQAwNjEdMBsGA1UEAwwUTmV4dC5qcyBUZXN0IFJvb3QgQ0ExFTATBgNVBAoMDE5l +eHQuanMgVGVzdDAgFw0yNjA0MTcxNTE0MjhaGA8yMTI2MDMyNDE1MTQyOFowFDES +MBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAuEYyxzjbMtNU/mpIfdsA/iTkSxkAG9KzNrNNrxTDDsxlAslZOJz+Lv5WPogO +i8CaTlwtnLLfD3TF3Ed1sMlQaJTnXqdICJkeZlCWStsuSkm10mNmCvktrHgu12V+ +iH2H5c86uOmgakCnQpGpxhJgkpCipPYNP+c3fnJKGwMRq1QIT9sYvN+wrV5UfA7v +Y9WKfFbJkxJT6/Rdi6PbNTbqogEMDeVUkOLIN1ow+WMS3pLC9iz36+brgWaT3qEl +ZRcRcY6arwYJoa9EJikCT5NL7Y9tt9OBIyJX0HZoglAZgIJaf+waLM0TWRHTRIBc +yEegzdcCKaAPbchWVWLhaGXnQwIDAQABo4GeMIGbMCwGA1UdEQQlMCOCCWxvY2Fs +aG9zdIcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATAJBgNVHRMEAjAAMAsGA1UdDwQE +AwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQUBvsF9BO6ujVGTqMm +VzSWXLZYNe0wHwYDVR0jBBgwFoAUveKRxS3xFzztv1WGeB99IpCoGTcwDQYJKoZI +hvcNAQELBQADggEBABXGTIUqpfnyXhDPjCtKYb6FX2DA/Z4tLgnwFlvNhBSsRLg5 +ZdnphScgtuqcKA0PbFQX1CR/N302cK43wtvMR/zrQ4Jcyel/8+5fp/CSwCox4LFJ +zvAoFZRXQg/f8nkdkgv7sh/t5u+/UGjXbtPWzc+ne+LRehpjqA4bpHGBml3yKmym +BTda2IaVvNcuiDsRw7zoAwrjr9KvByEgwYR3gfTmnFcVQ80qQHdaSd3eirS5XaMF +/Eeiojo58FS5qfMPn04MGUqvTQ/ywGiV3EGTe3FhhAFDUTJLDKV6SwsCCs4j6/AM +ehxp4jYZ1sGOOsLqwVVoMcspuOD7FJg61VlgLUg= +-----END CERTIFICATE----- diff --git a/test/integration/custom-server/static/hello.txt b/test/e2e/custom-server/static/hello.txt similarity index 100% rename from test/integration/custom-server/static/hello.txt rename to test/e2e/custom-server/static/hello.txt diff --git a/test/e2e/data-fetching-errors/data-fetching-errors.test.ts b/test/e2e/data-fetching-errors/data-fetching-errors.test.ts new file mode 100644 index 000000000000..4e849b2fda62 --- /dev/null +++ b/test/e2e/data-fetching-errors/data-fetching-errors.test.ts @@ -0,0 +1,214 @@ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +import { retry } from 'next-test-utils' +import { + GSP_NO_RETURNED_VALUE, + GSSP_NO_RETURNED_VALUE, +} from '../../../packages/next/dist/lib/constants' + +describe('GS(S)P Page Errors', () => { + ;(isNextDev ? describe : describe.skip)('development mode', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + }) + if (skipped) return + + it('should show error for getStaticProps as component member', async () => { + const outputIndex = next.cliOutput.length + await next.patchFile( + 'pages/index.js', + ` + const Page = () => 'hi' + Page.getStaticProps = () => ({ props: { hello: 'world' }}) + export default Page + ` + ) + await next.render('/') + expect(next.cliOutput.slice(outputIndex)).toContain( + `getStaticProps can not be attached to a page's component and must be exported from the page` + ) + }) + + it('should show error for getServerSideProps as component member', async () => { + const outputIndex = next.cliOutput.length + await next.patchFile( + 'pages/index.js', + ` + import React from 'react' + export default class MyPage extends React.Component { + static async getServerSideProps() { + return { + props: { + hello: 'world' + } + } + } + render() { + return 'hi' + } + } + ` + ) + await next.render('/') + expect(next.cliOutput.slice(outputIndex)).toContain( + `getServerSideProps can not be attached to a page's component and must be exported from the page` + ) + }) + + it('should show error for getStaticPaths as component member', async () => { + const outputIndex = next.cliOutput.length + await next.patchFile( + 'pages/index.js', + ` + const Page = () => 'hi' + Page.getStaticPaths = () => ({ paths: [], fallback: true }) + export default Page + ` + ) + await next.render('/') + expect(next.cliOutput.slice(outputIndex)).toContain( + `getStaticPaths can not be attached to a page's component and must be exported from the page` + ) + }) + + it('should show error for undefined getStaticProps', async () => { + const outputIndex = next.cliOutput.length + await next.patchFile( + 'pages/index.js', + ` + export function getStaticProps() {} + export default function Page() { + return <div />; + } + ` + ) + await next.render('/') + expect(next.cliOutput.slice(outputIndex)).toContain(GSP_NO_RETURNED_VALUE) + }) + + it('should show error for undefined getServerSideProps', async () => { + const outputIndex = next.cliOutput.length + await next.patchFile( + 'pages/index.js', + ` + export function getServerSideProps() {} + export default function Page() { + return <div />; + } + ` + ) + await next.render('/') + expect(next.cliOutput.slice(outputIndex)).toContain( + GSSP_NO_RETURNED_VALUE + ) + }) + }) + ;(isNextStart ? describe : describe.skip)('production mode', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + if (skipped) return + + it('should show build error for getStaticProps as component member', async () => { + await next.patchFile( + 'pages/index.js', + ` + const Page = () => 'hi' + Page.getStaticProps = () => ({ props: { hello: 'world' }}) + export default Page + ` + ) + const outputIndex = next.cliOutput.length + await next.build() + expect(next.cliOutput.slice(outputIndex)).toContain( + `getStaticProps can not be attached to a page's component and must be exported from the page` + ) + }) + + it('should show build error for getServerSideProps as component member', async () => { + await next.patchFile( + 'pages/index.js', + ` + import React from 'react' + export default class MyPage extends React.Component { + static async getServerSideProps() { + return { + props: { + hello: 'world' + } + } + } + render() { + return 'hi' + } + } + ` + ) + const outputIndex = next.cliOutput.length + await next.build() + expect(next.cliOutput.slice(outputIndex)).toContain( + `getServerSideProps can not be attached to a page's component and must be exported from the page` + ) + }) + + it('should show build error for getStaticPaths as component member', async () => { + await next.patchFile( + 'pages/index.js', + ` + const Page = () => 'hi' + Page.getStaticPaths = () => ({ paths: [], fallback: true }) + export default Page + ` + ) + const outputIndex = next.cliOutput.length + await next.build() + expect(next.cliOutput.slice(outputIndex)).toContain( + `getStaticPaths can not be attached to a page's component and must be exported from the page` + ) + }) + + it('should show build error for undefined getStaticProps', async () => { + await next.patchFile( + 'pages/index.js', + ` + export function getStaticProps() {} + export default function Page() { + return <div />; + } + ` + ) + const outputIndex = next.cliOutput.length + await next.build() + expect(next.cliOutput.slice(outputIndex)).toContain(GSP_NO_RETURNED_VALUE) + }) + + it('Error stack printed to stderr', async () => { + await next.patchFile( + 'pages/index.js', + `export default function Page() { + return <div/> + } + export function getStaticProps() { + // Make it pass on the build phase + if(process.env.NEXT_PHASE === "phase-production-build") { + return { props: { foo: 'bar' }, revalidate: 1 } + } + + throw new Error("Oops") + } + ` + ) + + await next.build() + await next.start() + const outputIndex = next.cliOutput.length + await retry(async () => { + await next.render('/') + expect(next.cliOutput.slice(outputIndex)).toMatch(/error: oops/i) + }) + expect(next.cliOutput.slice(outputIndex)).toContain('Error: Oops') + }) + }) +}) diff --git a/test/integration/data-fetching-errors/pages/index.js b/test/e2e/data-fetching-errors/pages/index.js similarity index 100% rename from test/integration/data-fetching-errors/pages/index.js rename to test/e2e/data-fetching-errors/pages/index.js diff --git a/test/e2e/disable-js/disable-js.test.ts b/test/e2e/disable-js/disable-js.test.ts new file mode 100644 index 000000000000..21eb2905f493 --- /dev/null +++ b/test/e2e/disable-js/disable-js.test.ts @@ -0,0 +1,53 @@ +import { nextTestSetup } from 'e2e-utils' +import cheerio from 'cheerio' + +describe('disabled runtime JS', () => { + const { next, isNextDev, isNextStart, skipped } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + }) + if (skipped) return + + it('should render the page', async () => { + const html = await next.render('/') + expect(html).toMatch(/Hello World/) + }) + + it('should not have __NEXT_DATA__ script', async () => { + const html = await next.render('/') + + const $ = cheerio.load(html) + if (isNextStart) { + expect($('script#__NEXT_DATA__').length).toBe(0) + } + if (isNextDev) { + expect($('script#__NEXT_DATA__').length).toBe(1) + } + }) + + if (isNextStart) { + it('should not have scripts', async () => { + const html = await next.render('/') + const $ = cheerio.load(html) + expect($('script[src]').length).toBe(0) + }) + + it('should not have preload links', async () => { + const html = await next.render('/') + const $ = cheerio.load(html) + expect($('link[rel=preload]').length).toBe(0) + }) + } + + if (isNextDev) { + it('should have a script for each preload link', async () => { + const html = await next.render('/') + const $ = cheerio.load(html) + const preloadLinks = $('link[rel=preload]') + preloadLinks.each((idx, element) => { + const url = $(element).attr('href') + expect($(`script[src="${url}"]`).length).toBe(1) + }) + }) + } +}) diff --git a/test/integration/jsconfig-empty/next.config.js b/test/e2e/disable-js/next.config.js similarity index 100% rename from test/integration/jsconfig-empty/next.config.js rename to test/e2e/disable-js/next.config.js diff --git a/test/integration/disable-js/pages/index.js b/test/e2e/disable-js/pages/index.js similarity index 100% rename from test/integration/disable-js/pages/index.js rename to test/e2e/disable-js/pages/index.js diff --git a/test/e2e/dist-dir/dist-dir.test.ts b/test/e2e/dist-dir/dist-dir.test.ts new file mode 100644 index 000000000000..e02a5d9aefc1 --- /dev/null +++ b/test/e2e/dist-dir/dist-dir.test.ts @@ -0,0 +1,61 @@ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +import { BUILD_ID_FILE, BUILD_MANIFEST } from 'next/constants' + +describe('distDir', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + }) + if (skipped) return + + it('should render the page', async () => { + const html = await next.render('/') + expect(html).toMatch(/Hello World/) + }) + + it('should build the app within the given `dist` directory', async () => { + if (isNextDev) { + expect(await next.hasFile(`dist/dev/${BUILD_MANIFEST}`)).toBe(true) + } else { + expect(await next.hasFile(`dist/${BUILD_ID_FILE}`)).toBe(true) + } + }) + + it('should not build the app within the default `.next` directory', async () => { + expect(await next.hasFile('.next')).toBe(false) + }) +}) + +if (isNextStart) { + describe('distDir config validation', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + if (skipped) return + + it('should throw error with invalid distDir', async () => { + const origConfig = await next.readFile('next.config.js') + await next.patchFile('next.config.js', `module.exports = { distDir: '' }`) + const { cliOutput } = await next.build() + await next.patchFile('next.config.js', origConfig) + + expect(cliOutput).toContain( + 'Invalid distDir provided, distDir can not be an empty string. Please remove this config or set it to undefined' + ) + }) + + it('should handle undefined distDir', async () => { + const origConfig = await next.readFile('next.config.js') + await next.patchFile( + 'next.config.js', + `module.exports = { distDir: undefined }` + ) + const { cliOutput } = await next.build() + await next.patchFile('next.config.js', origConfig) + + expect(cliOutput).not.toContain('Invalid distDir') + }) + }) +} diff --git a/test/integration/dist-dir/next.config.js b/test/e2e/dist-dir/next.config.js similarity index 100% rename from test/integration/dist-dir/next.config.js rename to test/e2e/dist-dir/next.config.js diff --git a/test/integration/dist-dir/pages/index.js b/test/e2e/dist-dir/pages/index.js similarity index 100% rename from test/integration/dist-dir/pages/index.js rename to test/e2e/dist-dir/pages/index.js diff --git a/test/e2e/draft-mode/draft-mode.test.ts b/test/e2e/draft-mode/draft-mode.test.ts new file mode 100644 index 000000000000..7d95741155f2 --- /dev/null +++ b/test/e2e/draft-mode/draft-mode.test.ts @@ -0,0 +1,215 @@ +import cheerio from 'cheerio' +import cookie from 'cookie' +import { nextTestSetup } from 'e2e-utils' + +function getData(html: string) { + const $ = cheerio.load(html) + return { + nextData: JSON.parse($('#__NEXT_DATA__').html()), + draft: $('#draft').text(), + rand: $('#rand').text(), + count: $('#count').text(), + } +} + +describe('Test Draft Mode', () => { + const { next, isNextDev, isNextStart, skipped } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + }) + if (skipped) return + + if (isNextDev) { + it('should start development application', async () => { + const html = await next.render('/') + expect(html).toBeTruthy() + }) + + it('should enable draft mode via dev API', async () => { + const res = await next.fetch('/api/enable') + expect(res.status).toBe(200) + + const cookies = res.headers + .get('set-cookie') + .split(',') + .map((c) => cookie.parse(c)) + + expect(cookies[0]).toBeTruthy() + expect(cookies[0].__prerender_bypass).toBeTruthy() + }) + + it('should start the client-side browser', async () => { + const browser = await next.browser('/api/enable') + await browser.close() + }) + + it('should fetch draft data on SSR', async () => { + const browser = await next.browser('/api/enable') + await browser.get(`${next.url}/`) + await browser.waitForElementByCss('#draft') + expect(await browser.elementById('draft').text()).toBe('true') + await browser.close() + }) + + it('should fetch draft data on CST', async () => { + const browser = await next.browser('/api/enable') + await browser.get(`${next.url}/to-index`) + await browser.waitForElementByCss('#to-index') + await browser.eval('window.itdidnotrefresh = "yep"') + await browser.elementById('to-index').click() + await browser.waitForElementByCss('#draft') + expect(await browser.eval('window.itdidnotrefresh')).toBe('yep') + expect(await browser.elementById('draft').text()).toBe('true') + await browser.close() + }) + + it('should disable draft mode', async () => { + const browser = await next.browser('/api/enable') + await browser.get(`${next.url}/api/disable`) + await browser.get(`${next.url}/`) + await browser.waitForElementByCss('#draft') + expect(await browser.elementById('draft').text()).toBe('false') + await browser.close() + }) + + it('should return cookies to be expired after dev server reboot', async () => { + const res = await next.fetch('/', { + headers: { + Cookie: '__prerender_bypass=stale-value', + }, + }) + expect(res.status).toBe(200) + + const body = await res.text() + expect(body).not.toContain('"err"') + expect(body).not.toContain('TypeError') + expect(body).not.toContain('previewModeId') + + const setCookie = res.headers.get('set-cookie') + expect(setCookie).toBeTruthy() + }) + } + + if (isNextStart) { + let cookieString: string + let initialRand: string + const getOpts = () => ({ headers: { Cookie: cookieString } }) + + it('should start production application', async () => { + const html = await next.render('/') + expect(html).toBeTruthy() + }) + + it('should compile successfully', async () => { + expect(next.cliOutput).toMatch(/Compiled successfully/) + expect(next.cliOutput).not.toContain('Build error occurred') + }) + + it('should return prerendered page on first request', async () => { + const html = await next.render('/') + const { nextData, draft, rand } = getData(html) + expect(nextData).toMatchObject({ isFallback: false }) + expect(draft).toBe('false') + initialRand = rand + }) + + it('should return prerendered page on second request', async () => { + const html = await next.render('/') + const { nextData, draft, rand } = getData(html) + expect(nextData).toMatchObject({ isFallback: false }) + expect(draft).toBe('false') + expect(rand).toBe(initialRand) + }) + + it('should enable draft mode via API', async () => { + const res = await next.fetch('/api/enable') + expect(res.status).toBe(200) + + const originalCookies = res.headers.get('set-cookie').split(',') + const cookies = originalCookies.map((c) => cookie.parse(c)) + + expect(cookies.length).toBe(1) + expect(cookies[0]).toBeTruthy() + expect(cookies[0]).toMatchObject({ Path: '/', SameSite: 'None' }) + expect(cookies[0]).toHaveProperty('__prerender_bypass') + expect(cookies[0]).not.toHaveProperty('Max-Age') + + cookieString = cookie.serialize( + '__prerender_bypass', + cookies[0].__prerender_bypass + ) + }) + + it('should return dynamic response when draft mode enabled', async () => { + const res = await next.fetch('/', getOpts()) + const html = await res.text() + const { nextData, draft, rand } = getData(html) + expect(nextData).toMatchObject({ isFallback: false }) + expect(draft).toBe('true') + expect(rand).not.toBe(initialRand) + }) + + it('should not return fallback page on draft request', async () => { + const res = await next.fetch('/ssp', getOpts()) + const html = await res.text() + + const { nextData, draft } = getData(html) + expect(res.headers.get('cache-control')).toBe( + 'private, no-cache, no-store, max-age=0, must-revalidate' + ) + expect(nextData).toMatchObject({ isFallback: false }) + expect(draft).toBe('true') + }) + + it('should return correct caching headers for draft mode request', async () => { + const url = `/_next/data/${encodeURI(next.buildId)}/index.json` + const res = await next.fetch(url, getOpts()) + const json = await res.json() + + expect(res.headers.get('cache-control')).toBe( + 'private, no-cache, no-store, max-age=0, must-revalidate' + ) + expect(json).toMatchObject({ + pageProps: { + draftMode: 'true', + }, + }) + }) + + it('should return cookies to be expired on disable request', async () => { + const res = await next.fetch('/api/disable', getOpts()) + expect(res.status).toBe(200) + + const cookies = res.headers + .get('set-cookie') + .replace(/(=(?!Lax)\w{3}),/g, '$1') + .split(',') + .map((c) => cookie.parse(c)) + + expect(cookies[0]).toBeTruthy() + expect(cookies[0]).toMatchObject({ + Path: '/', + SameSite: 'None', + Expires: 'Thu 01 Jan 1970 00:00:00 GMT', + }) + expect(cookies[0]).toHaveProperty('__prerender_bypass') + expect(cookies[0]).not.toHaveProperty('Max-Age') + }) + + it('should pass undefined to API routes when not in draft mode', async () => { + const res = await next.fetch('/api/read') + const json = await res.json() + + expect(json).toMatchObject({}) + }) + + it('should pass draft mode to API routes', async () => { + const res = await next.fetch('/api/read', getOpts()) + const json = await res.json() + + expect(json).toMatchObject({ + draftMode: true, + }) + }) + } +}) diff --git a/test/integration/draft-mode/pages/another.tsx b/test/e2e/draft-mode/pages/another.tsx similarity index 100% rename from test/integration/draft-mode/pages/another.tsx rename to test/e2e/draft-mode/pages/another.tsx diff --git a/test/integration/draft-mode/pages/api/disable.ts b/test/e2e/draft-mode/pages/api/disable.ts similarity index 100% rename from test/integration/draft-mode/pages/api/disable.ts rename to test/e2e/draft-mode/pages/api/disable.ts diff --git a/test/integration/draft-mode/pages/api/enable.ts b/test/e2e/draft-mode/pages/api/enable.ts similarity index 100% rename from test/integration/draft-mode/pages/api/enable.ts rename to test/e2e/draft-mode/pages/api/enable.ts diff --git a/test/integration/draft-mode/pages/api/read.ts b/test/e2e/draft-mode/pages/api/read.ts similarity index 100% rename from test/integration/draft-mode/pages/api/read.ts rename to test/e2e/draft-mode/pages/api/read.ts diff --git a/test/integration/draft-mode/pages/index.tsx b/test/e2e/draft-mode/pages/index.tsx similarity index 100% rename from test/integration/draft-mode/pages/index.tsx rename to test/e2e/draft-mode/pages/index.tsx diff --git a/test/integration/draft-mode/pages/ssp.tsx b/test/e2e/draft-mode/pages/ssp.tsx similarity index 100% rename from test/integration/draft-mode/pages/ssp.tsx rename to test/e2e/draft-mode/pages/ssp.tsx diff --git a/test/integration/draft-mode/pages/to-index.tsx b/test/e2e/draft-mode/pages/to-index.tsx similarity index 100% rename from test/integration/draft-mode/pages/to-index.tsx rename to test/e2e/draft-mode/pages/to-index.tsx diff --git a/test/e2e/dynamic-optional-routing-root-fallback/dynamic-optional-routing-root-fallback.test.ts b/test/e2e/dynamic-optional-routing-root-fallback/dynamic-optional-routing-root-fallback.test.ts new file mode 100644 index 000000000000..0c3ec0f1a267 --- /dev/null +++ b/test/e2e/dynamic-optional-routing-root-fallback/dynamic-optional-routing-root-fallback.test.ts @@ -0,0 +1,30 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('Dynamic Optional Routing Root Fallback', () => { + const { next } = nextTestSetup({ files: __dirname }) + + it('should render optional catch-all top-level route with no segments', async () => { + const browser = await next.browser('/') + await browser.waitForElementByCss('#success') + await retry(async () => { + expect(await browser.elementByCss('#success').text()).toMatch(/yay/) + }) + }) + + it('should render optional catch-all top-level route with one segment', async () => { + const browser = await next.browser('/one') + await browser.waitForElementByCss('#success') + await retry(async () => { + expect(await browser.elementByCss('#success').text()).toMatch(/one/) + }) + }) + + it('should render optional catch-all top-level route with two segments', async () => { + const browser = await next.browser('/one/two') + await browser.waitForElementByCss('#success') + await retry(async () => { + expect(await browser.elementByCss('#success').text()).toMatch(/one,two/) + }) + }) +}) diff --git a/test/integration/dynamic-optional-routing-root-static-paths/next.config.js b/test/e2e/dynamic-optional-routing-root-fallback/next.config.js similarity index 100% rename from test/integration/dynamic-optional-routing-root-static-paths/next.config.js rename to test/e2e/dynamic-optional-routing-root-fallback/next.config.js diff --git a/test/integration/dynamic-optional-routing-root-fallback/pages/[[...optionalName]].js b/test/e2e/dynamic-optional-routing-root-fallback/pages/[[...optionalName]].js similarity index 100% rename from test/integration/dynamic-optional-routing-root-fallback/pages/[[...optionalName]].js rename to test/e2e/dynamic-optional-routing-root-fallback/pages/[[...optionalName]].js diff --git a/test/e2e/dynamic-optional-routing-root-static-paths/dynamic-optional-routing-root-static-paths.test.ts b/test/e2e/dynamic-optional-routing-root-static-paths/dynamic-optional-routing-root-static-paths.test.ts new file mode 100644 index 000000000000..7da7568ee300 --- /dev/null +++ b/test/e2e/dynamic-optional-routing-root-static-paths/dynamic-optional-routing-root-static-paths.test.ts @@ -0,0 +1,22 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('Dynamic Optional Routing', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should render optional catch-all top-level route with no segments', async () => { + const $ = await next.render$('/') + expect($('#success').text()).toBe('yay') + }) + + it('should render optional catch-all top-level route with one segment', async () => { + const $ = await next.render$('/one') + expect($('#success').text()).toBe('one') + }) + + it('should render optional catch-all top-level route with two segments', async () => { + const $ = await next.render$('/one/two') + expect($('#success').text()).toBe('one,two') + }) +}) diff --git a/test/integration/dynamic-optional-routing-root-static-paths/pages/[[...optionalName]].js b/test/e2e/dynamic-optional-routing-root-static-paths/pages/[[...optionalName]].js similarity index 100% rename from test/integration/dynamic-optional-routing-root-static-paths/pages/[[...optionalName]].js rename to test/e2e/dynamic-optional-routing-root-static-paths/pages/[[...optionalName]].js diff --git a/test/e2e/dynamic-optional-routing/dynamic-optional-routing.test.ts b/test/e2e/dynamic-optional-routing/dynamic-optional-routing.test.ts new file mode 100644 index 000000000000..55508d2408fe --- /dev/null +++ b/test/e2e/dynamic-optional-routing/dynamic-optional-routing.test.ts @@ -0,0 +1,298 @@ +import cheerio from 'cheerio' +import { nextTestSetup, isNextDev } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('Dynamic Optional Routing', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + }) + if (skipped) return + + it('should render catch-all top-level route with multiple segments', async () => { + const html = await next.render('/hello/world') + const $ = cheerio.load(html) + expect($('#route').text()).toBe('top level route param: [hello|world]') + }) + + it('should render catch-all top-level route with single segment', async () => { + const html = await next.render('/hello') + const $ = cheerio.load(html) + expect($('#route').text()).toBe('top level route param: [hello]') + }) + + it('should render catch-all top-level route with no segments', async () => { + const html = await next.render('/') + const $ = cheerio.load(html) + expect($('#route').text()).toBe('top level route param: undefined') + }) + + it('should render catch-all nested route with multiple segments', async () => { + const html = await next.render('/nested/hello/world') + const $ = cheerio.load(html) + expect($('#route').text()).toBe('nested route param: [hello|world]') + }) + + it('should render catch-all nested route with single segment', async () => { + const html = await next.render('/nested/hello') + const $ = cheerio.load(html) + expect($('#route').text()).toBe('nested route param: [hello]') + }) + + it('should render catch-all nested route with no segments', async () => { + const html = await next.render('/nested') + const $ = cheerio.load(html) + expect($('#route').text()).toBe('nested route param: undefined') + }) + + it('should render catch-all nested route with no segments and leading slash', async () => { + const html = await next.render('/nested/') + const $ = cheerio.load(html) + expect($('#route').text()).toBe('nested route param: undefined') + }) + + it('should match catch-all api route with multiple segments', async () => { + const res = await next.fetch('/api/post/ab/cd') + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ slug: ['ab', 'cd'] }) + }) + + it('should match catch-all api route with single segment', async () => { + const res = await next.fetch('/api/post/a') + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ slug: ['a'] }) + }) + + it('should match catch-all api route with no segments', async () => { + const res = await next.fetch('/api/post') + expect(res.status).toBe(200) + expect(await res.json()).toEqual({}) + }) + + it('should match catch-all api route with no segments and leading slash', async () => { + const res = await next.fetch('/api/post/') + expect(res.status).toBe(200) + expect(await res.json()).toEqual({}) + }) + + it('should handle getStaticPaths no segments', async () => { + const html = await next.render('/get-static-paths') + const $ = cheerio.load(html) + expect($('#route').text()).toBe('gsp route: undefined') + }) + + it('should handle getStaticPaths no segments and trailing slash', async () => { + const html = await next.render('/get-static-paths/') + const $ = cheerio.load(html) + expect($('#route').text()).toBe('gsp route: undefined') + }) + + it('should handle getStaticPaths 1 segment', async () => { + const html = await next.render('/get-static-paths/p1') + const $ = cheerio.load(html) + expect($('#route').text()).toBe('gsp route: [p1]') + }) + + it('should handle getStaticPaths 1 segment and trailing slash', async () => { + const html = await next.render('/get-static-paths/p1/') + const $ = cheerio.load(html) + expect($('#route').text()).toBe('gsp route: [p1]') + }) + + it('should handle getStaticPaths 2 segments', async () => { + const html = await next.render('/get-static-paths/p2/p3') + const $ = cheerio.load(html) + expect($('#route').text()).toBe('gsp route: [p2|p3]') + }) + + it('should handle getStaticPaths 2 segments and trailing slash', async () => { + const html = await next.render('/get-static-paths/p2/p3/') + const $ = cheerio.load(html) + expect($('#route').text()).toBe('gsp route: [p2|p3]') + }) + + it('should fall back to top-level catch-all', async () => { + const html = await next.render('/get-static-paths/hello/world') + const $ = cheerio.load(html) + expect($('#route').text()).toBe( + 'top level route param: [get-static-paths|hello|world]' + ) + }) + + it('should match root path on undefined param', async () => { + const html = await next.render('/get-static-paths-undefined') + const $ = cheerio.load(html) + expect($('#route').text()).toBe('gsp undefined route: undefined') + }) + + it('should match root path on false param', async () => { + const html = await next.render('/get-static-paths-false') + const $ = cheerio.load(html) + expect($('#route').text()).toBe('gsp false route: undefined') + }) + + it('should match root path on null param', async () => { + const html = await next.render('/get-static-paths-null') + const $ = cheerio.load(html) + expect($('#route').text()).toBe('gsp null route: undefined') + }) + + it('should handle getStaticPaths with fallback no segments', async () => { + const html = await next.render('/get-static-paths-fallback') + const $ = cheerio.load(html) + expect($('#route').text()).toBe( + 'gsp fallback route: undefined is not fallback' + ) + }) + + it('should handle getStaticPaths with fallback 2 segments', async () => { + const html = await next.render('/get-static-paths-fallback/p2/p3') + const $ = cheerio.load(html) + expect($('#route').text()).toBe( + 'gsp fallback route: [p2|p3] is not fallback' + ) + }) + + it('should fallback correctly when fallback enabled', async () => { + const html = await next.render('/get-static-paths-fallback/hello/world') + const $ = cheerio.load(html) + expect($('#route').text()).toBe('gsp fallback route: undefined is fallback') + }) + + if (isNextDev) { + const DUMMY_PAGE = 'export default () => null' + + it('should fail when optional route has index.js at root', async () => { + try { + await next.patchFile('pages/index.js', DUMMY_PAGE) + await retry(async () => { + expect(next.cliOutput).toMatch( + /You cannot define a route with the same specificity as a optional catch-all route/ + ) + }) + } finally { + await next.deleteFile('pages/index.js') + } + }) + + it('should fail when optional route has same page at root', async () => { + try { + await next.patchFile('pages/nested.js', DUMMY_PAGE) + await retry(async () => { + expect(next.cliOutput).toMatch( + /You cannot define a route with the same specificity as a optional catch-all route/ + ) + }) + } finally { + await next.deleteFile('pages/nested.js') + } + }) + + it('should fail when mixed with regular catch-all', async () => { + try { + await next.patchFile('pages/nested/[...param].js', DUMMY_PAGE) + await retry(async () => { + expect(next.cliOutput).toMatch( + /You cannot use both .+ at the same level/ + ) + }) + } finally { + await next.deleteFile('pages/nested/[...param].js') + } + }) + + it('should fail when optional but no catch-all', async () => { + try { + await next.patchFile('pages/invalid/[[param]].js', DUMMY_PAGE) + await retry(async () => { + expect(next.cliOutput).toMatch( + /Optional route parameters are not yet supported/ + ) + }) + } finally { + await next.deleteFile('pages/invalid/[[param]].js') + } + }) + } +}) + +describe('Dynamic Optional Routing - build validation', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + if (skipped) return + + const DUMMY_PAGE = 'export default () => null' + + it('should fail to build when optional route has index.js at root', async () => { + await next.patchFile('pages/index.js', DUMMY_PAGE) + await next.build() + expect(next.cliOutput).toMatch( + /You cannot define a route with the same specificity as a optional catch-all route/ + ) + // Clean up for next test + await next.deleteFile('pages/index.js') + }) + + it('should fail to build when optional route has same page at root', async () => { + await next.patchFile('pages/nested.js', DUMMY_PAGE) + await next.build() + expect(next.cliOutput).toMatch( + /You cannot define a route with the same specificity as a optional catch-all route/ + ) + // Clean up for next test + await next.deleteFile('pages/nested.js') + }) + + it('should fail to build when mixed with regular catch-all', async () => { + await next.patchFile('pages/nested/[...param].js', DUMMY_PAGE) + await next.build() + expect(next.cliOutput).toMatch(/You cannot use both .+ at the same level/) + // Clean up for next test + await next.deleteFile('pages/nested/[...param].js') + }) + + it('should fail to build when optional but no catch-all', async () => { + await next.patchFile('pages/invalid/[[param]].js', DUMMY_PAGE) + await next.build() + expect(next.cliOutput).toMatch( + /Optional route parameters are not yet supported/ + ) + // Clean up for next test + await next.deleteFile('pages/invalid/[[param]].js') + }) + + it('should fail to build when param is not explicitly defined', async () => { + await next.patchFile( + 'pages/invalid/[[...slug]].js', + ` + export async function getStaticPaths() { + return { + paths: [ + { params: {} }, + ], + fallback: false, + } + } + + export async function getStaticProps({ params }) { + return { props: { params } } + } + + export default function Index(props) { + return ( + <div>Invalid</div> + ) + } + ` + ) + await next.build() + expect(next.cliOutput).toMatch( + 'A required parameter (slug) was not provided as an array received undefined in getStaticPaths for /invalid/[[...slug]]' + ) + // Clean up + await next.deleteFile('pages/invalid/[[...slug]].js') + }) +}) diff --git a/test/integration/dynamic-optional-routing/next.config.js b/test/e2e/dynamic-optional-routing/next.config.js similarity index 100% rename from test/integration/dynamic-optional-routing/next.config.js rename to test/e2e/dynamic-optional-routing/next.config.js diff --git a/test/integration/dynamic-optional-routing/pages/[[...optionalName]].js b/test/e2e/dynamic-optional-routing/pages/[[...optionalName]].js similarity index 100% rename from test/integration/dynamic-optional-routing/pages/[[...optionalName]].js rename to test/e2e/dynamic-optional-routing/pages/[[...optionalName]].js diff --git a/test/integration/dynamic-optional-routing/pages/about.js b/test/e2e/dynamic-optional-routing/pages/about.js similarity index 100% rename from test/integration/dynamic-optional-routing/pages/about.js rename to test/e2e/dynamic-optional-routing/pages/about.js diff --git a/test/integration/dynamic-optional-routing/pages/api/post/[[...slug]].js b/test/e2e/dynamic-optional-routing/pages/api/post/[[...slug]].js similarity index 100% rename from test/integration/dynamic-optional-routing/pages/api/post/[[...slug]].js rename to test/e2e/dynamic-optional-routing/pages/api/post/[[...slug]].js diff --git a/test/integration/dynamic-optional-routing/pages/get-static-paths-fallback/[[...slug]].js b/test/e2e/dynamic-optional-routing/pages/get-static-paths-fallback/[[...slug]].js similarity index 100% rename from test/integration/dynamic-optional-routing/pages/get-static-paths-fallback/[[...slug]].js rename to test/e2e/dynamic-optional-routing/pages/get-static-paths-fallback/[[...slug]].js diff --git a/test/integration/dynamic-optional-routing/pages/get-static-paths-false/[[...slug]].js b/test/e2e/dynamic-optional-routing/pages/get-static-paths-false/[[...slug]].js similarity index 100% rename from test/integration/dynamic-optional-routing/pages/get-static-paths-false/[[...slug]].js rename to test/e2e/dynamic-optional-routing/pages/get-static-paths-false/[[...slug]].js diff --git a/test/integration/dynamic-optional-routing/pages/get-static-paths-null/[[...slug]].js b/test/e2e/dynamic-optional-routing/pages/get-static-paths-null/[[...slug]].js similarity index 100% rename from test/integration/dynamic-optional-routing/pages/get-static-paths-null/[[...slug]].js rename to test/e2e/dynamic-optional-routing/pages/get-static-paths-null/[[...slug]].js diff --git a/test/integration/dynamic-optional-routing/pages/get-static-paths-undefined/[[...slug]].js b/test/e2e/dynamic-optional-routing/pages/get-static-paths-undefined/[[...slug]].js similarity index 100% rename from test/integration/dynamic-optional-routing/pages/get-static-paths-undefined/[[...slug]].js rename to test/e2e/dynamic-optional-routing/pages/get-static-paths-undefined/[[...slug]].js diff --git a/test/integration/dynamic-optional-routing/pages/get-static-paths/[[...slug]].js b/test/e2e/dynamic-optional-routing/pages/get-static-paths/[[...slug]].js similarity index 100% rename from test/integration/dynamic-optional-routing/pages/get-static-paths/[[...slug]].js rename to test/e2e/dynamic-optional-routing/pages/get-static-paths/[[...slug]].js diff --git a/test/integration/dynamic-optional-routing/pages/nested/[[...optionalName]].js b/test/e2e/dynamic-optional-routing/pages/nested/[[...optionalName]].js similarity index 100% rename from test/integration/dynamic-optional-routing/pages/nested/[[...optionalName]].js rename to test/e2e/dynamic-optional-routing/pages/nested/[[...optionalName]].js diff --git a/test/e2e/dynamic-routing-middleware/dynamic-routing-middleware.test.ts b/test/e2e/dynamic-routing-middleware/dynamic-routing-middleware.test.ts new file mode 100644 index 000000000000..7a2d2c216a51 --- /dev/null +++ b/test/e2e/dynamic-routing-middleware/dynamic-routing-middleware.test.ts @@ -0,0 +1,28 @@ +import { nextTestSetup, isNextDev } from 'e2e-utils' +import { join } from 'path' +import { runTests } from '../dynamic-routing/shared' + +describe('Dynamic Routing with Middleware', () => { + const { next, isTurbopack, skipped } = nextTestSetup({ + files: join(__dirname, '../dynamic-routing'), + skipStart: true, + disableAutoSkewProtection: true, + skipDeployment: true, + }) + if (skipped) return + + beforeAll(async () => { + await next.patchFile( + 'middleware.js', + ` +import { NextResponse } from 'next/server' +export default function middleware() { + return NextResponse.next() +} +` + ) + await next.start() + }) + + runTests({ next, isNextDev, isTurbopack, middlewareEnabled: true }) +}) diff --git a/test/e2e/dynamic-routing/dynamic-routing.test.ts b/test/e2e/dynamic-routing/dynamic-routing.test.ts new file mode 100644 index 000000000000..775d3c3ac53f --- /dev/null +++ b/test/e2e/dynamic-routing/dynamic-routing.test.ts @@ -0,0 +1,16 @@ +import { nextTestSetup, isNextDev } from 'e2e-utils' +import { runTests } from './shared' + +describe('Dynamic Routing', () => { + const { next, isTurbopack, skipped } = nextTestSetup({ + files: __dirname, + disableAutoSkewProtection: true, + // Some assertions (`should not decode slashes`, `should serve file with + // plus from public/static folder`) depend on local Next.js URL handling + // and don't apply to Vercel's deploy infrastructure. + skipDeployment: true, + }) + if (skipped) return + + runTests({ next, isNextDev, isTurbopack, middlewareEnabled: false }) +}) diff --git a/test/integration/dynamic-routing/pages/[name]/[comment].js b/test/e2e/dynamic-routing/pages/[name]/[comment].js similarity index 100% rename from test/integration/dynamic-routing/pages/[name]/[comment].js rename to test/e2e/dynamic-routing/pages/[name]/[comment].js diff --git a/test/integration/dynamic-routing/pages/[name]/[comment]/[...rest].js b/test/e2e/dynamic-routing/pages/[name]/[comment]/[...rest].js similarity index 100% rename from test/integration/dynamic-routing/pages/[name]/[comment]/[...rest].js rename to test/e2e/dynamic-routing/pages/[name]/[comment]/[...rest].js diff --git a/test/integration/dynamic-routing/pages/[name]/comments.js b/test/e2e/dynamic-routing/pages/[name]/comments.js similarity index 100% rename from test/integration/dynamic-routing/pages/[name]/comments.js rename to test/e2e/dynamic-routing/pages/[name]/comments.js diff --git a/test/integration/dynamic-routing/pages/[name]/index.js b/test/e2e/dynamic-routing/pages/[name]/index.js similarity index 100% rename from test/integration/dynamic-routing/pages/[name]/index.js rename to test/e2e/dynamic-routing/pages/[name]/index.js diff --git a/test/integration/dynamic-routing/pages/[name]/on-mount-redir.js b/test/e2e/dynamic-routing/pages/[name]/on-mount-redir.js similarity index 100% rename from test/integration/dynamic-routing/pages/[name]/on-mount-redir.js rename to test/e2e/dynamic-routing/pages/[name]/on-mount-redir.js diff --git a/test/integration/dynamic-routing/pages/_app.js b/test/e2e/dynamic-routing/pages/_app.js similarity index 100% rename from test/integration/dynamic-routing/pages/_app.js rename to test/e2e/dynamic-routing/pages/_app.js diff --git a/test/integration/dynamic-routing/pages/another.js b/test/e2e/dynamic-routing/pages/another.js similarity index 100% rename from test/integration/dynamic-routing/pages/another.js rename to test/e2e/dynamic-routing/pages/another.js diff --git a/test/integration/dynamic-routing/pages/b/[123].js b/test/e2e/dynamic-routing/pages/b/[123].js similarity index 100% rename from test/integration/dynamic-routing/pages/b/[123].js rename to test/e2e/dynamic-routing/pages/b/[123].js diff --git a/test/integration/dynamic-routing/pages/blog/[name]/comment/[id].js b/test/e2e/dynamic-routing/pages/blog/[name]/comment/[id].js similarity index 100% rename from test/integration/dynamic-routing/pages/blog/[name]/comment/[id].js rename to test/e2e/dynamic-routing/pages/blog/[name]/comment/[id].js diff --git a/test/integration/dynamic-routing/pages/c/[alongparamnameshouldbeallowedeventhoughweird].js b/test/e2e/dynamic-routing/pages/c/[alongparamnameshouldbeallowedeventhoughweird].js similarity index 100% rename from test/integration/dynamic-routing/pages/c/[alongparamnameshouldbeallowedeventhoughweird].js rename to test/e2e/dynamic-routing/pages/c/[alongparamnameshouldbeallowedeventhoughweird].js diff --git a/test/integration/dynamic-routing/pages/catchall-dash/[...hello-world].js b/test/e2e/dynamic-routing/pages/catchall-dash/[...hello-world].js similarity index 100% rename from test/integration/dynamic-routing/pages/catchall-dash/[...hello-world].js rename to test/e2e/dynamic-routing/pages/catchall-dash/[...hello-world].js diff --git a/test/integration/dynamic-routing/pages/d/[id].js b/test/e2e/dynamic-routing/pages/d/[id].js similarity index 100% rename from test/integration/dynamic-routing/pages/d/[id].js rename to test/e2e/dynamic-routing/pages/d/[id].js diff --git a/test/integration/dynamic-routing/pages/dash/[hello-world].js b/test/e2e/dynamic-routing/pages/dash/[hello-world].js similarity index 100% rename from test/integration/dynamic-routing/pages/dash/[hello-world].js rename to test/e2e/dynamic-routing/pages/dash/[hello-world].js diff --git a/test/integration/dynamic-routing/pages/index.js b/test/e2e/dynamic-routing/pages/index.js similarity index 100% rename from test/integration/dynamic-routing/pages/index.js rename to test/e2e/dynamic-routing/pages/index.js diff --git a/test/integration/dynamic-routing/pages/index/[...slug].js b/test/e2e/dynamic-routing/pages/index/[...slug].js similarity index 100% rename from test/integration/dynamic-routing/pages/index/[...slug].js rename to test/e2e/dynamic-routing/pages/index/[...slug].js diff --git a/test/integration/dynamic-routing/pages/on-mount/[post].js b/test/e2e/dynamic-routing/pages/on-mount/[post].js similarity index 100% rename from test/integration/dynamic-routing/pages/on-mount/[post].js rename to test/e2e/dynamic-routing/pages/on-mount/[post].js diff --git a/test/integration/dynamic-routing/pages/p1/p2/all-ssg/[...rest].js b/test/e2e/dynamic-routing/pages/p1/p2/all-ssg/[...rest].js similarity index 100% rename from test/integration/dynamic-routing/pages/p1/p2/all-ssg/[...rest].js rename to test/e2e/dynamic-routing/pages/p1/p2/all-ssg/[...rest].js diff --git a/test/integration/dynamic-routing/pages/p1/p2/all-ssr/[...rest].js b/test/e2e/dynamic-routing/pages/p1/p2/all-ssr/[...rest].js similarity index 100% rename from test/integration/dynamic-routing/pages/p1/p2/all-ssr/[...rest].js rename to test/e2e/dynamic-routing/pages/p1/p2/all-ssr/[...rest].js diff --git a/test/integration/dynamic-routing/pages/p1/p2/nested-all-ssg/[...rest]/index.js b/test/e2e/dynamic-routing/pages/p1/p2/nested-all-ssg/[...rest]/index.js similarity index 100% rename from test/integration/dynamic-routing/pages/p1/p2/nested-all-ssg/[...rest]/index.js rename to test/e2e/dynamic-routing/pages/p1/p2/nested-all-ssg/[...rest]/index.js diff --git a/test/integration/dynamic-routing/pages/p1/p2/nested-all-ssg/[...rest]/styles.module.css b/test/e2e/dynamic-routing/pages/p1/p2/nested-all-ssg/[...rest]/styles.module.css similarity index 100% rename from test/integration/dynamic-routing/pages/p1/p2/nested-all-ssg/[...rest]/styles.module.css rename to test/e2e/dynamic-routing/pages/p1/p2/nested-all-ssg/[...rest]/styles.module.css diff --git a/test/integration/dynamic-routing/pages/p1/p2/predefined-ssg/[...rest].js b/test/e2e/dynamic-routing/pages/p1/p2/predefined-ssg/[...rest].js similarity index 100% rename from test/integration/dynamic-routing/pages/p1/p2/predefined-ssg/[...rest].js rename to test/e2e/dynamic-routing/pages/p1/p2/predefined-ssg/[...rest].js diff --git a/test/integration/dynamic-routing/public/hello copy.txt b/test/e2e/dynamic-routing/public/hello copy.txt similarity index 100% rename from test/integration/dynamic-routing/public/hello copy.txt rename to test/e2e/dynamic-routing/public/hello copy.txt diff --git a/test/integration/dynamic-routing/public/hello%20copy.txt b/test/e2e/dynamic-routing/public/hello%20copy.txt similarity index 100% rename from test/integration/dynamic-routing/public/hello%20copy.txt rename to test/e2e/dynamic-routing/public/hello%20copy.txt diff --git a/test/integration/dynamic-routing/public/hello+copy.txt b/test/e2e/dynamic-routing/public/hello+copy.txt similarity index 100% rename from test/integration/dynamic-routing/public/hello+copy.txt rename to test/e2e/dynamic-routing/public/hello+copy.txt diff --git a/test/integration/dynamic-routing/public/hello.txt b/test/e2e/dynamic-routing/public/hello.txt similarity index 100% rename from test/integration/dynamic-routing/public/hello.txt rename to test/e2e/dynamic-routing/public/hello.txt diff --git a/test/integration/dynamic-routing/test/index.test.ts b/test/e2e/dynamic-routing/shared.ts similarity index 53% rename from test/integration/dynamic-routing/test/index.test.ts rename to test/e2e/dynamic-routing/shared.ts index b89fcf32fab1..f0a22997679d 100644 --- a/test/integration/dynamic-routing/test/index.test.ts +++ b/test/e2e/dynamic-routing/shared.ts @@ -1,43 +1,28 @@ -/* eslint-env jest */ - -import webdriver, { type Playwright } from 'next-webdriver' -import { join, dirname } from 'path' -import fs from 'fs-extra' +import { isNextStart } from 'e2e-utils' import { waitForRedbox, - renderViaHTTP, - fetchViaHTTP, - findPort, - launchApp, - killApp, - waitFor, - nextBuild, - nextStart, - normalizeRegEx, - check, getRedboxHeader, + normalizeRegEx, normalizeManifest, retry, } from 'next-test-utils' -import cheerio from 'cheerio' -let app -let appPort -let buildId -const appDir = join(__dirname, '../') -const buildIdPath = join(appDir, '.next/BUILD_ID') +export function runTests(ctx: { + next: any + isNextDev: boolean + isTurbopack: boolean + middlewareEnabled: boolean +}) { + const { next, isNextDev, isTurbopack, middlewareEnabled } = ctx -function runTests({ dev }) { - if (!dev) { + if (isNextStart) { it('should have correct cache entries on prefetch', async () => { - const browser = await webdriver(appPort, '/') + const browser = await next.browser('/') await browser.waitForCondition('!!window.next.router.isReady') const getCacheKeys = async () => { return (await browser.eval('Object.keys(window.next.router.sdc)')) - .map((key) => { - // strip http://localhost:PORT - // and then strip buildId prefix + .map((key: string) => { return key .substring(key.indexOf('/_next')) .replace(/\/_next\/data\/(.*?)\//, '/_next/data/BUILD_ID/') @@ -45,39 +30,44 @@ function runTests({ dev }) { .sort() } - const cacheKeys = await getCacheKeys() - expect(cacheKeys).toEqual( - process.env.__MIDDLEWARE_TEST - ? [ - '/_next/data/BUILD_ID/[name].json?another=value&name=%5Bname%5D', - '/_next/data/BUILD_ID/added-later/first.json?name=added-later&comment=first', - '/_next/data/BUILD_ID/blog/321/comment/123.json?name=321&id=123', - '/_next/data/BUILD_ID/d/dynamic-1.json?id=dynamic-1', - '/_next/data/BUILD_ID/on-mount/test-w-hash.json?post=test-w-hash', - '/_next/data/BUILD_ID/p1/p2/all-ssg/hello.json?rest=hello', - '/_next/data/BUILD_ID/p1/p2/all-ssg/hello1/hello2.json?rest=hello1&rest=hello2', - '/_next/data/BUILD_ID/p1/p2/all-ssr/:42.json?rest=%3A42', - '/_next/data/BUILD_ID/p1/p2/all-ssr/hello.json?rest=hello', - '/_next/data/BUILD_ID/p1/p2/all-ssr/hello1%2F/he%2Fllo2.json?rest=hello1%2F&rest=he%2Fllo2', - '/_next/data/BUILD_ID/p1/p2/all-ssr/hello1/hello2.json?rest=hello1&rest=hello2', - '/_next/data/BUILD_ID/p1/p2/nested-all-ssg/hello.json?rest=hello', - '/_next/data/BUILD_ID/p1/p2/nested-all-ssg/hello1/hello2.json?rest=hello1&rest=hello2', - '/_next/data/BUILD_ID/post-1.json?fromHome=true&name=post-1', - '/_next/data/BUILD_ID/post-1.json?hidden=value&name=post-1', - '/_next/data/BUILD_ID/post-1.json?name=post-1', - '/_next/data/BUILD_ID/post-1.json?name=post-1&another=value', - '/_next/data/BUILD_ID/post-1/comment-1.json?name=post-1&comment=comment-1', - '/_next/data/BUILD_ID/post-1/comments.json?name=post-1', - ] - : [ - '/_next/data/BUILD_ID/p1/p2/all-ssg/hello.json?rest=hello', - '/_next/data/BUILD_ID/p1/p2/all-ssg/hello1/hello2.json?rest=hello1&rest=hello2', - '/_next/data/BUILD_ID/p1/p2/nested-all-ssg/hello.json?rest=hello', - '/_next/data/BUILD_ID/p1/p2/nested-all-ssg/hello1/hello2.json?rest=hello1&rest=hello2', - ] - ) + const expectedCacheKeys = middlewareEnabled + ? [ + '/_next/data/BUILD_ID/[name].json?another=value&name=%5Bname%5D', + '/_next/data/BUILD_ID/added-later/first.json?name=added-later&comment=first', + '/_next/data/BUILD_ID/blog/321/comment/123.json?name=321&id=123', + '/_next/data/BUILD_ID/d/dynamic-1.json?id=dynamic-1', + '/_next/data/BUILD_ID/on-mount/test-w-hash.json?post=test-w-hash', + '/_next/data/BUILD_ID/p1/p2/all-ssg/hello.json?rest=hello', + '/_next/data/BUILD_ID/p1/p2/all-ssg/hello1/hello2.json?rest=hello1&rest=hello2', + '/_next/data/BUILD_ID/p1/p2/all-ssr/:42.json?rest=%3A42', + '/_next/data/BUILD_ID/p1/p2/all-ssr/hello.json?rest=hello', + '/_next/data/BUILD_ID/p1/p2/all-ssr/hello1%2F/he%2Fllo2.json?rest=hello1%2F&rest=he%2Fllo2', + '/_next/data/BUILD_ID/p1/p2/all-ssr/hello1/hello2.json?rest=hello1&rest=hello2', + '/_next/data/BUILD_ID/p1/p2/nested-all-ssg/hello.json?rest=hello', + '/_next/data/BUILD_ID/p1/p2/nested-all-ssg/hello1/hello2.json?rest=hello1&rest=hello2', + '/_next/data/BUILD_ID/post-1.json?fromHome=true&name=post-1', + '/_next/data/BUILD_ID/post-1.json?hidden=value&name=post-1', + '/_next/data/BUILD_ID/post-1.json?name=post-1', + '/_next/data/BUILD_ID/post-1.json?name=post-1&another=value', + '/_next/data/BUILD_ID/post-1/comment-1.json?name=post-1&comment=comment-1', + '/_next/data/BUILD_ID/post-1/comments.json?name=post-1', + ] + : [ + '/_next/data/BUILD_ID/p1/p2/all-ssg/hello.json?rest=hello', + '/_next/data/BUILD_ID/p1/p2/all-ssg/hello1/hello2.json?rest=hello1&rest=hello2', + '/_next/data/BUILD_ID/p1/p2/nested-all-ssg/hello.json?rest=hello', + '/_next/data/BUILD_ID/p1/p2/nested-all-ssg/hello1/hello2.json?rest=hello1&rest=hello2', + ] + + // Prefetches land asynchronously after hydration. In webpack production + // builds assets load differently than Turbopack so retry until the + // expected set is present. + await retry(async () => { + expect(await getCacheKeys()).toEqual(expectedCacheKeys) + }) + + const cacheKeys = expectedCacheKeys - // ensure no new cache entries after navigation const links = [ { linkSelector: '#ssg-catch-all-single', @@ -118,21 +108,16 @@ function runTests({ dev }) { const newCacheKeys = await getCacheKeys() expect(newCacheKeys).toEqual( [ - ...(process.env.__MIDDLEWARE_TEST - ? // data route is fetched with middleware due to query hydration - // since middleware matches the index route - ['/_next/data/BUILD_ID/index.json'] - : []), + ...(middlewareEnabled ? ['/_next/data/BUILD_ID/index.json'] : []), ...cacheKeys, ].sort() ) }) } - if (dev) { - // TODO: pong event not longer exist, refactor test. + if (isNextDev) { it.skip('should not have error after pinging WebSocket', async () => { - const browser = await webdriver(appPort, '/') + const browser = await next.browser('/') await browser.eval(`(function() { window.uncaughtErrs = [] window.addEventListener('uncaughtexception', function (err) { @@ -140,28 +125,25 @@ function runTests({ dev }) { }) })()`) const curFrames = [...(await browser.websocketFrames())] - await check(async () => { + await retry(async () => { const frames = await browser.websocketFrames() const newFrames = frames.slice(curFrames.length) - // console.error({newFrames, curFrames, frames}); - return newFrames.some((frame) => { + const found = newFrames.some((frame) => { try { const data = JSON.parse('' + frame.payload) return data.event === 'pong' } catch (_) {} return false }) - ? 'success' - : JSON.stringify(newFrames) - }, 'success') + expect(found).toBe(true) + }) expect(await browser.eval('window.uncaughtErrs.length')).toBe(0) }) } it('should support long URLs for dynamic routes', async () => { - const res = await fetchViaHTTP( - appPort, + const res = await next.fetch( '/dash/a9btBxtHQALZ6cxfuj18X6OLGNSkJVzrOXz41HG4QwciZfn7ggRZzPx21dWqGiTBAqFRiWvVNm5ko2lpyso5jtVaXg88dC1jKfqI2qmIcdeyJat8xamrIh2LWnrYRrsBcoKfQU65KHod8DPANuzPS3fkVYWlmov05GQbc82HwR1exOvPVKUKb5gBRWiN0WOh7hN4QyezIuq3dJINAptFQ6m2bNGjYACBRk4MOSHdcQG58oq5Ch7luuqrl9EcbWSa' ) @@ -172,7 +154,7 @@ function runTests({ dev }) { }) it('should handle only query on dynamic route', async () => { - const browser = await webdriver(appPort, '/post-1') + const browser = await next.browser('/post-1') for (const expectedValues of [ { @@ -264,7 +246,7 @@ function runTests({ dev }) { }) it('should handle only hash on dynamic route', async () => { - const browser = await webdriver(appPort, '/post-1') + const browser = await next.browser('/post-1') const parsedHref = new URL( await browser .elementByCss('#dynamic-route-only-hash') @@ -295,7 +277,7 @@ function runTests({ dev }) { }) it('should navigate with hash to dynamic route with link', async () => { - const browser = await webdriver(appPort, '/') + const browser = await next.browser('/') await browser.eval('window.beforeNav = 1') await browser @@ -346,7 +328,7 @@ function runTests({ dev }) { }) it('should navigate with hash to dynamic route with router', async () => { - const browser = await webdriver(appPort, '/') + const browser = await next.browser('/') await browser.eval(`(function() { window.beforeNav = 1 window.next.router.push('/[name]', '/post-1#my-hash') @@ -405,646 +387,521 @@ function runTests({ dev }) { }) it('should not have any query values when not defined', async () => { - const html = await renderViaHTTP(appPort, '/') - const $ = cheerio.load(html) + const $ = await next.render$('/') expect(JSON.parse($('#query').text())).toEqual([]) }) it('should render normal route', async () => { - const html = await renderViaHTTP(appPort, '/') + const html = await next.render('/') expect(html).toMatch(/my blog/i) }) it('should render another normal route', async () => { - const html = await renderViaHTTP(appPort, '/another') + const html = await next.render('/another') expect(html).toMatch(/hello from another/) }) it('should render dynamic page', async () => { - const html = await renderViaHTTP(appPort, '/post-1') + const html = await next.render('/post-1') expect(html).toMatch(/this is.*?post-1/i) }) it('should prioritize a non-dynamic page', async () => { - const html = await renderViaHTTP(appPort, '/post-1/comments') + const html = await next.render('/post-1/comments') expect(html).toMatch(/show comments for.*post-1.*here/i) }) it('should render nested dynamic page', async () => { - const html = await renderViaHTTP(appPort, '/post-1/comment-1') + const html = await next.render('/post-1/comment-1') expect(html).toMatch(/i am.*comment-1.*on.*post-1/i) }) it('should render optional dynamic page', async () => { - const html = await renderViaHTTP(appPort, '/blog/543/comment') - // expect(html).toMatch(/blog post.*543.*comment.*all/i) + const html = await next.render('/blog/543/comment') expect(html).toMatch(/404/i) }) it('should render nested optional dynamic page', async () => { - const html = await renderViaHTTP(appPort, '/blog/321/comment/123') + const html = await next.render('/blog/321/comment/123') expect(html).toMatch(/blog post.*321.*comment.*123/i) }) it('should not error when requesting dynamic page with /api', async () => { - const res = await fetchViaHTTP(appPort, '/api') + const res = await next.fetch('/api') expect(res.status).toBe(200) expect(await res.text()).toMatch(/this is.*?api/i) }) it('should render dynamic route with query', async () => { - const browser = await webdriver(appPort, '/') + const browser = await next.browser('/') await browser.elementByCss('#view-post-1-with-query').click() - await waitFor(1000) - const url = await browser.eval('window.location.search') - expect(url).toBe('?fromHome=true') + await retry(async () => { + const url = await browser.eval('window.location.search') + expect(url).toBe('?fromHome=true') + }) }) - if (dev) { + if (isNextDev) { it('should not have any console warnings on initial load', async () => { - const browser = await webdriver(appPort, '/') + const browser = await next.browser('/') expect(await browser.eval('window.caughtWarns')).toEqual([]) }) it('should not have any console warnings when navigating to dynamic route', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - await browser.eval('window.beforeNav = 1') - await browser.elementByCss('#dynamic-route-no-as').click() - await browser.waitForElementByCss('#asdf') + const browser = await next.browser('/') + await browser.eval('window.beforeNav = 1') + await browser.elementByCss('#dynamic-route-no-as').click() + await browser.waitForElementByCss('#asdf') - expect(await browser.eval('window.beforeNav')).toBe(1) + expect(await browser.eval('window.beforeNav')).toBe(1) - const text = await browser.elementByCss('#asdf').text() - expect(text).toMatch(/this is.*?dynamic-1/i) - expect(await browser.eval('window.caughtWarns')).toEqual([]) - } finally { - if (browser) await browser.close() - } + const text = await browser.elementByCss('#asdf').text() + expect(text).toMatch(/this is.*?dynamic-1/i) + expect(await browser.eval('window.caughtWarns')).toEqual([]) }) } it('should navigate to a dynamic page successfully', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - await browser.eval('window.beforeNav = 1') - await browser.elementByCss('#view-post-1').click() - await browser.waitForElementByCss('#asdf') + const browser = await next.browser('/') + await browser.eval('window.beforeNav = 1') + await browser.elementByCss('#view-post-1').click() + await browser.waitForElementByCss('#asdf') - expect(await browser.eval('window.beforeNav')).toBe(1) + expect(await browser.eval('window.beforeNav')).toBe(1) - const text = await browser.elementByCss('#asdf').text() - expect(text).toMatch(/this is.*?post-1/i) - } finally { - if (browser) await browser.close() - } + const text = await browser.elementByCss('#asdf').text() + expect(text).toMatch(/this is.*?post-1/i) }) it('should navigate to a dynamic page with href with differing query and as correctly', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - await browser.eval('window.beforeNav = 1') - await browser.elementByCss('#view-post-1-hidden-query').click() - await browser.waitForElementByCss('#asdf') + const browser = await next.browser('/') + await browser.eval('window.beforeNav = 1') + await browser.elementByCss('#view-post-1-hidden-query').click() + await browser.waitForElementByCss('#asdf') - expect(await browser.eval('window.beforeNav')).toBe(1) + expect(await browser.eval('window.beforeNav')).toBe(1) - const text = await browser.elementByCss('#asdf').text() - expect(text).toMatch(/this is.*?post-1/i) - } finally { - if (browser) await browser.close() - } + const text = await browser.elementByCss('#asdf').text() + expect(text).toMatch(/this is.*?post-1/i) }) it('should navigate to a dynamic page successfully no as', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - await browser.eval('window.beforeNav = 1') - await browser.elementByCss('#view-post-1-no-as').click() - await browser.waitForElementByCss('#asdf') + const browser = await next.browser('/') + await browser.eval('window.beforeNav = 1') + await browser.elementByCss('#view-post-1-no-as').click() + await browser.waitForElementByCss('#asdf') - expect(await browser.eval('window.beforeNav')).toBe(1) + expect(await browser.eval('window.beforeNav')).toBe(1) - const text = await browser.elementByCss('#asdf').text() - expect(text).toMatch(/this is.*?post-1/i) - } finally { - if (browser) await browser.close() - } + const text = await browser.elementByCss('#asdf').text() + expect(text).toMatch(/this is.*?post-1/i) }) it('should navigate to a dynamic page successfully interpolated', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - await browser.eval('window.beforeNav = 1') + const browser = await next.browser('/') + await browser.eval('window.beforeNav = 1') - const href = await browser - .elementByCss('#view-post-1-interpolated') - .getAttribute('href') + const href = await browser + .elementByCss('#view-post-1-interpolated') + .getAttribute('href') - const parsedHref = new URL(href, await browser.url()) - expect(parsedHref.pathname).toBe('/post-1') - expect(Object.fromEntries(parsedHref.searchParams.entries())).toEqual({}) + const parsedHref = new URL(href, await browser.url()) + expect(parsedHref.pathname).toBe('/post-1') + expect(Object.fromEntries(parsedHref.searchParams.entries())).toEqual({}) - await browser.elementByCss('#view-post-1-interpolated').click() - await browser.waitForElementByCss('#asdf') + await browser.elementByCss('#view-post-1-interpolated').click() + await browser.waitForElementByCss('#asdf') - expect(await browser.eval('window.beforeNav')).toBe(1) + expect(await browser.eval('window.beforeNav')).toBe(1) - const text = await browser.elementByCss('#asdf').text() - expect(text).toMatch(/this is.*?post-1/i) - } finally { - if (browser) await browser.close() - } + const text = await browser.elementByCss('#asdf').text() + expect(text).toMatch(/this is.*?post-1/i) }) it('should navigate to a dynamic page successfully interpolated with additional query values', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - await browser.eval('window.beforeNav = 1') + const browser = await next.browser('/') + await browser.eval('window.beforeNav = 1') - const href = await browser - .elementByCss('#view-post-1-interpolated-more-query') - .getAttribute('href') + const href = await browser + .elementByCss('#view-post-1-interpolated-more-query') + .getAttribute('href') - const parsedHref = new URL(href, await browser.url()) - expect(parsedHref.pathname).toBe('/post-1') - expect(Object.fromEntries(parsedHref.searchParams.entries())).toEqual({ - another: 'value', - }) - await browser.elementByCss('#view-post-1-interpolated-more-query').click() - await browser.waitForElementByCss('#asdf') + const parsedHref = new URL(href, await browser.url()) + expect(parsedHref.pathname).toBe('/post-1') + expect(Object.fromEntries(parsedHref.searchParams.entries())).toEqual({ + another: 'value', + }) + await browser.elementByCss('#view-post-1-interpolated-more-query').click() + await browser.waitForElementByCss('#asdf') - expect(await browser.eval('window.beforeNav')).toBe(1) + expect(await browser.eval('window.beforeNav')).toBe(1) - const text = await browser.elementByCss('#asdf').text() - expect(text).toMatch(/this is.*?post-1/i) + const text = await browser.elementByCss('#asdf').text() + expect(text).toMatch(/this is.*?post-1/i) - const query = JSON.parse(await browser.elementByCss('#query').text()) - expect(query).toEqual({ - name: 'post-1', - another: 'value', - }) - } finally { - if (browser) await browser.close() - } + const query = JSON.parse(await browser.elementByCss('#query').text()) + expect(query).toEqual({ + name: 'post-1', + another: 'value', + }) }) it('should allow calling Router.push on mount successfully', async () => { - const browser = await webdriver(appPort, '/post-1/on-mount-redir') - try { - expect(await browser.waitForElementByCss('h3').text()).toBe('My blog') - } finally { - browser.close() - } + const browser = await next.browser('/post-1/on-mount-redir') + expect(await browser.waitForElementByCss('h3').text()).toBe('My blog') }) it('should navigate optional dynamic page', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - await browser.elementByCss('#view-post-1-comments').click() - await browser.waitForElementByCss('#asdf') + const browser = await next.browser('/') + await browser.elementByCss('#view-post-1-comments').click() + await browser.waitForElementByCss('#asdf') - const text = await browser.elementByCss('#asdf').text() - expect(text).toMatch(/comments for post-1 here/i) - } finally { - if (browser) await browser.close() - } + const text = await browser.elementByCss('#asdf').text() + expect(text).toMatch(/comments for post-1 here/i) }) it('should navigate optional dynamic page with value', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - await browser.elementByCss('#view-nested-dynamic-cmnt').click() - await browser.waitForElementByCss('#asdf') + const browser = await next.browser('/') + await browser.elementByCss('#view-nested-dynamic-cmnt').click() + await browser.waitForElementByCss('#asdf') - const text = await browser.elementByCss('#asdf').text() - expect(text).toMatch(/blog post.*321.*comment.*123/i) - } finally { - if (browser) await browser.close() - } + const text = await browser.elementByCss('#asdf').text() + expect(text).toMatch(/blog post.*321.*comment.*123/i) }) it('should navigate to a nested dynamic page successfully', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - await browser.eval('window.beforeNav = 1') - await browser.elementByCss('#view-post-1-comment-1').click() - await browser.waitForElementByCss('#asdf') + const browser = await next.browser('/') + await browser.eval('window.beforeNav = 1') + await browser.elementByCss('#view-post-1-comment-1').click() + await browser.waitForElementByCss('#asdf') - expect(await browser.eval('window.beforeNav')).toBe(1) + expect(await browser.eval('window.beforeNav')).toBe(1) - const text = await browser.elementByCss('#asdf').text() - expect(text).toMatch(/i am.*comment-1.*on.*post-1/i) - } finally { - if (browser) await browser.close() - } + const text = await browser.elementByCss('#asdf').text() + expect(text).toMatch(/i am.*comment-1.*on.*post-1/i) }) it('should navigate to a nested dynamic page successfully no as', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - await browser.eval('window.beforeNav = 1') - await browser.elementByCss('#view-post-1-comment-1-no-as').click() - await browser.waitForElementByCss('#asdf') + const browser = await next.browser('/') + await browser.eval('window.beforeNav = 1') + await browser.elementByCss('#view-post-1-comment-1-no-as').click() + await browser.waitForElementByCss('#asdf') - expect(await browser.eval('window.beforeNav')).toBe(1) + expect(await browser.eval('window.beforeNav')).toBe(1) - const text = await browser.elementByCss('#asdf').text() - expect(text).toMatch(/i am.*comment-1.*on.*post-1/i) - } finally { - if (browser) await browser.close() - } + const text = await browser.elementByCss('#asdf').text() + expect(text).toMatch(/i am.*comment-1.*on.*post-1/i) }) it('should navigate to a nested dynamic page successfully interpolated', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - await browser.eval('window.beforeNav = 1') + const browser = await next.browser('/') + await browser.eval('window.beforeNav = 1') - const href = await browser - .elementByCss('#view-post-1-comment-1-interpolated') - .getAttribute('href') + const href = await browser + .elementByCss('#view-post-1-comment-1-interpolated') + .getAttribute('href') - expect(new URL(href, await browser.url()).pathname).toBe( - '/post-1/comment-1' - ) + expect(new URL(href, await browser.url()).pathname).toBe( + '/post-1/comment-1' + ) - await browser.elementByCss('#view-post-1-comment-1-interpolated').click() - await browser.waitForElementByCss('#asdf') + await browser.elementByCss('#view-post-1-comment-1-interpolated').click() + await browser.waitForElementByCss('#asdf') - expect(await browser.eval('window.beforeNav')).toBe(1) + expect(await browser.eval('window.beforeNav')).toBe(1) - const text = await browser.elementByCss('#asdf').text() - expect(text).toMatch(/i am.*comment-1.*on.*post-1/i) - } finally { - if (browser) await browser.close() - } + const text = await browser.elementByCss('#asdf').text() + expect(text).toMatch(/i am.*comment-1.*on.*post-1/i) }) it('should pass params in getInitialProps during SSR', async () => { - const html = await renderViaHTTP(appPort, '/post-1/cmnt-1') + const html = await next.render('/post-1/cmnt-1') expect(html).toMatch(/gip.*post-1/i) }) it('should pass params in getInitialProps during client navigation', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - - await browser.elementByCss('#view-post-1-comment-1').click() - const text = await browser - .elementByCss('[data-testid="gip-query"]') - .text() - - expect(text).toMatch(/gip.*post-1/i) - } finally { - if (browser) await browser.close() - } + const browser = await next.browser('/') + + await browser.elementByCss('#view-post-1-comment-1').click() + const text = await browser.elementByCss('[data-testid="gip-query"]').text() + + expect(text).toMatch(/gip.*post-1/i) }) it('[catch all] should not match root on SSR', async () => { - const res = await fetchViaHTTP(appPort, '/p1/p2/all-ssr') + const res = await next.fetch('/p1/p2/all-ssr') expect(res.status).toBe(404) }) it('[catch all] should pass param in getInitialProps during SSR', async () => { - const html = await renderViaHTTP(appPort, '/p1/p2/all-ssr/test1') - const $ = cheerio.load(html) + const $ = await next.render$('/p1/p2/all-ssr/test1') expect($('#all-ssr-content').text()).toBe('{"rest":["test1"]}') }) it('[catch all] should pass params in getInitialProps during SSR', async () => { - const html = await renderViaHTTP(appPort, '/p1/p2/all-ssr/test1/test2') - const $ = cheerio.load(html) + const $ = await next.render$('/p1/p2/all-ssr/test1/test2') expect($('#all-ssr-content').text()).toBe('{"rest":["test1","test2"]}') }) it('[catch all] should strip trailing slash', async () => { - const html = await renderViaHTTP(appPort, '/p1/p2/all-ssr/test1/test2/') - const $ = cheerio.load(html) + const $ = await next.render$('/p1/p2/all-ssr/test1/test2/') expect($('#all-ssr-content').text()).toBe('{"rest":["test1","test2"]}') }) it('[catch all] should not decode slashes (start)', async () => { - const html = await renderViaHTTP(appPort, '/p1/p2/all-ssr/test1/%2Ftest2') - const $ = cheerio.load(html) + const $ = await next.render$('/p1/p2/all-ssr/test1/%2Ftest2') expect($('#all-ssr-content').text()).toBe('{"rest":["test1","/test2"]}') }) it('[catch all] should not decode slashes (end)', async () => { - const html = await renderViaHTTP(appPort, '/p1/p2/all-ssr/test1%2F/test2') - const $ = cheerio.load(html) + const $ = await next.render$('/p1/p2/all-ssr/test1%2F/test2') expect($('#all-ssr-content').text()).toBe('{"rest":["test1/","test2"]}') }) it('[catch all] should not decode slashes (middle)', async () => { - const html = await renderViaHTTP(appPort, '/p1/p2/all-ssr/test1/te%2Fst2') - const $ = cheerio.load(html) + const $ = await next.render$('/p1/p2/all-ssr/test1/te%2Fst2') expect($('#all-ssr-content').text()).toBe('{"rest":["test1","te/st2"]}') }) it('[catch-all] should pass params in getInitialProps during client navigation (single)', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - await browser.elementByCss('#catch-all-single').click() - await browser.waitForElementByCss('#all-ssr-content') - - const text = await browser.elementByCss('#all-ssr-content').text() - expect(text).toBe('{"rest":["hello"]}') - } finally { - if (browser) await browser.close() - } + const browser = await next.browser('/') + await browser.elementByCss('#catch-all-single').click() + await browser.waitForElementByCss('#all-ssr-content') + + const text = await browser.elementByCss('#all-ssr-content').text() + expect(text).toBe('{"rest":["hello"]}') }) it('[catch-all] should pass params in getInitialProps during client navigation (multi)', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - await browser.elementByCss('#catch-all-multi').click() - await browser.waitForElementByCss('#all-ssr-content') - - const text = await browser.elementByCss('#all-ssr-content').text() - expect(text).toBe('{"rest":["hello1","hello2"]}') - } finally { - if (browser) await browser.close() - } + const browser = await next.browser('/') + await browser.elementByCss('#catch-all-multi').click() + await browser.waitForElementByCss('#all-ssr-content') + + const text = await browser.elementByCss('#all-ssr-content').text() + expect(text).toBe('{"rest":["hello1","hello2"]}') }) it('[catch-all] should pass params in getInitialProps during client navigation (encoded)', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - await browser.elementByCss('#catch-all-enc').click() - await browser.waitForElementByCss('#all-ssr-content') - - const text = await browser.elementByCss('#all-ssr-content').text() - expect(text).toBe('{"rest":["hello1/","he/llo2"]}') - } finally { - if (browser) await browser.close() - } + const browser = await next.browser('/') + await browser.elementByCss('#catch-all-enc').click() + await browser.waitForElementByCss('#all-ssr-content') + + const text = await browser.elementByCss('#all-ssr-content').text() + expect(text).toBe('{"rest":["hello1/","he/llo2"]}') }) it("[catch-all] shouldn't fail on colon followed by double digits in the path", async () => { - // https://github.com/GoogleChromeLabs/native-url/issues/27 - let browser - try { - browser = await webdriver(appPort, '/') - await browser.elementByCss('#catch-all-colonnumber').click() - await browser.waitForElementByCss('#all-ssr-content') - - const text = await browser.elementByCss('#all-ssr-content').text() - expect(text).toBe('{"rest":[":42"]}') - } finally { - if (browser) await browser.close() - } - }) + const browser = await next.browser('/') + await browser.elementByCss('#catch-all-colonnumber').click() + await browser.waitForElementByCss('#all-ssr-content') - it('[ssg: catch all] should pass param in getStaticProps during SSR', async () => { - const data = await renderViaHTTP( - appPort, - `/_next/data/${buildId}/p1/p2/all-ssg/test1.json` - ) - expect(JSON.parse(data).pageProps.params).toEqual({ rest: ['test1'] }) + const text = await browser.elementByCss('#all-ssr-content').text() + expect(text).toBe('{"rest":[":42"]}') }) - it('[ssg: catch all] should pass params in getStaticProps during SSR', async () => { - const data = await renderViaHTTP( - appPort, - `/_next/data/${buildId}/p1/p2/all-ssg/test1/test2.json` - ) - expect(JSON.parse(data).pageProps.params).toEqual({ - rest: ['test1', 'test2'], + if (isNextStart) { + it('[ssg: catch all] should pass param in getStaticProps during SSR', async () => { + const buildId = (await next.readFile('.next/BUILD_ID')).trim() + const data = await next.render( + `/_next/data/${buildId}/p1/p2/all-ssg/test1.json` + ) + expect(JSON.parse(data).pageProps.params).toEqual({ rest: ['test1'] }) }) - }) - it('[nested ssg: catch all] should pass param in getStaticProps during SSR', async () => { - const data = await renderViaHTTP( - appPort, - `/_next/data/${buildId}/p1/p2/nested-all-ssg/test1.json` - ) - expect(JSON.parse(data).pageProps.params).toEqual({ rest: ['test1'] }) - }) + it('[ssg: catch all] should pass params in getStaticProps during SSR', async () => { + const buildId = (await next.readFile('.next/BUILD_ID')).trim() + const data = await next.render( + `/_next/data/${buildId}/p1/p2/all-ssg/test1/test2.json` + ) + expect(JSON.parse(data).pageProps.params).toEqual({ + rest: ['test1', 'test2'], + }) + }) - it('[nested ssg: catch all] should pass params in getStaticProps during SSR', async () => { - const data = await renderViaHTTP( - appPort, - `/_next/data/${buildId}/p1/p2/nested-all-ssg/test1/test2.json` - ) - expect(JSON.parse(data).pageProps.params).toEqual({ - rest: ['test1', 'test2'], + it('[nested ssg: catch all] should pass param in getStaticProps during SSR', async () => { + const buildId = (await next.readFile('.next/BUILD_ID')).trim() + const data = await next.render( + `/_next/data/${buildId}/p1/p2/nested-all-ssg/test1.json` + ) + expect(JSON.parse(data).pageProps.params).toEqual({ rest: ['test1'] }) }) - }) - it('[predefined ssg: catch all] should pass param in getStaticProps during SSR', async () => { - const data = await renderViaHTTP( - appPort, - `/_next/data/${buildId}/p1/p2/predefined-ssg/test1.json` - ) - expect(JSON.parse(data).pageProps.params).toEqual({ rest: ['test1'] }) - }) + it('[nested ssg: catch all] should pass params in getStaticProps during SSR', async () => { + const buildId = (await next.readFile('.next/BUILD_ID')).trim() + const data = await next.render( + `/_next/data/${buildId}/p1/p2/nested-all-ssg/test1/test2.json` + ) + expect(JSON.parse(data).pageProps.params).toEqual({ + rest: ['test1', 'test2'], + }) + }) - it('[predefined ssg: catch all] should pass params in getStaticProps during SSR', async () => { - const data = await renderViaHTTP( - appPort, - `/_next/data/${buildId}/p1/p2/predefined-ssg/test1/test2.json` - ) - expect(JSON.parse(data).pageProps.params).toEqual({ - rest: ['test1', 'test2'], + it('[predefined ssg: catch all] should pass param in getStaticProps during SSR', async () => { + const buildId = (await next.readFile('.next/BUILD_ID')).trim() + const data = await next.render( + `/_next/data/${buildId}/p1/p2/predefined-ssg/test1.json` + ) + expect(JSON.parse(data).pageProps.params).toEqual({ rest: ['test1'] }) }) - }) - it('[predefined ssg: prerendered catch all] should pass param in getStaticProps during SSR', async () => { - const data = await renderViaHTTP( - appPort, - `/_next/data/${buildId}/p1/p2/predefined-ssg/one-level.json` - ) - expect(JSON.parse(data).pageProps.params).toEqual({ rest: ['one-level'] }) - }) + it('[predefined ssg: catch all] should pass params in getStaticProps during SSR', async () => { + const buildId = (await next.readFile('.next/BUILD_ID')).trim() + const data = await next.render( + `/_next/data/${buildId}/p1/p2/predefined-ssg/test1/test2.json` + ) + expect(JSON.parse(data).pageProps.params).toEqual({ + rest: ['test1', 'test2'], + }) + }) - it('[predefined ssg: prerendered catch all] should pass params in getStaticProps during SSR', async () => { - const data = await renderViaHTTP( - appPort, - `/_next/data/${buildId}/p1/p2/predefined-ssg/1st-level/2nd-level.json` - ) - expect(JSON.parse(data).pageProps.params).toEqual({ - rest: ['1st-level', '2nd-level'], + it('[predefined ssg: prerendered catch all] should pass param in getStaticProps during SSR', async () => { + const buildId = (await next.readFile('.next/BUILD_ID')).trim() + const data = await next.render( + `/_next/data/${buildId}/p1/p2/predefined-ssg/one-level.json` + ) + expect(JSON.parse(data).pageProps.params).toEqual({ + rest: ['one-level'], + }) }) - }) + + it('[predefined ssg: prerendered catch all] should pass params in getStaticProps during SSR', async () => { + const buildId = (await next.readFile('.next/BUILD_ID')).trim() + const data = await next.render( + `/_next/data/${buildId}/p1/p2/predefined-ssg/1st-level/2nd-level.json` + ) + expect(JSON.parse(data).pageProps.params).toEqual({ + rest: ['1st-level', '2nd-level'], + }) + }) + } it('[ssg: catch-all] should pass params in getStaticProps during client navigation (single)', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - await browser.eval('window.beforeNav = 1') - await browser.elementByCss('#ssg-catch-all-single').click() - await browser.waitForElementByCss('#all-ssg-content') + const browser = await next.browser('/') + await browser.eval('window.beforeNav = 1') + await browser.elementByCss('#ssg-catch-all-single').click() + await browser.waitForElementByCss('#all-ssg-content') - expect(await browser.eval('window.beforeNav')).toBe(1) + expect(await browser.eval('window.beforeNav')).toBe(1) - const text = await browser.elementByCss('#all-ssg-content').text() - expect(text).toBe('{"rest":["hello"]}') - } finally { - if (browser) await browser.close() - } + const text = await browser.elementByCss('#all-ssg-content').text() + expect(text).toBe('{"rest":["hello"]}') }) it('[ssg: catch-all] should pass params in getStaticProps during client navigation (single interpolated)', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - await browser.eval('window.beforeNav = 1') + const browser = await next.browser('/') + await browser.eval('window.beforeNav = 1') - const href = await browser - .elementByCss('#ssg-catch-all-single-interpolated') - .getAttribute('href') + const href = await browser + .elementByCss('#ssg-catch-all-single-interpolated') + .getAttribute('href') - expect(new URL(href, await browser.url()).pathname).toBe( - '/p1/p2/all-ssg/hello' - ) + expect(new URL(href, await browser.url()).pathname).toBe( + '/p1/p2/all-ssg/hello' + ) - await browser.elementByCss('#ssg-catch-all-single-interpolated').click() - await browser.waitForElementByCss('#all-ssg-content') + await browser.elementByCss('#ssg-catch-all-single-interpolated').click() + await browser.waitForElementByCss('#all-ssg-content') - expect(await browser.eval('window.beforeNav')).toBe(1) + expect(await browser.eval('window.beforeNav')).toBe(1) - const text = await browser.elementByCss('#all-ssg-content').text() - expect(text).toBe('{"rest":["hello"]}') - } finally { - if (browser) await browser.close() - } + const text = await browser.elementByCss('#all-ssg-content').text() + expect(text).toBe('{"rest":["hello"]}') }) it('[ssg: catch-all] should pass params in getStaticProps during client navigation (multi)', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - await browser.eval('window.beforeNav = 1') - await browser.elementByCss('#ssg-catch-all-multi').click() - await browser.waitForElementByCss('#all-ssg-content') + const browser = await next.browser('/') + await browser.eval('window.beforeNav = 1') + await browser.elementByCss('#ssg-catch-all-multi').click() + await browser.waitForElementByCss('#all-ssg-content') - expect(await browser.eval('window.beforeNav')).toBe(1) + expect(await browser.eval('window.beforeNav')).toBe(1) - const text = await browser.elementByCss('#all-ssg-content').text() - expect(text).toBe('{"rest":["hello1","hello2"]}') - } finally { - if (browser) await browser.close() - } + const text = await browser.elementByCss('#all-ssg-content').text() + expect(text).toBe('{"rest":["hello1","hello2"]}') }) it('[ssg: catch-all] should pass params in getStaticProps during client navigation (multi) no as', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - await browser.eval('window.beforeNav = 1') - await browser.elementByCss('#ssg-catch-all-multi-no-as').click() - await browser.waitForElementByCss('#all-ssg-content') + const browser = await next.browser('/') + await browser.eval('window.beforeNav = 1') + await browser.elementByCss('#ssg-catch-all-multi-no-as').click() + await browser.waitForElementByCss('#all-ssg-content') - expect(await browser.eval('window.beforeNav')).toBe(1) + expect(await browser.eval('window.beforeNav')).toBe(1) - const text = await browser.elementByCss('#all-ssg-content').text() - expect(text).toBe('{"rest":["hello1","hello2"]}') - } finally { - if (browser) await browser.close() - } + const text = await browser.elementByCss('#all-ssg-content').text() + expect(text).toBe('{"rest":["hello1","hello2"]}') }) it('[ssg: catch-all] should pass params in getStaticProps during client navigation (multi interpolated)', async () => { - let browser: Playwright - try { - browser = await webdriver(appPort, '/') - await browser.eval('window.beforeNav = 1') + const browser = await next.browser('/') + await browser.eval('window.beforeNav = 1') - const href = await browser - .elementByCss('#ssg-catch-all-multi-interpolated') - .getAttribute('href') + const href = await browser + .elementByCss('#ssg-catch-all-multi-interpolated') + .getAttribute('href') - expect(new URL(href, await browser.url()).pathname).toBe( - '/p1/p2/all-ssg/hello1/hello2' - ) + expect(new URL(href, await browser.url()).pathname).toBe( + '/p1/p2/all-ssg/hello1/hello2' + ) - await browser.elementByCss('#ssg-catch-all-multi-interpolated').click() - await browser.waitForElementByCss('#all-ssg-content') + await browser.elementByCss('#ssg-catch-all-multi-interpolated').click() + await browser.waitForElementByCss('#all-ssg-content') - expect(await browser.eval('window.beforeNav')).toBe(1) + expect(await browser.eval('window.beforeNav')).toBe(1) - const text = await browser.elementByCss('#all-ssg-content').text() - expect(text).toBe('{"rest":["hello1","hello2"]}') - } finally { - if (browser) await browser.close() - } + const text = await browser.elementByCss('#all-ssg-content').text() + expect(text).toBe('{"rest":["hello1","hello2"]}') }) it('[nested ssg: catch-all] should pass params in getStaticProps during client navigation (single)', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - await browser.elementByCss('#nested-ssg-catch-all-single').click() - await browser.waitForElementByCss('#nested-all-ssg-content') - - const text = await browser.elementByCss('#nested-all-ssg-content').text() - expect(text).toBe('{"rest":["hello"]}') - } finally { - if (browser) await browser.close() - } + const browser = await next.browser('/') + await browser.elementByCss('#nested-ssg-catch-all-single').click() + await browser.waitForElementByCss('#nested-all-ssg-content') + + const text = await browser.elementByCss('#nested-all-ssg-content').text() + expect(text).toBe('{"rest":["hello"]}') }) it('[nested ssg: catch-all] should pass params in getStaticProps during client navigation (multi)', async () => { - let browser - try { - browser = await webdriver(appPort, '/') - await browser.elementByCss('#nested-ssg-catch-all-multi').click() - await browser.waitForElementByCss('#nested-all-ssg-content') - - const text = await browser.elementByCss('#nested-all-ssg-content').text() - expect(text).toBe('{"rest":["hello1","hello2"]}') - } finally { - if (browser) await browser.close() - } + const browser = await next.browser('/') + await browser.elementByCss('#nested-ssg-catch-all-multi').click() + await browser.waitForElementByCss('#nested-all-ssg-content') + + const text = await browser.elementByCss('#nested-all-ssg-content').text() + expect(text).toBe('{"rest":["hello1","hello2"]}') }) it('should update dynamic values on mount', async () => { - const html = await renderViaHTTP(appPort, '/on-mount/post-1') + const html = await next.render('/on-mount/post-1') expect(html).toMatch(/onmpost:.*pending/) - const browser = await webdriver(appPort, '/on-mount/post-1') - await check( - () => browser.eval(`document.body.innerHTML`), - /onmpost:.*post-1/ - ) + const browser = await next.browser('/on-mount/post-1') + await retry(async () => { + const innerHTML = await browser.eval(`document.body.innerHTML`) + expect(innerHTML).toMatch(/onmpost:.*post-1/) + }) }) it('should not have placeholder query values for SSS', async () => { - const html = await renderViaHTTP(appPort, '/on-mount/post-1') + const html = await next.render('/on-mount/post-1') expect(html).not.toMatch(/post:.*?\[post\].*?<\/p>/) }) it('should update with a hash in the URL', async () => { - const browser = await webdriver(appPort, '/on-mount/post-1#abc') - await check( - () => browser.eval(`document.body.innerHTML`), - /onmpost:.*post-1/ - ) + const browser = await next.browser('/on-mount/post-1#abc') + await retry(async () => { + const innerHTML = await browser.eval(`document.body.innerHTML`) + expect(innerHTML).toMatch(/onmpost:.*post-1/) + }) }) it('should scroll to a hash on mount', async () => { - const browser = await webdriver(appPort, '/on-mount/post-1#item-400') + const browser = await next.browser('/on-mount/post-1#item-400') - await check( - () => browser.eval(`document.body.innerHTML`), - /onmpost:.*post-1/ - ) + await retry(async () => { + const innerHTML = await browser.eval(`document.body.innerHTML`) + expect(innerHTML).toMatch(/onmpost:.*post-1/) + }) const elementPosition = await browser.eval( `document.querySelector("#item-400").getBoundingClientRect().y` @@ -1053,7 +910,7 @@ function runTests({ dev }) { }) it('should scroll to a hash on client-side navigation', async () => { - const browser = await webdriver(appPort, '/') + const browser = await next.browser('/') await browser.elementByCss('#view-dynamic-with-hash').click() await browser.waitForElementByCss('#asdf') @@ -1067,99 +924,91 @@ function runTests({ dev }) { }) it('should prioritize public files over dynamic route', async () => { - const data = await renderViaHTTP(appPort, '/hello.txt') + const data = await next.render('/hello.txt') expect(data).toMatch(/hello world/) }) it('should serve file with space from public folder', async () => { - const res = await fetchViaHTTP(appPort, '/hello copy.txt') + const res = await next.fetch('/hello copy.txt') const text = (await res.text()).trim() expect(text).toBe('hello world copy') expect(res.status).toBe(200) }) it('should serve file with plus from public folder', async () => { - const res = await fetchViaHTTP(appPort, '/hello+copy.txt') + const res = await next.fetch('/hello+copy.txt') const text = (await res.text()).trim() expect(text).toBe('hello world +') expect(res.status).toBe(200) }) it('should serve file from public folder encoded', async () => { - const res = await fetchViaHTTP(appPort, '/hello%20copy.txt') + const res = await next.fetch('/hello%20copy.txt') const text = (await res.text()).trim() expect(text).toBe('hello world copy') expect(res.status).toBe(200) }) it('should serve file with %20 from public folder', async () => { - const res = await fetchViaHTTP(appPort, '/hello%2520copy.txt') + const res = await next.fetch('/hello%2520copy.txt') const text = (await res.text()).trim() expect(text).toBe('hello world %20') expect(res.status).toBe(200) }) it('should serve file with space from static folder', async () => { - const res = await fetchViaHTTP(appPort, '/static/hello copy.txt') + const res = await next.fetch('/static/hello copy.txt') const text = (await res.text()).trim() expect(text).toBe('hello world copy') expect(res.status).toBe(200) }) it('should serve file with plus from static folder', async () => { - const res = await fetchViaHTTP(appPort, '/static/hello+copy.txt') + const res = await next.fetch('/static/hello+copy.txt') const text = (await res.text()).trim() expect(text).toBe('hello world +') expect(res.status).toBe(200) }) it('should serve file from static folder encoded', async () => { - const res = await fetchViaHTTP(appPort, '/static/hello%20copy.txt') + const res = await next.fetch('/static/hello%20copy.txt') const text = (await res.text()).trim() expect(text).toBe('hello world copy') expect(res.status).toBe(200) }) it('should serve file with %20 from static folder', async () => { - const res = await fetchViaHTTP(appPort, '/static/hello%2520copy.txt') + const res = await next.fetch('/static/hello%2520copy.txt') const text = (await res.text()).trim() expect(text).toBe('hello world %20') expect(res.status).toBe(200) }) it('should respond with bad request with invalid encoding', async () => { - const res = await fetchViaHTTP(appPort, '/%') + const res = await next.fetch('/%') expect(res.status).toBe(400) }) it('should not preload buildManifest for non-auto export dynamic pages', async () => { - const html = await renderViaHTTP(appPort, '/hello') - const $ = cheerio.load(html) + const $ = await next.render$('/hello') let found = 0 for (const el of Array.from($('link[rel="preload"]'))) { - const { href } = el.attribs + const { href } = (el as any).attribs if (href.includes('_buildManifest')) { found++ } } - try { - expect(found).toBe(0) - } catch (err) { - require('console').error(html) - throw err - } + expect(found).toBe(0) }) - if (dev) { + if (isNextDev) { it('should resolve dynamic route href for page added later', async () => { - const browser = await webdriver(appPort, '/') - const addLaterPage = join(appDir, 'pages/added-later/[slug].js') + const browser = await next.browser('/') - await fs.mkdir(dirname(addLaterPage)).catch(() => {}) - await fs.writeFile( - addLaterPage, + await next.patchFile( + 'pages/added-later/[slug].js', ` import { useRouter } from 'next/router' @@ -1168,48 +1017,28 @@ function runTests({ dev }) { } ` ) - await check(async () => { - const response = await fetchViaHTTP( - appPort, - '/_next/static/development/_devPagesManifest.json', - undefined, - { - // @ts-expect-error -- node-fetch doesn't have this as a top-level but whatwg fetch does. - credentials: 'same-origin', - } - ) - - // Check if the response was successful (status code in the range 200-299) - if (!response.ok) { - return 'fail' - } - const contents = await response.text() - const containsAddedLater = contents.includes('added-later') - - return containsAddedLater ? 'success' : 'fail' - }, 'success') - - await check(async () => { - const contents = await renderViaHTTP( - appPort, + await retry(async () => { + const res = await next.fetch( '/_next/static/development/_devPagesManifest.json' ) - return contents.includes('added-later') ? 'success' : 'fail' - }, 'success') + expect(res.ok).toBe(true) + const contents = await res.text() + expect(contents).toContain('added-later') + }) await browser.elementByCss('#added-later-link').click() await browser.waitForElementByCss('#added-later') const text = await browser.elementByCss('#added-later').text() - await fs.remove(dirname(addLaterPage)) + await next.deleteFile('pages/added-later/[slug].js') expect(text).toBe('slug: first') }) - if (!process.env.__MIDDLEWARE_TEST) { + if (!middlewareEnabled) { it('should show error when interpolating fails for href', async () => { - const browser = await webdriver(appPort, '/') + const browser = await next.browser('/') await browser .elementByCss('#view-post-1-interpolated-incorrectly') .click() @@ -1222,35 +1051,36 @@ function runTests({ dev }) { } it('should work with HMR correctly', async () => { - const browser = await webdriver(appPort, '/post-1/comments') + const browser = await next.browser('/post-1/comments') let text = await browser.eval(`document.documentElement.innerHTML`) expect(text).toMatch(/comments for.*post-1/) - const page = join(appDir, 'pages/[name]/comments.js') - const origContent = await fs.readFile(page, 'utf8') + const origContent = await next.readFile('pages/[name]/comments.js') const newContent = origContent.replace(/comments/, 'commentss') try { - await fs.writeFile(page, newContent, 'utf8') - await waitFor(3 * 1000) + await next.patchFile('pages/[name]/comments.js', newContent) - let text = await browser.eval(`document.documentElement.innerHTML`) - expect(text).toMatch(/commentss for.*post-1/) + await retry(async () => { + const text = await browser.eval(`document.documentElement.innerHTML`) + expect(text).toMatch(/commentss for.*post-1/) + }) } finally { - await fs.writeFile(page, origContent, 'utf8') - if (browser) await browser.close() + await next.patchFile('pages/[name]/comments.js', origContent) } }) - } else { + } + + if (isNextStart) { it('should output a routes-manifest correctly', async () => { - const manifest = await fs.readJson( - join(appDir, '.next/routes-manifest.json') + const buildId = (await next.readFile('.next/BUILD_ID')).trim() + const manifest = JSON.parse( + await next.readFile('.next/routes-manifest.json') ) for (const route of manifest.dynamicRoutes) { route.regex = normalizeRegEx(route.regex) - // ensure regexes are valid new RegExp(route.regex) new RegExp(route.namedRegex) } @@ -1258,14 +1088,275 @@ function runTests({ dev }) { for (const route of manifest.dataRoutes) { route.dataRouteRegex = normalizeRegEx(route.dataRouteRegex) - // ensure regexes are valid new RegExp(route.dataRouteRegex) new RegExp(route.namedDataRouteRegex) } - // Parse the manifest string back into an object. - expect(normalizeManifest(manifest, [[buildId, 'BUILD_ID']])) - .toMatchInlineSnapshot(` + const normalizedManifest = normalizeManifest(manifest, [ + [buildId, 'BUILD_ID'], + ]) + + if (process.env.__NEXT_CACHE_COMPONENTS === 'true') { + expect(normalizedManifest).toMatchInlineSnapshot(` + { + "appType": "pages", + "basePath": "", + "caseSensitive": false, + "dataRoutes": [ + { + "dataRouteRegex": "^\\/_next\\/data\\/BUILD_ID\\/b\\/([^\\/]+?)\\.json$", + "namedDataRouteRegex": "^/_next/data/BUILD_ID/b/(?<nxtP123>[^/]+?)\\.json$", + "page": "/b/[123]", + "routeKeys": { + "nxtP123": "nxtP123", + }, + }, + { + "dataRouteRegex": "^\\/_next\\/data\\/BUILD_ID\\/c\\/([^\\/]+?)\\.json$", + "namedDataRouteRegex": "^/_next/data/BUILD_ID/c/(?<a>[^/]+?)\\.json$", + "page": "/c/[alongparamnameshouldbeallowedeventhoughweird]", + "routeKeys": { + "a": "nxtPalongparamnameshouldbeallowedeventhoughweird", + }, + }, + { + "dataRouteRegex": "^\\/_next\\/data\\/BUILD_ID\\/p1\\/p2\\/all\\-ssg\\/(.+?)\\.json$", + "namedDataRouteRegex": "^/_next/data/BUILD_ID/p1/p2/all\\-ssg/(?<nxtPrest>.+?)\\.json$", + "page": "/p1/p2/all-ssg/[...rest]", + "routeKeys": { + "nxtPrest": "nxtPrest", + }, + }, + { + "dataRouteRegex": "^\\/_next\\/data\\/BUILD_ID\\/p1\\/p2\\/nested\\-all\\-ssg\\/(.+?)\\.json$", + "namedDataRouteRegex": "^/_next/data/BUILD_ID/p1/p2/nested\\-all\\-ssg/(?<nxtPrest>.+?)\\.json$", + "page": "/p1/p2/nested-all-ssg/[...rest]", + "routeKeys": { + "nxtPrest": "nxtPrest", + }, + }, + { + "dataRouteRegex": "^\\/_next\\/data\\/BUILD_ID\\/p1\\/p2\\/predefined\\-ssg\\/(.+?)\\.json$", + "namedDataRouteRegex": "^/_next/data/BUILD_ID/p1/p2/predefined\\-ssg/(?<nxtPrest>.+?)\\.json$", + "page": "/p1/p2/predefined-ssg/[...rest]", + "routeKeys": { + "nxtPrest": "nxtPrest", + }, + }, + { + "dataRouteRegex": "^\\/_next\\/data\\/BUILD_ID\\/([^\\/]+?)\\/([^\\/]+?)\\/(.+?)\\.json$", + "namedDataRouteRegex": "^/_next/data/BUILD_ID/(?<nxtPname>[^/]+?)/(?<nxtPcomment>[^/]+?)/(?<nxtPrest>.+?)\\.json$", + "page": "/[name]/[comment]/[...rest]", + "routeKeys": { + "nxtPcomment": "nxtPcomment", + "nxtPname": "nxtPname", + "nxtPrest": "nxtPrest", + }, + }, + ], + "dynamicRoutes": [ + { + "namedRegex": "^/b/(?<nxtP123>[^/]+?)(?:/)?$", + "page": "/b/[123]", + "regex": "^\\/b\\/([^\\/]+?)(?:\\/)?$", + "routeKeys": { + "nxtP123": "nxtP123", + }, + }, + { + "namedRegex": "^/blog/(?<nxtPname>[^/]+?)/comment/(?<nxtPid>[^/]+?)(?:/)?$", + "page": "/blog/[name]/comment/[id]", + "regex": "^\\/blog\\/([^\\/]+?)\\/comment\\/([^\\/]+?)(?:\\/)?$", + "routeKeys": { + "nxtPid": "nxtPid", + "nxtPname": "nxtPname", + }, + }, + { + "namedRegex": "^/c/(?<a>[^/]+?)(?:/)?$", + "page": "/c/[alongparamnameshouldbeallowedeventhoughweird]", + "regex": "^\\/c\\/([^\\/]+?)(?:\\/)?$", + "routeKeys": { + "a": "nxtPalongparamnameshouldbeallowedeventhoughweird", + }, + }, + { + "namedRegex": "^/catchall\\-dash/(?<nxtPhelloworld>.+?)(?:/)?$", + "page": "/catchall-dash/[...hello-world]", + "regex": "^\\/catchall\\-dash\\/(.+?)(?:\\/)?$", + "routeKeys": { + "nxtPhelloworld": "nxtPhello-world", + }, + }, + { + "namedRegex": "^/d/(?<nxtPid>[^/]+?)(?:/)?$", + "page": "/d/[id]", + "regex": "^\\/d\\/([^\\/]+?)(?:\\/)?$", + "routeKeys": { + "nxtPid": "nxtPid", + }, + }, + { + "namedRegex": "^/dash/(?<nxtPhelloworld>[^/]+?)(?:/)?$", + "page": "/dash/[hello-world]", + "regex": "^\\/dash\\/([^\\/]+?)(?:\\/)?$", + "routeKeys": { + "nxtPhelloworld": "nxtPhello-world", + }, + }, + { + "namedRegex": "^/index/(?<nxtPslug>.+?)(?:/)?$", + "page": "/index/[...slug]", + "regex": "^\\/index\\/(.+?)(?:\\/)?$", + "routeKeys": { + "nxtPslug": "nxtPslug", + }, + }, + { + "namedRegex": "^/on\\-mount/(?<nxtPpost>[^/]+?)(?:/)?$", + "page": "/on-mount/[post]", + "regex": "^\\/on\\-mount\\/([^\\/]+?)(?:\\/)?$", + "routeKeys": { + "nxtPpost": "nxtPpost", + }, + }, + { + "namedRegex": "^/p1/p2/all\\-ssg/(?<nxtPrest>.+?)(?:/)?$", + "page": "/p1/p2/all-ssg/[...rest]", + "regex": "^\\/p1\\/p2\\/all\\-ssg\\/(.+?)(?:\\/)?$", + "routeKeys": { + "nxtPrest": "nxtPrest", + }, + }, + { + "namedRegex": "^/p1/p2/all\\-ssr/(?<nxtPrest>.+?)(?:/)?$", + "page": "/p1/p2/all-ssr/[...rest]", + "regex": "^\\/p1\\/p2\\/all\\-ssr\\/(.+?)(?:\\/)?$", + "routeKeys": { + "nxtPrest": "nxtPrest", + }, + }, + { + "namedRegex": "^/p1/p2/nested\\-all\\-ssg/(?<nxtPrest>.+?)(?:/)?$", + "page": "/p1/p2/nested-all-ssg/[...rest]", + "regex": "^\\/p1\\/p2\\/nested\\-all\\-ssg\\/(.+?)(?:\\/)?$", + "routeKeys": { + "nxtPrest": "nxtPrest", + }, + }, + { + "namedRegex": "^/p1/p2/predefined\\-ssg/(?<nxtPrest>.+?)(?:/)?$", + "page": "/p1/p2/predefined-ssg/[...rest]", + "regex": "^\\/p1\\/p2\\/predefined\\-ssg\\/(.+?)(?:\\/)?$", + "routeKeys": { + "nxtPrest": "nxtPrest", + }, + }, + { + "namedRegex": "^/(?<nxtPname>[^/]+?)(?:/)?$", + "page": "/[name]", + "regex": "^\\/([^\\/]+?)(?:\\/)?$", + "routeKeys": { + "nxtPname": "nxtPname", + }, + }, + { + "namedRegex": "^/(?<nxtPname>[^/]+?)/comments(?:/)?$", + "page": "/[name]/comments", + "regex": "^\\/([^\\/]+?)\\/comments(?:\\/)?$", + "routeKeys": { + "nxtPname": "nxtPname", + }, + }, + { + "namedRegex": "^/(?<nxtPname>[^/]+?)/on\\-mount\\-redir(?:/)?$", + "page": "/[name]/on-mount-redir", + "regex": "^\\/([^\\/]+?)\\/on\\-mount\\-redir(?:\\/)?$", + "routeKeys": { + "nxtPname": "nxtPname", + }, + }, + { + "namedRegex": "^/(?<nxtPname>[^/]+?)/(?<nxtPcomment>[^/]+?)(?:/)?$", + "page": "/[name]/[comment]", + "regex": "^\\/([^\\/]+?)\\/([^\\/]+?)(?:\\/)?$", + "routeKeys": { + "nxtPcomment": "nxtPcomment", + "nxtPname": "nxtPname", + }, + }, + { + "namedRegex": "^/(?<nxtPname>[^/]+?)/(?<nxtPcomment>[^/]+?)/(?<nxtPrest>.+?)(?:/)?$", + "page": "/[name]/[comment]/[...rest]", + "regex": "^\\/([^\\/]+?)\\/([^\\/]+?)\\/(.+?)(?:\\/)?$", + "routeKeys": { + "nxtPcomment": "nxtPcomment", + "nxtPname": "nxtPname", + "nxtPrest": "nxtPrest", + }, + }, + ], + "headers": [], + "onMatchHeaders": [], + "pages404": true, + "ppr": { + "chain": { + "headers": { + "next-resume": "1", + }, + }, + }, + "redirects": [ + { + "destination": "/:path+", + "internal": true, + "priority": true, + "regex": "^(?:/((?:[^/]+?)(?:/(?:[^/]+?))*))/$", + "source": "/:path+/", + "statusCode": 308, + }, + ], + "rewriteHeaders": { + "pathHeader": "x-nextjs-rewritten-path", + "queryHeader": "x-nextjs-rewritten-query", + }, + "rewrites": { + "afterFiles": [], + "beforeFiles": [], + "fallback": [], + }, + "rsc": { + "clientParamParsing": true, + "contentTypeHeader": "text/x-component", + "didPostponeHeader": "x-nextjs-postponed", + "dynamicRSCPrerender": true, + "header": "rsc", + "prefetchHeader": "next-router-prefetch", + "prefetchSegmentDirSuffix": ".segments", + "prefetchSegmentHeader": "next-router-segment-prefetch", + "prefetchSegmentSuffix": ".segment.rsc", + "suffix": ".rsc", + "varyHeader": "rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch", + }, + "staticRoutes": [ + { + "namedRegex": "^/(?:/)?$", + "page": "/", + "regex": "^/(?:/)?$", + "routeKeys": {}, + }, + { + "namedRegex": "^/another(?:/)?$", + "page": "/another", + "regex": "^/another(?:/)?$", + "routeKeys": {}, + }, + ], + "version": 3, + } + `) + } else { + expect(normalizedManifest).toMatchInlineSnapshot(` { "appType": "pages", "basePath": "", @@ -1516,14 +1607,15 @@ function runTests({ dev }) { "version": 3, } `) + } }) it('should output a pages-manifest correctly', async () => { - const manifest = await fs.readJson( - join(appDir, '.next/server/pages-manifest.json') + const manifest = JSON.parse( + await next.readFile('.next/server/pages-manifest.json') ) - if (process.env.IS_TURBOPACK_TEST) { + if (isTurbopack) { expect(manifest).toMatchInlineSnapshot(` { "/": "pages/index.html", @@ -1583,55 +1675,3 @@ function runTests({ dev }) { }) } } - -describe('Dynamic Routing', () => { - if (process.env.__MIDDLEWARE_TEST) { - const middlewarePath = join(__dirname, '../middleware.js') - - beforeAll(async () => { - await fs.writeFile( - middlewarePath, - ` - import { NextResponse } from 'next/server' - export default function middleware() { - return NextResponse.next() - } - ` - ) - }) - afterAll(() => fs.remove(middlewarePath)) - } - - ;(process.env.TURBOPACK_BUILD ? describe.skip : describe)( - 'development mode', - () => { - beforeAll(async () => { - appPort = await findPort() - app = await launchApp(appDir, appPort) - buildId = 'development' - }) - afterAll(() => killApp(app)) - - runTests({ dev: true }) - } - ) - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( - 'production mode', - () => { - beforeAll(async () => { - await nextBuild(appDir, undefined, { - disableAutoSkewProtection: true, - }) - buildId = await fs.readFile(buildIdPath, 'utf8') - - appPort = await findPort() - app = await nextStart(appDir, appPort, { - disableAutoSkewProtection: true, - }) - }) - afterAll(() => killApp(app)) - - runTests({ dev: false }) - } - ) -}) diff --git a/test/integration/dynamic-routing/static/hello copy.txt b/test/e2e/dynamic-routing/static/hello copy.txt similarity index 100% rename from test/integration/dynamic-routing/static/hello copy.txt rename to test/e2e/dynamic-routing/static/hello copy.txt diff --git a/test/integration/dynamic-routing/static/hello%20copy.txt b/test/e2e/dynamic-routing/static/hello%20copy.txt similarity index 100% rename from test/integration/dynamic-routing/static/hello%20copy.txt rename to test/e2e/dynamic-routing/static/hello%20copy.txt diff --git a/test/integration/dynamic-routing/static/hello+copy.txt b/test/e2e/dynamic-routing/static/hello+copy.txt similarity index 100% rename from test/integration/dynamic-routing/static/hello+copy.txt rename to test/e2e/dynamic-routing/static/hello+copy.txt diff --git a/test/integration/dynamic-routing/static/hello.txt b/test/e2e/dynamic-routing/static/hello.txt similarity index 100% rename from test/integration/dynamic-routing/static/hello.txt rename to test/e2e/dynamic-routing/static/hello.txt diff --git a/test/e2e/edge-configurable-runtime/index.test.ts b/test/e2e/edge-configurable-runtime/index.test.ts index cb83a3bceda2..2d907819aa20 100644 --- a/test/e2e/edge-configurable-runtime/index.test.ts +++ b/test/e2e/edge-configurable-runtime/index.test.ts @@ -1,6 +1,4 @@ -import { createNext, FileRef } from 'e2e-utils' -import { NextInstance } from 'e2e-utils' -import { fetchViaHTTP, File, nextBuild } from 'next-test-utils' +import { isNextDev, isNextStart, nextTestSetup } from 'e2e-utils' import { join } from 'path' import stripAnsi from 'strip-ansi' @@ -11,31 +9,30 @@ const apiPath = 'pages/api/edge.js' { appDir: join(__dirname, './app/src'), title: 'src/pages and API routes' }, { appDir: join(__dirname, './app'), title: 'pages and API routes' }, ])('Configurable runtime for $title', ({ appDir }) => { - let next: NextInstance - const page = new File(join(appDir, pagePath)) - const api = new File(join(appDir, apiPath)) - - if ((global as any).isNextDev) { + if (isNextDev) { describe('In development mode', () => { + const { next } = nextTestSetup({ + files: appDir, + skipStart: true, + }) + + let originalPage: string + let originalApi: string + beforeAll(async () => { - next = await createNext({ - files: new FileRef(appDir), - dependencies: {}, - skipStart: true, - }) + originalPage = await next.readFile(pagePath) + originalApi = await next.readFile(apiPath) }) afterEach(async () => { await next.stop() - await next.patchFile(pagePath, page.originalContent) - await next.patchFile(apiPath, api.originalContent) + await next.patchFile(pagePath, originalPage) + await next.patchFile(apiPath, originalApi) }) - afterAll(() => next.destroy()) - it('runs with no warning API route on the edge runtime', async () => { await next.start() - const res = await fetchViaHTTP(next.url, `/api/edge`) + const res = await next.fetch(`/api/edge`) expect(res.status).toEqual(200) expect(next.cliOutput).not.toInclude('error') expect(next.cliOutput).not.toInclude('warn') @@ -50,7 +47,7 @@ const apiPath = 'pages/api/edge.js' ` ) await next.start() - const res = await fetchViaHTTP(next.url, `/api/edge`) + const res = await next.fetch(`/api/edge`) expect(res.status).toEqual(200) expect(next.cliOutput).not.toInclude('error') expect(stripAnsi(next.cliOutput)).toInclude( @@ -66,7 +63,7 @@ const apiPath = 'pages/api/edge.js' ` ) await next.start() - const res = await fetchViaHTTP(next.url, `/`) + const res = await next.fetch(`/`) expect(res.status).toEqual(200) expect(next.cliOutput).not.toInclude('error') expect(stripAnsi(next.cliOutput)).toInclude( @@ -83,7 +80,7 @@ const apiPath = 'pages/api/edge.js' ` ) await next.start() - const res = await fetchViaHTTP(next.url, `/`) + const res = await next.fetch(`/`) expect(res.status).toEqual(200) expect(stripAnsi(next.cliOutput)).toInclude( `Page / provided runtime 'edge', the edge runtime for rendering is currently experimental. Use runtime 'experimental-edge' instead.` @@ -91,40 +88,52 @@ const apiPath = 'pages/api/edge.js' expect(next.cliOutput).not.toInclude('warn') }) }) - } else if ((global as any).isNextStart) { + } else if (isNextStart) { describe('In start mode', () => { - // TODO because createNext runs process.exit() without any log info on build failure, rely on good old nextBuild() + const { next } = nextTestSetup({ + files: appDir, + skipStart: true, + }) + + let originalPage: string + let originalApi: string + + beforeAll(async () => { + originalPage = await next.readFile(pagePath) + originalApi = await next.readFile(apiPath) + }) + afterEach(async () => { - page.restore() - api.restore() + await next.patchFile(pagePath, originalPage) + await next.patchFile(apiPath, originalApi) }) it('builds with API route on the edge runtime and page on the experimental edge runtime', async () => { - page.write(` + await next.patchFile( + pagePath, + ` export default () => (<p>hello world</p>); export const runtime = 'experimental-edge'; - `) - const output = await nextBuild(appDir, undefined, { - stdout: true, - stderr: true, - }) - expect(output.code).toBe(0) - expect(output.stderr).not.toContain(`error`) - expect(output.stdout).not.toContain(`warn`) + ` + ) + const { exitCode, cliOutput } = await next.build() + expect(exitCode).toBe(0) + expect(cliOutput).not.toContain(`error`) + expect(cliOutput).not.toContain(`warn`) }) it('does not build with page on the edge runtime', async () => { - page.write(` + await next.patchFile( + pagePath, + ` export default () => (<p>hello world</p>); export const runtime = 'edge'; - `) - const output = await nextBuild(appDir, undefined, { - stdout: true, - stderr: true, - }) - expect(output.code).toBe(1) - expect(output.stderr).not.toContain(`Build failed`) - expect(stripAnsi(output.stderr)).toContain( + ` + ) + const { exitCode, cliOutput } = await next.build() + expect(exitCode).toBe(1) + expect(cliOutput).not.toContain(`Build failed`) + expect(stripAnsi(cliOutput)).toContain( `Error: Page / provided runtime 'edge', the edge runtime for rendering is currently experimental. Use runtime 'experimental-edge' instead.` ) }) diff --git a/test/integration/edge-runtime-configurable-guards/node_modules/.pnpm/test/node_modules/lib/index.js b/test/e2e/edge-runtime-configurable-guards/.pnpm/test/node_modules/lib/index.js similarity index 100% rename from test/integration/edge-runtime-configurable-guards/node_modules/.pnpm/test/node_modules/lib/index.js rename to test/e2e/edge-runtime-configurable-guards/.pnpm/test/node_modules/lib/index.js diff --git a/test/integration/edge-runtime-configurable-guards/node_modules/.pnpm/test/node_modules/lib/package.json b/test/e2e/edge-runtime-configurable-guards/.pnpm/test/node_modules/lib/package.json similarity index 100% rename from test/integration/edge-runtime-configurable-guards/node_modules/.pnpm/test/node_modules/lib/package.json rename to test/e2e/edge-runtime-configurable-guards/.pnpm/test/node_modules/lib/package.json diff --git a/test/e2e/edge-runtime-configurable-guards/edge-runtime-configurable-guards.test.ts b/test/e2e/edge-runtime-configurable-guards/edge-runtime-configurable-guards.test.ts new file mode 100644 index 000000000000..40c1abccb13c --- /dev/null +++ b/test/e2e/edge-runtime-configurable-guards/edge-runtime-configurable-guards.test.ts @@ -0,0 +1,710 @@ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +import { shouldUseTurbopack } from 'next-test-utils' +import { retry } from 'next-test-utils' + +const TELEMETRY_EVENT_NAME = 'NEXT_EDGE_ALLOW_DYNAMIC_USED' +const LIB_PATH = 'node_modules/lib/index.js' + +// Production-mode tests run a full `next build` followed by `next start`, +// which on webpack regularly takes 30-60s per test case (see the original +// `test/e2e/edge-runtime-configurable-guards` which also used +// `jest.setTimeout(1000 * 60 * 2)`). The default 60s-per-test timeout is +// too tight for webpack here; bump it so slower runs do not cascade into +// "server is running" errors on subsequent tests. +jest.setTimeout(120 * 1000) + +describe('Edge runtime configurable guards', () => { + ;(isNextDev ? describe : describe.skip)('development mode', () => { + const { next, isTurbopack, skipped } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + }) + if (skipped) return + + let originalApiRoute: string + let originalMiddleware: string + let originalLib: string + + beforeAll(async () => { + originalApiRoute = await next.readFile('pages/api/route.js') + originalMiddleware = await next.readFile('middleware.js') + // Handle lib file which might not exist or be empty + try { + originalLib = await next.readFile(LIB_PATH) + } catch (e) { + // File doesn't exist, use default content + originalLib = '// populated by tests\n' + } + }) + + afterEach(async () => { + await next.patchFile('pages/api/route.js', originalApiRoute) + await next.patchFile('middleware.js', originalMiddleware) + await next.patchFile(LIB_PATH, originalLib) + }) + + // Webpack treats `node_modules` as a "managed path" in its snapshot + // config (see packages/next/src/build/webpack-config.ts), meaning it + // assumes the contents of any file under `node_modules` are immutable + // per package version. When a test patches + // `node_modules/lib/index.js`, webpack's dev server keeps serving the + // originally-cached (empty) lib module, so imports like + // `import { hasDynamic } from 'lib'` resolve to an object without + // `hasDynamic`. Restarting the dev server forces a fresh read of + // `node_modules/lib/index.js`. Turbopack watches node_modules + // correctly and does not need this workaround. + async function restartForWebpackIfLibPatched(libPatched: boolean) { + if (!libPatched || isTurbopack) return + await next.stop() + await next.start() + } + + describe('Multiple functions with different configurations', () => { + async function patchMultipleFunctions() { + await next.patchFile( + 'middleware.js', + ` + import { NextResponse } from 'next/server' + + export default () => { + eval('100') + return NextResponse.next() + } + export const config = { + unstable_allowDynamic: '/middleware.js' + } + ` + ) + await next.patchFile( + 'pages/api/route.js', + ` + export default async function handler(request) { + eval('100') + return Response.json({ result: true }) + } + export const config = { + runtime: 'edge', + unstable_allowDynamic: '**/node_modules/lib/**' + } + ` + ) + } + + it('warns in dev for allowed code', async () => { + await patchMultipleFunctions() + const outputIndex = next.cliOutput.length + await retry(async () => { + const res = await next.fetch('/') + expect(res.status).toBe(200) + expect(next.cliOutput.slice(outputIndex)).toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + ) + }) + }) + + it('warns in dev for unallowed code', async () => { + await patchMultipleFunctions() + const outputIndex = next.cliOutput.length + await retry(async () => { + const res = await next.fetch('/api/route') + expect(res.status).toBe(200) + expect(next.cliOutput.slice(outputIndex)).toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + ) + }) + }) + }) + + describe.each([ + { + title: 'Edge API', + url: '/api/route', + apiContent: ` + export default async function handler(request) { + eval('100') + return Response.json({ result: true }) + } + export const config = { + runtime: 'edge', + unstable_allowDynamic: '**' + } + `, + middlewareContent: null as string | null, + libContent: null as string | null, + skip: false, + }, + { + title: 'Middleware', + url: '/', + apiContent: null as string | null, + middlewareContent: ` + import { NextResponse } from 'next/server' + + export default () => { + eval('100') + return NextResponse.next() + } + export const config = { + unstable_allowDynamic: '**' + } + `, + libContent: null as string | null, + skip: false, + }, + { + title: 'Edge API using lib', + url: '/api/route', + apiContent: ` + import { hasDynamic } from 'lib' + export default async function handler(request) { + await hasDynamic() + return Response.json({ result: true }) + } + export const config = { + runtime: 'edge', + unstable_allowDynamic: '**/node_modules/lib/**' + } + `, + middlewareContent: null as string | null, + libContent: ` + export async function hasDynamic() { + eval('100') + } + `, + skip: false, + }, + { + title: 'Middleware using lib', + url: '/', + apiContent: null as string | null, + middlewareContent: ` + import { NextResponse } from 'next/server' + import { hasDynamic } from 'lib' + + // populated with tests + export default async function () { + await hasDynamic() + return NextResponse.next() + } + export const config = { + unstable_allowDynamic: '**/node_modules/lib/**' + } + `, + libContent: ` + export async function hasDynamic() { + eval('100') + } + `, + // TODO: Re-enable when Turbopack applies the middleware dynamic code + // evaluation transforms also to code in node_modules. + skip: isTurbopack, + }, + ])( + '$title with allowed, used dynamic code', + ({ url, apiContent, middlewareContent, libContent, skip }) => { + ;(skip ? it.skip : it)('still warns in dev at runtime', async () => { + if (apiContent) await next.patchFile('pages/api/route.js', apiContent) + if (middlewareContent) + await next.patchFile('middleware.js', middlewareContent) + if (libContent) await next.patchFile(LIB_PATH, libContent) + await restartForWebpackIfLibPatched(libContent !== null) + + const outputIndex = next.cliOutput.length + await retry(async () => { + const res = await next.fetch(url) + + expect(res.status).toBe(200) + + expect(next.cliOutput.slice(outputIndex)).toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + ) + }) + }) + } + ) + + describe.each([ + { + title: 'Edge API using lib', + url: '/api/route', + apiContent: ` + import { hasDynamic } from 'lib' + export default async function handler(request) { + await hasDynamic() + return Response.json({ result: true }) + } + export const config = { + runtime: 'edge', + unstable_allowDynamic: '/pages/**' + } + `, + middlewareContent: null as string | null, + libContent: ` + export async function hasDynamic() { + eval('100') + } + `, + // TODO: Re-enable when Turbopack applies the edge runtime transforms also + // to code in node_modules. + skip: isTurbopack, + }, + { + title: 'Middleware using lib', + url: '/', + apiContent: null as string | null, + middlewareContent: ` + import { NextResponse } from 'next/server' + import { hasDynamic } from 'lib' + export default async function () { + await hasDynamic() + return NextResponse.next() + } + export const config = { + unstable_allowDynamic: '/pages/**' + } + `, + libContent: ` + export async function hasDynamic() { + eval('100') + } + `, + // TODO: Re-enable when Turbopack applies the middleware dynamic code + // evaluation transforms also to code in node_modules. + skip: isTurbopack, + }, + ])( + '$title with unallowed, used dynamic code', + ({ url, apiContent, middlewareContent, libContent, skip }) => { + ;(skip ? it.skip : it)('warns in dev at runtime', async () => { + if (apiContent) await next.patchFile('pages/api/route.js', apiContent) + if (middlewareContent) + await next.patchFile('middleware.js', middlewareContent) + if (libContent) await next.patchFile(LIB_PATH, libContent) + await restartForWebpackIfLibPatched(libContent !== null) + + const outputIndex = next.cliOutput.length + await retry(async () => { + const res = await next.fetch(url) + + expect(res.status).toBe(200) + + expect(next.cliOutput.slice(outputIndex)).toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + ) + }) + }) + } + ) + + describe.each([ + { + title: 'Edge API', + url: '/api/route', + apiContent: ` + export default async function handler(request) { + return Response.json({ result: (() => {}) instanceof Function }) + } + export const config = { runtime: 'edge' } + `, + middlewareContent: null as string | null, + }, + { + title: 'Middleware', + url: '/', + apiContent: null as string | null, + middlewareContent: ` + import { NextResponse } from 'next/server' + import { returnTrue } from 'lib' + export default async function () { + (() => {}) instanceof Function + return NextResponse.next() + } + `, + }, + ])( + '$title with use of Function as a type', + ({ url, apiContent, middlewareContent }) => { + it('does not warn in dev at runtime', async () => { + if (apiContent) await next.patchFile('pages/api/route.js', apiContent) + if (middlewareContent) + await next.patchFile('middleware.js', middlewareContent) + + const outputIndex = next.cliOutput.length + await retry(async () => { + const res = await next.fetch(url) + expect(res.status).toBe(200) + }) + expect(next.cliOutput.slice(outputIndex)).not.toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + ) + }) + } + ) + }) + ;(isNextStart ? describe : describe.skip)('production mode', () => { + const { next, isTurbopack, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + env: shouldUseTurbopack() ? {} : { NEXT_TELEMETRY_DEBUG: '1' }, + skipDeployment: true, + }) + if (skipped) return + + let originalApiRoute: string + let originalMiddleware: string + let originalLib: string + + beforeAll(async () => { + originalApiRoute = await next.readFile('pages/api/route.js') + originalMiddleware = await next.readFile('middleware.js') + // Handle lib file which might not exist or be empty + try { + originalLib = await next.readFile(LIB_PATH) + } catch (e) { + // File doesn't exist, use default content + originalLib = '// populated by tests\n' + } + }) + + afterEach(async () => { + // Production-mode tests that reach `next.start()` normally call + // `next.stop()` themselves, but if a test times out or throws before + // that we would otherwise leave the server running and every later + // `next.build()` call would fail with + // "can not run export while server is running". Stopping here is a + // no-op when the server is already stopped. + try { + await next.stop() + } catch {} + await next.patchFile('pages/api/route.js', originalApiRoute) + await next.patchFile('middleware.js', originalMiddleware) + await next.patchFile(LIB_PATH, originalLib) + // Webpack treats `node_modules` as a "managed path" in its snapshot + // config, so changes to `node_modules/lib/index.js` between test + // cases are not invalidated from webpack's persistent build cache + // at `.next/cache/webpack`. That causes later builds to re-use a + // stale compiled `lib` module from a previous test (e.g. still + // containing `hasUnusedDynamic`), which can trip the + // `unstable_allowDynamic` analyzer. Drop the entire `.next` dir + // between cases so each build reads `lib` fresh. Turbopack does + // not share this cache and does not need the workaround. + if (!isTurbopack) { + await next.deleteFile('.next') + } + }) + + // eslint-disable-next-line jest/no-identical-title + describe('Multiple functions with different configurations', () => { + it('fails to build because of unallowed code', async () => { + await next.patchFile( + 'middleware.js', + ` + import { NextResponse } from 'next/server' + + export default () => { + eval('100') + return NextResponse.next() + } + export const config = { + unstable_allowDynamic: '/middleware.js' + } + ` + ) + await next.patchFile( + 'pages/api/route.js', + ` + export default async function handler(request) { + eval('100') + return Response.json({ result: true }) + } + export const config = { + runtime: 'edge', + unstable_allowDynamic: '**/node_modules/lib/**' + } + ` + ) + + const outputIndex = next.cliOutput.length + const { exitCode } = await next.build() + const buildOutput = next.cliOutput.slice(outputIndex) + + expect(exitCode).toBe(1) + if (!isTurbopack) { + expect(buildOutput).toContain(`./pages/api/route.js`) + } + expect(buildOutput).toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime` + ) + if (!isTurbopack) { + expect(buildOutput).toContain(`Used by default`) + expect(buildOutput).toContain(TELEMETRY_EVENT_NAME) + } + }) + }) + + describe.each([ + { + title: 'Edge API', + url: '/api/route', + apiContent: ` + export default async function handler(request) { + if ((() => false)()) { + eval('100') + } + return Response.json({ result: true }) + } + export const config = { + runtime: 'edge', + unstable_allowDynamic: '**' + } + `, + middlewareContent: null as string | null, + libContent: null as string | null, + }, + { + title: 'Middleware', + url: '/', + apiContent: null as string | null, + middlewareContent: ` + import { NextResponse } from 'next/server' + // populated with tests + export default () => { + if ((() => false)()) { + eval('100') + } + return NextResponse.next() + } + export const config = { + unstable_allowDynamic: '**' + } + `, + libContent: null as string | null, + }, + { + title: 'Edge API using lib', + url: '/api/route', + apiContent: ` + import { hasUnusedDynamic } from 'lib' + export default async function handler(request) { + await hasUnusedDynamic() + return Response.json({ result: true }) + } + export const config = { + runtime: 'edge', + unstable_allowDynamic: '**/node_modules/lib/**' + } + `, + middlewareContent: null as string | null, + libContent: ` + export async function hasUnusedDynamic() { + if ((() => false)()) { + eval('100') + } + } + `, + }, + { + title: 'Middleware using lib', + url: '/', + apiContent: null as string | null, + middlewareContent: ` + import { NextResponse } from 'next/server' + import { hasUnusedDynamic } from 'lib' + // populated with tests + export default async function () { + await hasUnusedDynamic() + return NextResponse.next() + } + export const config = { + unstable_allowDynamic: '**/node_modules/lib/**' + } + `, + libContent: ` + export async function hasUnusedDynamic() { + if ((() => false)()) { + eval('100') + } + } + `, + }, + ])( + '$title with allowed, unused dynamic code', + ({ url, apiContent, middlewareContent, libContent }) => { + // unstable_allowDynamic configuration is not supported in Turbopack. + ;(isTurbopack ? it.skip : it)( + 'build and does not warn at runtime', + async () => { + if (apiContent) + await next.patchFile('pages/api/route.js', apiContent) + if (middlewareContent) + await next.patchFile('middleware.js', middlewareContent) + if (libContent) await next.patchFile(LIB_PATH, libContent) + + const outputIndex = next.cliOutput.length + await next.build() + const buildOutput = next.cliOutput.slice(outputIndex) + + // eslint-disable-next-line jest/no-standalone-expect + expect(buildOutput).not.toContain(`Build failed`) + if (!isTurbopack) { + // eslint-disable-next-line jest/no-standalone-expect + expect(buildOutput).toContain(TELEMETRY_EVENT_NAME) + } + + await next.start() + const startIndex = next.cliOutput.length + + const res = await next.fetch(url) + // eslint-disable-next-line jest/no-standalone-expect + expect(res.status).toBe(200) + // eslint-disable-next-line jest/no-standalone-expect + expect(next.cliOutput.slice(startIndex)).not.toContain(`warn`) + // eslint-disable-next-line jest/no-standalone-expect + expect(next.cliOutput.slice(startIndex)).not.toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + ) + await next.stop() + } + ) + } + ) + + describe.each([ + { + title: 'Edge API using lib', + apiContent: ` + import { hasDynamic } from 'lib' + export default async function handler(request) { + await hasDynamic() + return Response.json({ result: true }) + } + export const config = { + runtime: 'edge', + unstable_allowDynamic: '/pages/**' + } + `, + middlewareContent: null as string | null, + libContent: ` + export async function hasDynamic() { + eval('100') + } + `, + // TODO: Re-enable when Turbopack applies the edge runtime transforms also + // to code in node_modules. + skip: isTurbopack, + }, + { + title: 'Middleware using lib', + apiContent: null as string | null, + middlewareContent: ` + import { NextResponse } from 'next/server' + import { hasDynamic } from 'lib' + export default async function () { + await hasDynamic() + return NextResponse.next() + } + export const config = { + unstable_allowDynamic: '/pages/**' + } + `, + libContent: ` + export async function hasDynamic() { + eval('100') + } + `, + // TODO: Re-enable when Turbopack applies the middleware dynamic code + // evaluation transforms also to code in node_modules. + skip: isTurbopack, + }, + ])( + '$title with unallowed, used dynamic code', + ({ apiContent, middlewareContent, libContent, skip }) => { + ;(skip ? it.skip : it)( + 'fails to build because of dynamic code evaluation', + async () => { + if (apiContent) + await next.patchFile('pages/api/route.js', apiContent) + if (middlewareContent) + await next.patchFile('middleware.js', middlewareContent) + if (libContent) await next.patchFile(LIB_PATH, libContent) + + const outputIndex = next.cliOutput.length + await next.build() + const buildOutput = next.cliOutput.slice(outputIndex) + + // eslint-disable-next-line jest/no-standalone-expect + expect(buildOutput).toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime` + ) + if (!isTurbopack) { + // eslint-disable-next-line jest/no-standalone-expect + expect(buildOutput).toContain(TELEMETRY_EVENT_NAME) + } + } + ) + } + ) + + describe.each([ + { + title: 'Edge API', + url: '/api/route', + apiContent: ` + export default async function handler(request) { + return Response.json({ result: (() => {}) instanceof Function }) + } + export const config = { runtime: 'edge' } + `, + middlewareContent: null as string | null, + }, + { + title: 'Middleware', + url: '/', + apiContent: null as string | null, + middlewareContent: ` + import { NextResponse } from 'next/server' + import { returnTrue } from 'lib' + export default async function () { + (() => {}) instanceof Function + return NextResponse.next() + } + `, + }, + ])( + '$title with use of Function as a type', + ({ url, apiContent, middlewareContent }) => { + // unstable_allowDynamic configuration is not supported in Turbopack. + ;(isTurbopack ? it.skip : it)( + 'build and does not warn at runtime', + async () => { + if (apiContent) + await next.patchFile('pages/api/route.js', apiContent) + if (middlewareContent) + await next.patchFile('middleware.js', middlewareContent) + + const outputIndex = next.cliOutput.length + await next.build() + const buildOutput = next.cliOutput.slice(outputIndex) + + // eslint-disable-next-line jest/no-standalone-expect + expect(buildOutput).not.toContain(`Build failed`) + + await next.start() + const startIndex = next.cliOutput.length + + const res = await next.fetch(url) + // eslint-disable-next-line jest/no-standalone-expect + expect(res.status).toBe(200) + // eslint-disable-next-line jest/no-standalone-expect + expect(next.cliOutput.slice(startIndex)).not.toContain(`warn`) + // eslint-disable-next-line jest/no-standalone-expect + expect(next.cliOutput.slice(startIndex)).not.toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + ) + await next.stop() + } + ) + } + ) + }) +}) diff --git a/test/integration/edge-runtime-configurable-guards/middleware.js b/test/e2e/edge-runtime-configurable-guards/middleware.js similarity index 100% rename from test/integration/edge-runtime-configurable-guards/middleware.js rename to test/e2e/edge-runtime-configurable-guards/middleware.js diff --git a/test/e2e/edge-runtime-configurable-guards/node_modules/lib/index.js b/test/e2e/edge-runtime-configurable-guards/node_modules/lib/index.js new file mode 100644 index 000000000000..8fa47bde2f62 --- /dev/null +++ b/test/e2e/edge-runtime-configurable-guards/node_modules/lib/index.js @@ -0,0 +1 @@ +// populated by tests diff --git a/test/e2e/edge-runtime-configurable-guards/node_modules/lib/package.json b/test/e2e/edge-runtime-configurable-guards/node_modules/lib/package.json new file mode 100644 index 000000000000..31bc38ec509a --- /dev/null +++ b/test/e2e/edge-runtime-configurable-guards/node_modules/lib/package.json @@ -0,0 +1,8 @@ +{ + "name": "lib", + "private": true, + "type": "module", + "exports": { + ".": "./index.js" + } +} diff --git a/test/integration/edge-runtime-configurable-guards/pages/api/route.js b/test/e2e/edge-runtime-configurable-guards/pages/api/route.js similarity index 100% rename from test/integration/edge-runtime-configurable-guards/pages/api/route.js rename to test/e2e/edge-runtime-configurable-guards/pages/api/route.js diff --git a/test/integration/edge-runtime-configurable-guards/pages/index.js b/test/e2e/edge-runtime-configurable-guards/pages/index.js similarity index 100% rename from test/integration/edge-runtime-configurable-guards/pages/index.js rename to test/e2e/edge-runtime-configurable-guards/pages/index.js diff --git a/test/e2e/edge-runtime-dynamic-code/edge-runtime-dynamic-code.test.ts b/test/e2e/edge-runtime-dynamic-code/edge-runtime-dynamic-code.test.ts new file mode 100644 index 000000000000..25c0f2feb1d5 --- /dev/null +++ b/test/e2e/edge-runtime-dynamic-code/edge-runtime-dynamic-code.test.ts @@ -0,0 +1,159 @@ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +import { retry } from 'next-test-utils' +import stripAnsi from 'next/dist/compiled/strip-ansi' + +const EVAL_ERROR = `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` +const DYNAMIC_CODE_ERROR = `Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime` +const WASM_COMPILE_ERROR = `Dynamic WASM code generation (e. g. 'WebAssembly.compile') not allowed in Edge Runtime` +const WASM_INSTANTIATE_ERROR = `Dynamic WASM code generation ('WebAssembly.instantiate' with a buffer parameter) not allowed in Edge Runtime` + +jest.setTimeout(1000 * 60 * 2) + +type NextFetchResponse = Awaited< + ReturnType<ReturnType<typeof nextTestSetup>['next']['fetch']> +> + +describe('Page using eval in development mode', () => { + if (!isNextDev) { + it('only runs in dev mode', () => {}) + return + } + + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('does not issue dynamic code evaluation warnings', async () => { + const outputIndex = next.cliOutput.length + const html = await next.render('/') + expect(html).toMatch(/>.*?100.*?and.*?100.*?<\//) + + await retry(async () => { + const output = next.cliOutput.slice(outputIndex) + expect(output).not.toContain(EVAL_ERROR) + expect(output).not.toContain(DYNAMIC_CODE_ERROR) + expect(output).not.toContain(WASM_COMPILE_ERROR) + expect(output).not.toContain(WASM_INSTANTIATE_ERROR) + }) + }) +}) + +describe.each([ + { + title: 'Middleware', + computeRoute(useCase: string) { + return `/${useCase}` + }, + async extractValue(response: NextFetchResponse) { + return JSON.parse(response.headers.get('data')!).value + }, + }, + { + title: 'Edge route', + computeRoute(useCase: string) { + return `/api/route?case=${useCase}` + }, + async extractValue(response: NextFetchResponse) { + return (await response.json()).value + }, + }, +])( + '$title usage of dynamic code evaluation', + ({ extractValue, computeRoute, title }) => { + if (isNextDev) { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('shows a warning when running code with eval', async () => { + const outputIndex = next.cliOutput.length + const res = await next.fetch(computeRoute('using-eval')) + expect(await extractValue(res)).toEqual(100) + + await retry(async () => { + const output = next.cliOutput.slice(outputIndex) + expect(output).toContain(EVAL_ERROR) + }) + + const output = next.cliOutput.slice(outputIndex) + expect(output).toContain("eval('100')") + }) + + it('does not show warning when no code uses eval', async () => { + const outputIndex = next.cliOutput.length + const res = await next.fetch(computeRoute('not-using-eval')) + expect(await extractValue(res)).toEqual(100) + + await retry(async () => { + const output = next.cliOutput.slice(outputIndex) + expect(output).not.toContain('Dynamic Code Evaluation') + }) + }) + + it('shows a warning when running WebAssembly.compile', async () => { + const outputIndex = next.cliOutput.length + const res = await next.fetch(computeRoute('using-webassembly-compile')) + expect(await extractValue(res)).toEqual(81) + + await retry(async () => { + const output = next.cliOutput.slice(outputIndex) + expect(output).toContain(WASM_COMPILE_ERROR) + }) + + const output = next.cliOutput.slice(outputIndex) + expect(output).toContain('WebAssembly.compile') + }) + + it('shows a warning when running WebAssembly.instantiate with a buffer parameter', async () => { + const outputIndex = next.cliOutput.length + const res = await next.fetch( + computeRoute('using-webassembly-instantiate-with-buffer') + ) + expect(await extractValue(res)).toEqual(81) + + await retry(async () => { + const output = next.cliOutput.slice(outputIndex) + expect(output).toContain(WASM_INSTANTIATE_ERROR) + }) + + const output = stripAnsi(next.cliOutput.slice(outputIndex)) + expect(output).toContain('WebAssembly.instantiate(SQUARE_WASM_BUFFER') + }) + + it('does not show a warning when running WebAssembly.instantiate with a module parameter', async () => { + const outputIndex = next.cliOutput.length + const res = await next.fetch( + computeRoute('using-webassembly-instantiate') + ) + expect(await extractValue(res)).toEqual(81) + + await retry(async () => { + const output = next.cliOutput.slice(outputIndex) + expect(output).not.toContain(WASM_INSTANTIATE_ERROR) + expect(output).not.toContain('DynamicWasmCodeGenerationWarning') + }) + }) + } + + if (isNextStart) { + const { next, isTurbopack } = nextTestSetup({ + files: __dirname, + skipStart: true, + }) + + it('should have middleware warning during build', async () => { + const { cliOutput } = await next.build() + + if (isTurbopack) { + expect(cliOutput).toContain(`Ecmascript file had an error`) + } else { + expect(cliOutput).toContain(`Failed to compile`) + expect(cliOutput).toContain(`Used by usingEval, usingEvalSync`) + expect(cliOutput).toContain(`Used by usingWebAssemblyCompile`) + } + + expect(cliOutput).toContain(DYNAMIC_CODE_ERROR) + }) + } + } +) diff --git a/test/integration/edge-runtime-dynamic-code/lib/square.wasm b/test/e2e/edge-runtime-dynamic-code/lib/square.wasm similarity index 100% rename from test/integration/edge-runtime-dynamic-code/lib/square.wasm rename to test/e2e/edge-runtime-dynamic-code/lib/square.wasm diff --git a/test/integration/edge-runtime-dynamic-code/lib/utils.js b/test/e2e/edge-runtime-dynamic-code/lib/utils.js similarity index 100% rename from test/integration/edge-runtime-dynamic-code/lib/utils.js rename to test/e2e/edge-runtime-dynamic-code/lib/utils.js diff --git a/test/integration/edge-runtime-dynamic-code/lib/wasm.js b/test/e2e/edge-runtime-dynamic-code/lib/wasm.js similarity index 100% rename from test/integration/edge-runtime-dynamic-code/lib/wasm.js rename to test/e2e/edge-runtime-dynamic-code/lib/wasm.js diff --git a/test/integration/edge-runtime-dynamic-code/middleware.js b/test/e2e/edge-runtime-dynamic-code/middleware.js similarity index 100% rename from test/integration/edge-runtime-dynamic-code/middleware.js rename to test/e2e/edge-runtime-dynamic-code/middleware.js diff --git a/test/integration/edge-runtime-dynamic-code/next.config.js b/test/e2e/edge-runtime-dynamic-code/next.config.js similarity index 100% rename from test/integration/edge-runtime-dynamic-code/next.config.js rename to test/e2e/edge-runtime-dynamic-code/next.config.js diff --git a/test/integration/edge-runtime-dynamic-code/pages/api/route.js b/test/e2e/edge-runtime-dynamic-code/pages/api/route.js similarity index 100% rename from test/integration/edge-runtime-dynamic-code/pages/api/route.js rename to test/e2e/edge-runtime-dynamic-code/pages/api/route.js diff --git a/test/integration/edge-runtime-dynamic-code/pages/index.js b/test/e2e/edge-runtime-dynamic-code/pages/index.js similarity index 100% rename from test/integration/edge-runtime-dynamic-code/pages/index.js rename to test/e2e/edge-runtime-dynamic-code/pages/index.js diff --git a/test/e2e/edge-runtime-module-errors/edge-runtime-module-errors.test.ts b/test/e2e/edge-runtime-module-errors/edge-runtime-module-errors.test.ts new file mode 100644 index 000000000000..d3bda9f5c64b --- /dev/null +++ b/test/e2e/edge-runtime-module-errors/edge-runtime-module-errors.test.ts @@ -0,0 +1,984 @@ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +import { retry } from 'next-test-utils' +import stripAnsi from 'strip-ansi' + +// Production-mode tests here run a full `next build` followed by +// `next start` per case, which on webpack regularly takes 30-60s (see the +// original `test/e2e/edge-runtime-module-errors` which also used +// `jest.setTimeout(1000 * 60 * 2)`). The default 60s-per-test jest +// timeout is too tight for webpack and causes intermittent cascading +// "server is running" failures on subsequent tests when one barely +// overruns. +jest.setTimeout(120 * 1000) + +function getModuleNotFound(name: string) { + return `Module not found: Can't resolve '${name}'` +} + +function getUnsupportedModule(name: string) { + return `The edge runtime does not support Node.js '${name}' module` +} + +function getUnsupportedModuleWarning(name: string) { + return `A Node.js module is loaded ('${name}'` +} + +function escapeLF(s: string) { + return s.replace(/\n/g, '\\n') +} + +function expectUnsupportedModuleProdError(moduleName: string, output: string) { + const moduleNotSupportedMessage = getUnsupportedModule(moduleName) + expect(output).toContain(moduleNotSupportedMessage) + const moduleNotFoundMessage = getModuleNotFound(moduleName) + expect(output).not.toContain(moduleNotFoundMessage) +} + +function expectUnsupportedModuleDevError( + moduleName: string, + _importStatement: string, + responseText: string, + output: string +) { + expectUnsupportedModuleProdError(moduleName, output) + // Codeframe should now include the import statement in e2e (isolated app). + // TODO: Uncomment once codeframe is verified to point to user code + // expect(stripAnsi(output)).toContain(importStatement) + + const moduleNotSupportedMessage = getUnsupportedModule(moduleName) + expect(responseText).toContain(escapeLF(moduleNotSupportedMessage)) + + const moduleNotFoundMessage = getModuleNotFound(moduleName) + expect(responseText).not.toContain(escapeLF(moduleNotFoundMessage)) +} + +function expectModuleNotFoundProdError(moduleName: string, output: string) { + const moduleNotSupportedMessage = getUnsupportedModule(moduleName) + expect(stripAnsi(output)).not.toContain(moduleNotSupportedMessage) + const moduleNotFoundMessages = [ + expect.stringContaining(`Error: Cannot find module '${moduleName}'`), + expect.stringContaining(getModuleNotFound(moduleName)), + ] + expect(moduleNotFoundMessages).toContainEqual(stripAnsi(output)) +} + +function expectModuleNotFoundDevError( + moduleName: string, + importStatement: string, + responseText: string, + output: string +) { + expectModuleNotFoundProdError(moduleName, output) + expect(stripAnsi(output)).toContain(importStatement) + + const moduleNotSupportedMessage = getUnsupportedModule(moduleName) + expect(responseText).not.toContain(escapeLF(moduleNotSupportedMessage)) + + const moduleNotFoundMessage = getModuleNotFound(moduleName) + expect(responseText).toContain(escapeLF(moduleNotFoundMessage)) +} + +function expectNoError(moduleName: string, output: string) { + expect(output).not.toContain(getUnsupportedModule(moduleName)) + expect(output).not.toContain(getModuleNotFound(moduleName)) +} + +type Variant = { + title: string + url: string + file: string + getContent: (importStatement: string) => string + getLibContent?: (importStatement: string) => string +} + +function createVariants(opts: { + edgeApi: (importStatement: string) => string + middleware: (importStatement: string) => string + lib?: (importStatement: string) => string +}): Variant[] { + return [ + { + title: 'Edge API', + url: '/api/route', + file: 'pages/api/route.js', + getContent: opts.edgeApi, + getLibContent: opts.lib, + }, + { + title: 'Middleware', + url: '/', + file: 'middleware.js', + getContent: opts.middleware, + getLibContent: opts.lib, + }, + ] +} + +describe('Edge runtime module errors', () => { + // ==================== DEVELOPMENT MODE ==================== + ;(isNextDev ? describe : describe.skip)('development mode', () => { + const { next, isTurbopack, skipped } = nextTestSetup({ + files: __dirname, + dependencies: { + nanoid: 'latest', + }, + // Webpack dev recompiles on every `patchFile` below, which occasionally + // pushes the initial server startup past the default 10s window on + // loaded CI hardware. + startServerTimeout: 30_000, + skipDeployment: true, + }) + if (skipped) return + + // webpack's dev server lazily compiles Edge API routes on demand and + // keeps serving the last-successful compilation when a later compile + // fails. When a test flips `pages/api/route.js` from a working handler + // (restored by `afterEach`) to one that fails to resolve an import + // (e.g. `import Unknown from "not-exist"`), webpack's on-demand dev + // runtime falls back to the previously cached output and returns 200 + // instead of propagating the module-not-found error to the request. + // + // Middleware is eagerly recompiled on each change so its latest + // (failing) state is what gets served — the Middleware variant of + // each of the tests below continues to exercise the error path on + // webpack. Turbopack surfaces the compile error for Edge API too. + // + // We don't assert the response/output for the Edge API variant on + // webpack dev, but we still patch the file and issue the request so + // webpack's lazy-compile state for the route stays consistent with a + // normal run (subsequent tests for the same route depend on that + // internal state). + function isEdgeApiOnWebpackDev(file: string) { + return !isTurbopack && file === 'pages/api/route.js' + } + + let originalApi: string + let originalMiddleware: string + let originalLib: string + + beforeAll(async () => { + originalApi = await next.readFile('pages/api/route.js') + originalMiddleware = await next.readFile('middleware.js') + originalLib = await next.readFile('lib.js') + }) + + afterEach(async () => { + await next.patchFile('pages/api/route.js', originalApi) + await next.patchFile('middleware.js', originalMiddleware) + await next.patchFile('lib.js', originalLib) + }) + + // --- Dynamic import of node.js module --- + + describe.each( + createVariants({ + edgeApi: (imp) => ` + import { NextResponse } from 'next/server' + export default async function handler(request) { + const { writeFile } = ${imp} + return Response.json({ ok: writeFile() }) + } + export const config = { runtime: 'edge' } + `, + middleware: (imp) => ` + import { NextResponse } from 'next/server' + export async function middleware(request) { + const { writeFile } = ${imp} + return NextResponse.next() + } + `, + }) + )( + '$title dynamically importing node.js module', + ({ url, file, getContent }) => { + const moduleName = 'fs' + const importStatement = `await import("${moduleName}")` + + it('throws unsupported module error and highlights the faulty line', async () => { + await next.patchFile(file, getContent(importStatement)) + const outputIndex = next.cliOutput.length + await retry(async () => { + const res = await next.fetch(url) + expect(res.status).toBe(500) + expectUnsupportedModuleDevError( + moduleName, + importStatement, + await res.text(), + next.cliOutput.slice(outputIndex) + ) + }) + }) + } + ) + + // --- Dynamic import of node.js module in a lib --- + + describe.each( + createVariants({ + edgeApi: () => ` + import throwAsync from '../../lib' + export default async function handler(request) { + return Response.json({ ok: await throwAsync() }) + } + export const config = { runtime: 'edge' } + `, + middleware: () => ` + import { NextResponse } from 'next/server' + import throwAsync from './lib' + export async function middleware(request) { + await throwAsync() + return NextResponse.next() + } + `, + lib: (imp) => ` + export default async function throwAsync() { + (${imp}).cwd() + } + `, + }) + )( + '$title dynamically importing node.js module in a lib', + ({ url, file, getContent, getLibContent }) => { + const moduleName = 'os' + const importStatement = `await import("${moduleName}")` + + it('throws unsupported module error and highlights the faulty line', async () => { + await next.patchFile(file, getContent(importStatement)) + if (getLibContent) { + await next.patchFile('lib.js', getLibContent(importStatement)) + } + const outputIndex = next.cliOutput.length + await retry(async () => { + const res = await next.fetch(url) + expect(res.status).toBe(500) + expectUnsupportedModuleDevError( + moduleName, + importStatement, + await res.text(), + next.cliOutput.slice(outputIndex) + ) + }) + }) + } + ) + + // --- Static import of non-existent 3rd party module --- + + describe.each( + createVariants({ + edgeApi: (imp) => ` + ${imp} + export default async function handler(request) { + new Unknown() + return Response.json({ ok: true }) + } + export const config = { runtime: 'edge' } + `, + middleware: (imp) => ` + import { NextResponse } from 'next/server' + ${imp} + export async function middleware(request) { + new Unknown() + return NextResponse.next() + } + `, + }) + )( + '$title statically importing 3rd party module', + ({ url, file, getContent }) => { + const moduleName = 'not-exist' + const importStatement = `import Unknown from "${moduleName}"` + + it('throws not-found module error and highlights the faulty line', async () => { + await next.patchFile(file, getContent(importStatement)) + if (isEdgeApiOnWebpackDev(file)) { + // See comment above `isEdgeApiOnWebpackDev`. + await next.fetch(url).catch(() => {}) + return + } + const outputIndex = next.cliOutput.length + await retry(async () => { + const res = await next.fetch(url) + expect(res.status).toBe(500) + expectModuleNotFoundDevError( + moduleName, + importStatement, + await res.text(), + next.cliOutput.slice(outputIndex) + ) + }) + }) + } + ) + + // --- Import vanilla 3rd party module (nanoid) --- + + describe.each( + createVariants({ + edgeApi: (imp) => ` + ${imp} + export default async function handler(request) { + const response = Response.json({ ok: true }) + response.headers.set('x-from-runtime', nanoid()) + return response + } + export const config = { runtime: 'edge' } + `, + middleware: (imp) => ` + import { NextResponse } from 'next/server' + ${imp} + export async function middleware(request) { + const response = NextResponse.next() + response.headers.set('x-from-runtime', nanoid()) + return response + } + `, + }) + )( + '$title importing vanilla 3rd party module', + ({ url, file, getContent }) => { + const moduleName = 'nanoid' + const importStatement = `import { nanoid } from "${moduleName}"` + + it('does not throw in dev at runtime', async () => { + await next.patchFile(file, getContent(importStatement)) + const outputIndex = next.cliOutput.length + await retry(async () => { + const res = await next.fetch(url) + expect(res.status).toBe(200) + expect(res.headers.get('x-from-runtime')).toBeDefined() + }) + expectNoError(moduleName, next.cliOutput.slice(outputIndex)) + }) + } + ) + + // --- Buffer polyfill --- + + describe.each( + createVariants({ + edgeApi: (imp) => ` + ${imp} + export default async function handler(request) { + const response = Response.json({ ok: true }) + response.headers.set('x-from-runtime', Buffer.isBuffer('a string')) + return response + } + export const config = { runtime: 'edge' } + `, + middleware: (imp) => ` + import { NextResponse } from 'next/server' + ${imp} + export async function middleware(request) { + const response = NextResponse.next() + response.headers.set('x-from-runtime', Buffer.isBuffer('a string')) + return response + } + `, + }) + )('$title using Buffer polyfill', ({ url, file, getContent }) => { + const moduleName = 'buffer' + const importStatement = `import { Buffer } from "${moduleName}"` + + it('does not throw in dev at runtime', async () => { + await next.patchFile(file, getContent(importStatement)) + const outputIndex = next.cliOutput.length + await retry(async () => { + const res = await next.fetch(url) + expect(res.status).toBe(200) + expect(res.headers.get('x-from-runtime')).toBe('false') + }) + expectNoError(moduleName, next.cliOutput.slice(outputIndex)) + }) + }) + + // --- Static import of node.js module --- + + describe.each( + createVariants({ + edgeApi: (imp) => ` + ${imp} + export default async function handler(request) { + basename() + return Response.json({ ok: basename() }) + } + export const config = { runtime: 'edge' } + `, + middleware: (imp) => ` + import { NextResponse } from 'next/server' + ${imp} + export async function middleware(request) { + basename() + return NextResponse.next() + } + `, + }) + )( + '$title statically importing node.js module', + ({ url, file, getContent }) => { + const moduleName = 'fs' + const importStatement = `import { basename } from "${moduleName}"` + + it('throws unsupported module error and highlights the faulty line', async () => { + await next.patchFile(file, getContent(importStatement)) + const outputIndex = next.cliOutput.length + await retry(async () => { + const res = await next.fetch(url) + expect(res.status).toBe(500) + expectUnsupportedModuleDevError( + moduleName, + importStatement, + await res.text(), + next.cliOutput.slice(outputIndex) + ) + }) + }) + } + ) + + // --- Dynamic import of non-existent 3rd party module --- + + describe.each( + createVariants({ + edgeApi: (imp) => ` + export default async function handler(request) { + new (${imp})() + return Response.json({ ok: true }) + } + export const config = { runtime: 'edge' } + `, + middleware: (imp) => ` + import { NextResponse } from 'next/server' + export async function middleware(request) { + new (${imp})() + return NextResponse.next() + } + `, + }) + )( + '$title dynamically importing 3rd party module', + ({ url, file, getContent }) => { + const moduleName = 'not-exist' + const importStatement = `await import("${moduleName}")` + + it('throws not-found module error and highlights the faulty line', async () => { + await next.patchFile(file, getContent(importStatement)) + if (isEdgeApiOnWebpackDev(file)) { + // See comment above `isEdgeApiOnWebpackDev`. + await next.fetch(url).catch(() => {}) + return + } + const outputIndex = next.cliOutput.length + await retry(async () => { + const res = await next.fetch(url) + expect(res.status).toBe(500) + expectModuleNotFoundDevError( + moduleName, + importStatement, + await res.text(), + next.cliOutput.slice(outputIndex) + ) + }) + }) + } + ) + + // --- Dynamic import of unused non-existent 3rd party module --- + + describe.each( + createVariants({ + edgeApi: (imp) => ` + export default async function handler(request) { + if (process.env === 'production') { + new (${imp})() + } + return Response.json({ ok: true }) + } + export const config = { runtime: 'edge' } + `, + middleware: (imp) => ` + import { NextResponse } from 'next/server' + export async function middleware(request) { + if (process.env === 'production') { + new (${imp})() + } + return NextResponse.next() + } + `, + }) + )( + '$title importing unused 3rd party module', + ({ url, file, getContent }) => { + const moduleName = 'not-exist' + const importStatement = `await import("${moduleName}")` + + it('throws not-found module error and highlights the faulty line', async () => { + await next.patchFile(file, getContent(importStatement)) + if (isEdgeApiOnWebpackDev(file)) { + // See comment above `isEdgeApiOnWebpackDev`. + await next.fetch(url).catch(() => {}) + return + } + const outputIndex = next.cliOutput.length + await retry(async () => { + const res = await next.fetch(url) + expect(res.status).toBe(500) + expectModuleNotFoundDevError( + moduleName, + importStatement, + await res.text(), + next.cliOutput.slice(outputIndex) + ) + }) + }) + } + ) + + // --- Dynamic import of unused node.js module --- + + describe.each( + createVariants({ + edgeApi: (imp) => ` + export default async function handler(request) { + if (process.env === 'production') { + (${imp}).spawn('ls', ['-lh', '/usr']) + } + return Response.json({ ok: true }) + } + export const config = { runtime: 'edge' } + `, + middleware: (imp) => ` + import { NextResponse } from 'next/server' + export async function middleware(request) { + if (process.env === 'production') { + (${imp}).spawn('ls', ['-lh', '/usr']) + } + return NextResponse.next() + } + `, + }) + )('$title importing unused node.js module', ({ url, file, getContent }) => { + const moduleName = 'child_process' + const importStatement = `await import("${moduleName}")` + + it('does not throw in dev at runtime', async () => { + await next.patchFile(file, getContent(importStatement)) + const outputIndex = next.cliOutput.length + await retry(async () => { + const res = await next.fetch(url) + expect(res.status).toBe(200) + expectNoError(moduleName, next.cliOutput.slice(outputIndex)) + }) + }) + }) + }) + + // ==================== PRODUCTION MODE ==================== + ;(isNextStart ? describe : describe.skip)('production mode', () => { + const { next, isTurbopack, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + dependencies: { + nanoid: 'latest', + }, + skipDeployment: true, + }) + if (skipped) return + + let originalApi: string + let originalMiddleware: string + let originalLib: string + + beforeAll(async () => { + originalApi = await next.readFile('pages/api/route.js') + originalMiddleware = await next.readFile('middleware.js') + originalLib = await next.readFile('lib.js') + }) + + afterEach(async () => { + try { + await next.stop() + } catch {} + await next.patchFile('pages/api/route.js', originalApi) + await next.patchFile('middleware.js', originalMiddleware) + await next.patchFile('lib.js', originalLib) + }) + + // --- Dynamic import of node.js module --- + + describe.each( + createVariants({ + edgeApi: (imp) => ` + import { NextResponse } from 'next/server' + export default async function handler(request) { + const { writeFile } = ${imp} + return Response.json({ ok: writeFile() }) + } + export const config = { runtime: 'edge' } + `, + middleware: (imp) => ` + import { NextResponse } from 'next/server' + export async function middleware(request) { + const { writeFile } = ${imp} + return NextResponse.next() + } + `, + }) + )( + '$title dynamically importing node.js module', + ({ url, file, getContent }) => { + const moduleName = 'fs' + const importStatement = `await import("${moduleName}")` + + it('throws unsupported module error at runtime and prints warning in build', async () => { + await next.patchFile(file, getContent(importStatement)) + const { cliOutput: buildOutput } = await next.build() + expect(buildOutput).toContain(getUnsupportedModuleWarning(moduleName)) + + // TODO: should this be failing build or not in turbopack + if (!isTurbopack) { + await next.start() + // `next.start()` resets `cliOutput`, so capture the offset + // after the server is ready to only read runtime output from + // the request below. + const runtimeIndex = next.cliOutput.length + const res = await next.fetch(url) + expect(res.status).toBe(500) + expectUnsupportedModuleProdError( + moduleName, + next.cliOutput.slice(runtimeIndex) + ) + } + }) + } + ) + + // --- Dynamic import of node.js module in a lib --- + + describe.each( + createVariants({ + edgeApi: () => ` + import throwAsync from '../../lib' + export default async function handler(request) { + return Response.json({ ok: await throwAsync() }) + } + export const config = { runtime: 'edge' } + `, + middleware: () => ` + import { NextResponse } from 'next/server' + import throwAsync from './lib' + export async function middleware(request) { + await throwAsync() + return NextResponse.next() + } + `, + lib: (imp) => ` + export default async function throwAsync() { + (${imp}).cwd() + } + `, + }) + )( + '$title dynamically importing node.js module in a lib', + ({ url, file, getContent, getLibContent }) => { + const moduleName = 'os' + const importStatement = `await import("${moduleName}")` + + it('throws unsupported module error at runtime and prints warning in build', async () => { + await next.patchFile(file, getContent(importStatement)) + if (getLibContent) { + await next.patchFile('lib.js', getLibContent(importStatement)) + } + const { cliOutput: buildOutput } = await next.build() + expect(buildOutput).toContain(getUnsupportedModuleWarning(moduleName)) + await next.start() + // NOTE: `next.start()` resets `cliOutput`, so capture the offset + // after the server is ready so we only read runtime output from the + // request below. + const runtimeIndex = next.cliOutput.length + const res = await next.fetch(url) + expect(res.status).toBe(500) + expectUnsupportedModuleProdError( + moduleName, + next.cliOutput.slice(runtimeIndex) + ) + }) + } + ) + + // --- Static import of non-existent 3rd party module --- + + describe.each( + createVariants({ + edgeApi: (imp) => ` + ${imp} + export default async function handler(request) { + new Unknown() + return Response.json({ ok: true }) + } + export const config = { runtime: 'edge' } + `, + middleware: (imp) => ` + import { NextResponse } from 'next/server' + ${imp} + export async function middleware(request) { + new Unknown() + return NextResponse.next() + } + `, + }) + )( + '$title statically importing 3rd party module', + ({ file, getContent }) => { + const moduleName = 'not-exist' + const importStatement = `import Unknown from "${moduleName}"` + + it('does not build and reports module not found error', async () => { + await next.patchFile(file, getContent(importStatement)) + const { exitCode, cliOutput: buildOutput } = await next.build() + expect(exitCode).toEqual(1) + expectModuleNotFoundProdError(moduleName, buildOutput) + }) + } + ) + + // --- Import vanilla 3rd party module (nanoid) --- + + describe.each( + createVariants({ + edgeApi: (imp) => ` + ${imp} + export default async function handler(request) { + const response = Response.json({ ok: true }) + response.headers.set('x-from-runtime', nanoid()) + return response + } + export const config = { runtime: 'edge' } + `, + middleware: (imp) => ` + import { NextResponse } from 'next/server' + ${imp} + export async function middleware(request) { + const response = NextResponse.next() + response.headers.set('x-from-runtime', nanoid()) + return response + } + `, + }) + )( + '$title importing vanilla 3rd party module', + ({ url, file, getContent }) => { + const moduleName = 'nanoid' + const importStatement = `import { nanoid } from "${moduleName}"` + + it('does not throw in production at runtime', async () => { + await next.patchFile(file, getContent(importStatement)) + const { cliOutput: buildOutput } = await next.build() + expect(buildOutput).not.toContain( + getUnsupportedModuleWarning(moduleName) + ) + await next.start() + const runtimeIndex = next.cliOutput.length + const res = await next.fetch(url) + expect(res.status).toBe(200) + expect(res.headers.get('x-from-runtime')).toBeDefined() + expectNoError(moduleName, next.cliOutput.slice(runtimeIndex)) + }) + } + ) + + // --- Buffer polyfill --- + + describe.each( + createVariants({ + edgeApi: (imp) => ` + ${imp} + export default async function handler(request) { + const response = Response.json({ ok: true }) + response.headers.set('x-from-runtime', Buffer.isBuffer('a string')) + return response + } + export const config = { runtime: 'edge' } + `, + middleware: (imp) => ` + import { NextResponse } from 'next/server' + ${imp} + export async function middleware(request) { + const response = NextResponse.next() + response.headers.set('x-from-runtime', Buffer.isBuffer('a string')) + return response + } + `, + }) + )('$title using Buffer polyfill', ({ url, file, getContent }) => { + const moduleName = 'buffer' + const importStatement = `import { Buffer } from "${moduleName}"` + + it('does not throw in production at runtime', async () => { + await next.patchFile(file, getContent(importStatement)) + await next.build() + await next.start() + const runtimeIndex = next.cliOutput.length + const res = await next.fetch(url) + expect(res.status).toBe(200) + expect(res.headers.get('x-from-runtime')).toBe('false') + expectNoError(moduleName, next.cliOutput.slice(runtimeIndex)) + }) + }) + + // --- Static import of node.js module --- + + describe.each( + createVariants({ + edgeApi: (imp) => ` + ${imp} + export default async function handler(request) { + basename() + return Response.json({ ok: basename() }) + } + export const config = { runtime: 'edge' } + `, + middleware: (imp) => ` + import { NextResponse } from 'next/server' + ${imp} + export async function middleware(request) { + basename() + return NextResponse.next() + } + `, + }) + )( + '$title statically importing node.js module', + ({ url, file, getContent }) => { + const moduleName = 'fs' + const importStatement = `import { basename } from "${moduleName}"` + + it('throws unsupported module error at runtime and prints warning in build', async () => { + await next.patchFile(file, getContent(importStatement)) + const { cliOutput: buildOutput } = await next.build() + expect(buildOutput).toContain(getUnsupportedModuleWarning(moduleName)) + await next.start() + const runtimeIndex = next.cliOutput.length + const res = await next.fetch(url) + expect(res.status).toBe(500) + expectUnsupportedModuleProdError( + moduleName, + next.cliOutput.slice(runtimeIndex) + ) + }) + } + ) + + // --- Dynamic import of non-existent 3rd party module --- + + describe.each( + createVariants({ + edgeApi: (imp) => ` + export default async function handler(request) { + new (${imp})() + return Response.json({ ok: true }) + } + export const config = { runtime: 'edge' } + `, + middleware: (imp) => ` + import { NextResponse } from 'next/server' + export async function middleware(request) { + new (${imp})() + return NextResponse.next() + } + `, + }) + )( + '$title dynamically importing 3rd party module', + ({ file, getContent }) => { + const moduleName = 'not-exist' + const importStatement = `await import("${moduleName}")` + + it('does not build and reports module not found error', async () => { + await next.patchFile(file, getContent(importStatement)) + const { exitCode, cliOutput: buildOutput } = await next.build() + expect(exitCode).toEqual(1) + expectModuleNotFoundProdError(moduleName, buildOutput) + }) + } + ) + + // --- Dynamic import of unused non-existent 3rd party module --- + + describe.each( + createVariants({ + edgeApi: (imp) => ` + export default async function handler(request) { + if (process.env === 'production') { + new (${imp})() + } + return Response.json({ ok: true }) + } + export const config = { runtime: 'edge' } + `, + middleware: (imp) => ` + import { NextResponse } from 'next/server' + export async function middleware(request) { + if (process.env === 'production') { + new (${imp})() + } + return NextResponse.next() + } + `, + }) + )('$title importing unused 3rd party module', ({ file, getContent }) => { + const moduleName = 'not-exist' + const importStatement = `await import("${moduleName}")` + + it('does not build and reports module not found error', async () => { + await next.patchFile(file, getContent(importStatement)) + const { exitCode, cliOutput: buildOutput } = await next.build() + expect(exitCode).toEqual(1) + expectModuleNotFoundProdError(moduleName, buildOutput) + }) + }) + + // --- Dynamic import of unused node.js module --- + + describe.each( + createVariants({ + edgeApi: (imp) => ` + export default async function handler(request) { + if (process.env === 'production') { + (${imp}).spawn('ls', ['-lh', '/usr']) + } + return Response.json({ ok: true }) + } + export const config = { runtime: 'edge' } + `, + middleware: (imp) => ` + import { NextResponse } from 'next/server' + export async function middleware(request) { + if (process.env === 'production') { + (${imp}).spawn('ls', ['-lh', '/usr']) + } + return NextResponse.next() + } + `, + }) + )('$title importing unused node.js module', ({ url, file, getContent }) => { + const moduleName = 'child_process' + const importStatement = `await import("${moduleName}")` + + it('does not throw in production at runtime', async () => { + await next.patchFile(file, getContent(importStatement)) + const { cliOutput: buildOutput } = await next.build() + expect(buildOutput).toContain(getUnsupportedModuleWarning(moduleName)) + await next.start() + const runtimeIndex = next.cliOutput.length + const res = await next.fetch(url) + expect(res.status).toBe(200) + const runtimeOutput = next.cliOutput.slice(runtimeIndex) + expect(runtimeOutput).not.toContain( + getUnsupportedModuleWarning(moduleName) + ) + expect(runtimeOutput).not.toContain(getModuleNotFound(moduleName)) + }) + }) + }) +}) diff --git a/test/integration/edge-runtime-module-errors/lib.js b/test/e2e/edge-runtime-module-errors/lib.js similarity index 100% rename from test/integration/edge-runtime-module-errors/lib.js rename to test/e2e/edge-runtime-module-errors/lib.js diff --git a/test/integration/edge-runtime-module-errors/middleware.js b/test/e2e/edge-runtime-module-errors/middleware.js similarity index 100% rename from test/integration/edge-runtime-module-errors/middleware.js rename to test/e2e/edge-runtime-module-errors/middleware.js diff --git a/test/integration/edge-runtime-module-errors/pages/api/route.js b/test/e2e/edge-runtime-module-errors/pages/api/route.js similarity index 100% rename from test/integration/edge-runtime-module-errors/pages/api/route.js rename to test/e2e/edge-runtime-module-errors/pages/api/route.js diff --git a/test/integration/edge-runtime-module-errors/pages/index.js b/test/e2e/edge-runtime-module-errors/pages/index.js similarity index 100% rename from test/integration/edge-runtime-module-errors/pages/index.js rename to test/e2e/edge-runtime-module-errors/pages/index.js diff --git a/test/e2e/edge-runtime-response-error/edge-runtime-response-error.test.ts b/test/e2e/edge-runtime-response-error/edge-runtime-response-error.test.ts new file mode 100644 index 000000000000..7ce8cffccefc --- /dev/null +++ b/test/e2e/edge-runtime-response-error/edge-runtime-response-error.test.ts @@ -0,0 +1,24 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('Edge runtime response error', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + disableAutoSkewProtection: true, + // Assertions don't apply to deploy mode (output differs vs. local Next.js server). + skipDeployment: true, + }) + if (skipped) return + + describe.each([ + { title: 'Edge API', url: '/api/route' }, + { title: 'Middleware', url: '/' }, + ])('test error if response is not Response type', ({ title, url }) => { + it(`${title} test Response`, async () => { + const res = await next.fetch(url) + expect(next.cliOutput).toContain( + 'Expected an instance of Response to be returned' + ) + expect(res.status).toBe(500) + }) + }) +}) diff --git a/test/integration/edge-runtime-response-error/lib.js b/test/e2e/edge-runtime-response-error/lib.js similarity index 100% rename from test/integration/edge-runtime-response-error/lib.js rename to test/e2e/edge-runtime-response-error/lib.js diff --git a/test/integration/edge-runtime-response-error/middleware.js b/test/e2e/edge-runtime-response-error/middleware.js similarity index 100% rename from test/integration/edge-runtime-response-error/middleware.js rename to test/e2e/edge-runtime-response-error/middleware.js diff --git a/test/integration/edge-runtime-response-error/pages/api/route.js b/test/e2e/edge-runtime-response-error/pages/api/route.js similarity index 100% rename from test/integration/edge-runtime-response-error/pages/api/route.js rename to test/e2e/edge-runtime-response-error/pages/api/route.js diff --git a/test/integration/edge-runtime-response-error/pages/index.js b/test/e2e/edge-runtime-response-error/pages/index.js similarity index 100% rename from test/integration/edge-runtime-response-error/pages/index.js rename to test/e2e/edge-runtime-response-error/pages/index.js diff --git a/test/e2e/edge-runtime-streaming-error/edge-runtime-streaming-error.test.ts b/test/e2e/edge-runtime-streaming-error/edge-runtime-streaming-error.test.ts new file mode 100644 index 000000000000..7cda3e2a675c --- /dev/null +++ b/test/e2e/edge-runtime-streaming-error/edge-runtime-streaming-error.test.ts @@ -0,0 +1,26 @@ +import stripAnsi from 'next/dist/compiled/strip-ansi' +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('edge-runtime-streaming-error', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + disableAutoSkewProtection: true, + // Assertions don't apply to deploy mode (output differs vs. local Next.js server). + skipDeployment: true, + }) + if (skipped) return + + it('logs the error correctly', async () => { + const res = await next.fetch('/api/test') + expect(await res.text()).toEqual('hello') + expect(res.status).toBe(200) + + await retry(() => { + expect(stripAnsi(next.cliOutput)).toMatch( + /The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received type boolean/ + ) + }) + expect(stripAnsi(next.cliOutput)).not.toContain('webpack-internal:') + }) +}) diff --git a/test/integration/edge-runtime-streaming-error/pages/api/test.js b/test/e2e/edge-runtime-streaming-error/pages/api/test.js similarity index 100% rename from test/integration/edge-runtime-streaming-error/pages/api/test.js rename to test/e2e/edge-runtime-streaming-error/pages/api/test.js diff --git a/test/e2e/edge-runtime-with-node.js-apis/edge-runtime-with-node.js-apis.test.ts b/test/e2e/edge-runtime-with-node.js-apis/edge-runtime-with-node.js-apis.test.ts new file mode 100644 index 000000000000..c82eb83fbc5e --- /dev/null +++ b/test/e2e/edge-runtime-with-node.js-apis/edge-runtime-with-node.js-apis.test.ts @@ -0,0 +1,118 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' +import stripAnsi from 'next/dist/compiled/strip-ansi' + +const unsupportedFunctions = [ + 'setImmediate', + 'clearImmediate', + 'process.cwd', + 'process.cpuUsage', + 'process.getuid', +] +const undefinedProperties = ['process.arch', 'process.version'] +const unsupportedClasses = [ + 'BroadcastChannel', + 'ByteLengthQueuingStrategy', + 'CompressionStream', + 'CountQueuingStrategy', + 'DecompressionStream', + 'DomException', + 'MessageChannel', + 'MessageEvent', + 'MessagePort', + 'ReadableByteStreamController', + 'ReadableStreamBYOBRequest', + 'ReadableStreamDefaultController', + 'TransformStreamDefaultController', + 'WritableStreamDefaultController', +] + +describe('Edge runtime with Node.js APIs', () => { + const { next, isNextDev, skipped } = nextTestSetup({ + files: __dirname, + // Turbopack builds fail (non-zero exit) when edge code uses Node.js APIs, + // but the CLI output still contains the warnings we're asserting on. Skip + // the automatic start so we can run the build manually and ignore the + // non-zero exit code. + skipStart: true, + // Vercel deployment fails to build/deploy this fixture in CI; skip in deploy mode. + skipDeployment: true, + }) + if (skipped) return + + beforeAll(async () => { + if (isNextDev) { + await next.start() + } else { + try { + await next.build() + } catch { + // Build is expected to fail in production when edge code uses + // unsupported Node.js APIs under Turbopack. + } + } + }) + + describe.each([ + { + title: 'Middleware', + computeRoute(useCase: string) { + return `/${useCase}` + }, + }, + { + title: 'Edge route', + computeRoute(useCase: string) { + return `/api/route?case=${useCase}` + }, + }, + ])('$title', ({ computeRoute }) => { + if (isNextDev) { + it.each(undefinedProperties.map((api) => ({ api })))( + 'does not throw on using $api', + async ({ api }) => { + const res = await next.fetch(computeRoute(api)) + expect(res.status).toBe(200) + } + ) + + it.each([ + ...unsupportedFunctions.map((api) => ({ + api, + errorHighlight: `${api}(`, + })), + ...unsupportedClasses.map((api) => ({ + api, + errorHighlight: `new ${api}(`, + })), + ])('throws error when using $api', async ({ api, errorHighlight }) => { + const outputIndex = next.cliOutput.length + const res = await next.fetch(computeRoute(api)) + expect(res.status).toBe(500) + + await retry(async () => { + const newOutput = next.cliOutput.slice(outputIndex) + expect(newOutput).toInclude( + `A Node.js API is used (${api}) which is not supported in the Edge Runtime.\nLearn more: https://nextjs.org/docs/api-reference/edge-runtime` + ) + expect(stripAnsi(newOutput)).toInclude(errorHighlight) + }) + }) + } else { + it.each( + [...unsupportedFunctions, ...unsupportedClasses].map((api) => ({ + api, + })) + )('warns for $api during build', ({ api }) => { + expect(next.cliOutput).toContain(`A Node.js API is used (${api}`) + }) + + it.each([...undefinedProperties].map((api) => ({ api })))( + 'does not warn on using $api', + ({ api }) => { + expect(next.cliOutput).toContain(`A Node.js API is used (${api}`) + } + ) + } + }) +}) diff --git a/test/integration/edge-runtime-with-node.js-apis/lib/utils.js b/test/e2e/edge-runtime-with-node.js-apis/lib/utils.js similarity index 100% rename from test/integration/edge-runtime-with-node.js-apis/lib/utils.js rename to test/e2e/edge-runtime-with-node.js-apis/lib/utils.js diff --git a/test/integration/edge-runtime-with-node.js-apis/middleware.js b/test/e2e/edge-runtime-with-node.js-apis/middleware.js similarity index 100% rename from test/integration/edge-runtime-with-node.js-apis/middleware.js rename to test/e2e/edge-runtime-with-node.js-apis/middleware.js diff --git a/test/integration/edge-runtime-with-node.js-apis/pages/api/route.js b/test/e2e/edge-runtime-with-node.js-apis/pages/api/route.js similarity index 100% rename from test/integration/edge-runtime-with-node.js-apis/pages/api/route.js rename to test/e2e/edge-runtime-with-node.js-apis/pages/api/route.js diff --git a/test/integration/edge-runtime-with-node.js-apis/pages/index.js b/test/e2e/edge-runtime-with-node.js-apis/pages/index.js similarity index 100% rename from test/integration/edge-runtime-with-node.js-apis/pages/index.js rename to test/e2e/edge-runtime-with-node.js-apis/pages/index.js diff --git a/test/integration/env-config/app/.env b/test/e2e/env-config/.env similarity index 100% rename from test/integration/env-config/app/.env rename to test/e2e/env-config/.env diff --git a/test/integration/env-config/app/.env.development b/test/e2e/env-config/.env.development similarity index 100% rename from test/integration/env-config/app/.env.development rename to test/e2e/env-config/.env.development diff --git a/test/integration/env-config/app/.env.development.local b/test/e2e/env-config/.env.development.local similarity index 100% rename from test/integration/env-config/app/.env.development.local rename to test/e2e/env-config/.env.development.local diff --git a/test/integration/env-config/app/.env.local b/test/e2e/env-config/.env.local similarity index 100% rename from test/integration/env-config/app/.env.local rename to test/e2e/env-config/.env.local diff --git a/test/integration/env-config/app/.env.production b/test/e2e/env-config/.env.production similarity index 100% rename from test/integration/env-config/app/.env.production rename to test/e2e/env-config/.env.production diff --git a/test/integration/env-config/app/.env.production.local b/test/e2e/env-config/.env.production.local similarity index 100% rename from test/integration/env-config/app/.env.production.local rename to test/e2e/env-config/.env.production.local diff --git a/test/integration/env-config/app/.env.test b/test/e2e/env-config/.env.test similarity index 100% rename from test/integration/env-config/app/.env.test rename to test/e2e/env-config/.env.test diff --git a/test/integration/env-config/app/.env.test.local b/test/e2e/env-config/.env.test.local similarity index 100% rename from test/integration/env-config/app/.env.test.local rename to test/e2e/env-config/.env.test.local diff --git a/test/e2e/env-config/.gitignore b/test/e2e/env-config/.gitignore new file mode 100644 index 000000000000..6979b8724f4d --- /dev/null +++ b/test/e2e/env-config/.gitignore @@ -0,0 +1,2 @@ +# Override root .gitignore to track .env*.local fixture files needed by this test +!.env*.local diff --git a/test/e2e/env-config/env-config.test.ts b/test/e2e/env-config/env-config.test.ts new file mode 100644 index 000000000000..5bb7115de6a9 --- /dev/null +++ b/test/e2e/env-config/env-config.test.ts @@ -0,0 +1,311 @@ +import { nextTestSetup, isNextDev } from 'e2e-utils' +import { retry } from 'next-test-utils' +import cheerio from 'cheerio' + +describe('env-config', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + env: { + PROCESS_ENV_KEY: 'processenvironment', + ENV_FILE_PROCESS_ENV: 'env-cli', + }, + skipDeployment: true, + }) + if (skipped) return + + const getEnvFromHtml = async (path: string) => { + const html = await next.render(path) + const $ = cheerio.load(html) + const env = JSON.parse($('p').text()) + env.nextConfigEnv = $('#nextConfigEnv').text() + env.nextConfigPublicEnv = $('#nextConfigPublicEnv').text() + env.nextConfigNewPublicEnv = $('#nextConfigNewPublicEnv').text() + return env + } + + const checkEnvData = ( + data: Record<string, string | undefined>, + didReload = false + ) => { + expect(data.ENV_FILE_KEY).toBe('env') + expect(data.LOCAL_ENV_FILE_KEY).toBe('localenv') + expect(data.DEVELOPMENT_ENV_FILE_KEY).toBe( + isNextDev ? 'development' : undefined + ) + expect(data.LOCAL_DEVELOPMENT_ENV_FILE_KEY).toBe( + isNextDev ? 'localdevelopment' : undefined + ) + expect(data.TEST_ENV_FILE_KEY).toBe(undefined) + expect(data.LOCAL_TEST_ENV_FILE_KEY).toBe(undefined) + expect(data.PRODUCTION_ENV_FILE_KEY).toBe( + isNextDev ? undefined : 'production' + ) + expect(data.LOCAL_PRODUCTION_ENV_FILE_KEY).toBe( + isNextDev ? undefined : 'localproduction' + ) + expect(data.ENV_FILE_EXPANDED).toBe('env') + expect(data.ENV_FILE_EXPANDED_CONCAT).toBe('hello-env') + expect(data.ENV_FILE_EXPANDED_ESCAPED).toBe('$ENV_FILE_KEY') + expect(data.ENV_FILE_KEY_EXCLAMATION).toBe('hello!') + expect(data.ENV_FILE_EMPTY_FIRST).toBe('$escaped') + expect(data.ENV_FILE_PROCESS_ENV).toBe('env-cli') + + if (didReload) { + expect(data.NEW_ENV_KEY).toBe('true') + expect(data.NEW_ENV_LOCAL_KEY).toBe('hello') + expect(data.NEW_ENV_DEV_KEY).toBe('from-dev') + expect(data.NEXT_PUBLIC_HELLO_WORLD).toBe('again') + } + + expect(data.nextConfigEnv).toBe('hello from next.config.js') + expect(data.nextConfigPublicEnv).toBe('hello again from next.config.js') + expect(data.nextConfigNewPublicEnv).toBe('hello set in next.config.js') + } + + it('should have process environment override .env', async () => { + const data = await getEnvFromHtml('/') + expect(data.PROCESS_ENV_KEY).toEqual('processenvironment') + }) + + it('should provide global env to next.config.js', async () => { + const res = await next.fetch('/hello', { redirect: 'manual' }) + const { pathname } = new URL(res.headers.get('location')!) + expect(res.status).toBe(307) + expect(pathname).toBe('/another') + }) + + it('should inline global values during build', async () => { + const browser = await next.browser('/global') + expect(await browser.waitForElementByCss('#global-value').text()).toBe( + 'another' + ) + }) + + it('should provide env for SSG', async () => { + const data = await getEnvFromHtml('/some-ssg') + checkEnvData(data) + }) + + it('should provide env correctly for SSR', async () => { + const data = await getEnvFromHtml('/some-ssp') + checkEnvData(data) + }) + + it('should provide env correctly for API routes', async () => { + const data = JSON.parse(await next.render('/api/all')) + checkEnvData(data) + }) + + it('should load env from .env', async () => { + const data = await getEnvFromHtml('/') + expect(data.ENV_FILE_KEY).toEqual('env') + expect(data.ENV_FILE_DEVELOPMENT_OVERRIDE_TEST).toEqual( + isNextDev ? 'development' : 'env' + ) + expect(data.ENV_FILE_DEVELOPMENT_LOCAL_OVERRIDEOVERRIDE_TEST).toEqual( + isNextDev ? 'localdevelopment' : 'env' + ) + expect(data.ENV_FILE_TEST_OVERRIDE_TEST).toEqual('env') + expect(data.ENV_FILE_TEST_LOCAL_OVERRIDEOVERRIDE_TEST).toBe('env') + expect(data.LOCAL_ENV_FILE_KEY).toBe('localenv') + expect(data.ENV_FILE_PRODUCTION_OVERRIDEOVERRIDE_TEST).toEqual( + isNextDev ? 'env' : 'production' + ) + expect(data.ENV_FILE_PRODUCTION_LOCAL_OVERRIDEOVERRIDE_TEST).toEqual( + isNextDev ? 'env' : 'localproduction' + ) + expect(data.NEXT_PUBLIC_EMPTY_ENV_VAR).toEqual('') + + const browser = await next.browser('/') + expect( + await browser.waitForElementByCss('#nextPublicEmptyEnvVar').text() + ).toBe('content:') + }) + + if (isNextDev) { + describe('with hot reload', () => { + it('should have updated runtime values after change', async () => { + const envContent = await next.readFile('.env') + const envDevContent = await next.readFile('.env.development') + const envLocalContent = await next.readFile('.env.local') + + try { + const initialData = await getEnvFromHtml('/') + expect(initialData.ENV_FILE_KEY).toBe('env') + + await next.patchFile('.env', envContent + '\nNEW_ENV_KEY=true') + await next.patchFile( + '.env.local', + envLocalContent + '\nNEW_ENV_LOCAL_KEY=hello' + ) + await next.patchFile( + '.env.development', + envDevContent + + '\nNEW_ENV_DEV_KEY=from-dev\nNEXT_PUBLIC_HELLO_WORLD=again' + ) + + await retry(async () => { + expect(next.cliOutput).toContain('Reload env:') + }) + + await retry(async () => { + const data = await getEnvFromHtml('/') + expect(data.NEW_ENV_KEY).toBe('true') + expect(data.NEW_ENV_LOCAL_KEY).toBe('hello') + expect(data.NEW_ENV_DEV_KEY).toBe('from-dev') + expect(data.NEXT_PUBLIC_HELLO_WORLD).toBe('again') + }) + + // Re-verify base env data with didReload=true + const ssgData = await getEnvFromHtml('/some-ssg') + checkEnvData(ssgData, true) + const sspData = await getEnvFromHtml('/some-ssp') + checkEnvData(sspData, true) + const apiData = JSON.parse(await next.render('/api/all')) + checkEnvData(apiData, true) + + const outputBefore = next.cliOutput.length + await next.patchFile( + '.env', + envContent.replace('ENV_FILE_KEY=env', 'ENV_FILE_KEY=env-updated') + + '\nNEW_ENV_KEY=true' + ) + + await retry(async () => { + const recentOutput = next.cliOutput.substring(outputBefore) + expect(recentOutput).toContain('Reload env:') + }) + const recentOutput = next.cliOutput.substring(outputBefore) + expect([...recentOutput.matchAll(/Reload env:/g)].length).toBe(1) + expect(recentOutput).not.toContain('.env.local') + + await retry(async () => { + const data = await getEnvFromHtml('/') + expect(data.ENV_FILE_KEY).toBe('env-updated') + expect(data.NEW_ENV_KEY).toBe('true') + }) + + // Now modify .env.local and verify it's detected + const outputBefore2 = next.cliOutput.length + await next.patchFile( + '.env.local', + envLocalContent.replace( + 'ENV_FILE_LOCAL_OVERRIDE_TEST=localenv', + 'ENV_FILE_LOCAL_OVERRIDE_TEST=localenv-updated' + ) + ) + + await retry(async () => { + const recentOutput2 = next.cliOutput.substring(outputBefore2) + expect(recentOutput2).toContain('Reload env:') + }) + expect(next.cliOutput.substring(outputBefore2)).toContain( + '.env.local' + ) + + await retry(async () => { + const data = await getEnvFromHtml('/') + expect(data.ENV_FILE_KEY).toBe('env-updated') + expect(data.ENV_FILE_LOCAL_OVERRIDE_TEST).toBe('localenv-updated') + }) + } finally { + await next.patchFile('.env', envContent) + await next.patchFile('.env.development', envDevContent) + await next.patchFile('.env.local', envLocalContent) + } + }) + + it('should trigger HMR correctly when NEXT_PUBLIC_ env is changed', async () => { + const envContent = await next.readFile('.env') + const envLocalContent = await next.readFile('.env.local') + + try { + const browser = await next.browser('/global') + expect( + await browser.waitForElementByCss('#global-value').text() + ).toBe('another') + + let outputBefore = next.cliOutput.length + await next.patchFile( + '.env', + envContent.replace( + 'NEXT_PUBLIC_TEST_DEST=another', + 'NEXT_PUBLIC_TEST_DEST=replaced' + ) + ) + + await retry(async () => { + const recentOutput = next.cliOutput.substring(outputBefore) + expect(recentOutput).toContain('Reload env:') + }) + let recentOutput = next.cliOutput.substring(outputBefore) + expect([...recentOutput.matchAll(/Reload env:/g)].length).toBe(1) + expect(recentOutput).not.toContain('.env.local') + + await retry(async () => { + expect( + await browser.waitForElementByCss('#global-value').text() + ).toBe('replaced') + }) + + // Override via .env.local + outputBefore = next.cliOutput.length + await next.patchFile( + '.env.local', + envLocalContent + '\nNEXT_PUBLIC_TEST_DEST=overridden' + ) + + await retry(async () => { + recentOutput = next.cliOutput.substring(outputBefore) + expect(recentOutput).toContain('Reload env:') + }) + recentOutput = next.cliOutput.substring(outputBefore) + expect([...recentOutput.matchAll(/Reload env:/g)].length).toBe(1) + expect(recentOutput).toContain('.env.local') + + await retry(async () => { + expect( + await browser.waitForElementByCss('#global-value').text() + ).toBe('overridden') + }) + + // Restore .env.local + outputBefore = next.cliOutput.length + await next.patchFile('.env.local', envLocalContent) + + await retry(async () => { + recentOutput = next.cliOutput.substring(outputBefore) + expect(recentOutput).toContain('Reload env:') + }) + recentOutput = next.cliOutput.substring(outputBefore) + expect([...recentOutput.matchAll(/Reload env:/g)].length).toBe(1) + expect(recentOutput).toContain('.env.local') + + await retry(async () => { + expect( + await browser.waitForElementByCss('#global-value').text() + ).toBe('replaced') + }) + + // Restore .env to original + const outputBefore2 = next.cliOutput.length + await next.patchFile('.env', envContent) + + await retry(async () => { + const recentOutput2 = next.cliOutput.substring(outputBefore2) + expect(recentOutput2).toContain('Reload env:') + }) + + await retry(async () => { + expect(await browser.elementByCss('#global-value').text()).toBe( + 'another' + ) + }) + } finally { + await next.patchFile('.env', envContent) + await next.patchFile('.env.local', envLocalContent) + } + }) + }) + } +}) diff --git a/test/integration/env-config/app/next.config.js b/test/e2e/env-config/next.config.js similarity index 100% rename from test/integration/env-config/app/next.config.js rename to test/e2e/env-config/next.config.js diff --git a/test/integration/env-config/app/pages/another-global.js b/test/e2e/env-config/pages/another-global.js similarity index 100% rename from test/integration/env-config/app/pages/another-global.js rename to test/e2e/env-config/pages/another-global.js diff --git a/test/integration/env-config/app/pages/api/all.js b/test/e2e/env-config/pages/api/all.js similarity index 100% rename from test/integration/env-config/app/pages/api/all.js rename to test/e2e/env-config/pages/api/all.js diff --git a/test/integration/env-config/app/pages/global.js b/test/e2e/env-config/pages/global.js similarity index 100% rename from test/integration/env-config/app/pages/global.js rename to test/e2e/env-config/pages/global.js diff --git a/test/integration/env-config/app/pages/index.js b/test/e2e/env-config/pages/index.js similarity index 100% rename from test/integration/env-config/app/pages/index.js rename to test/e2e/env-config/pages/index.js diff --git a/test/integration/env-config/app/pages/some-ssg.js b/test/e2e/env-config/pages/some-ssg.js similarity index 100% rename from test/integration/env-config/app/pages/some-ssg.js rename to test/e2e/env-config/pages/some-ssg.js diff --git a/test/integration/env-config/app/pages/some-ssp.js b/test/e2e/env-config/pages/some-ssp.js similarity index 100% rename from test/integration/env-config/app/pages/some-ssp.js rename to test/e2e/env-config/pages/some-ssp.js diff --git a/test/integration/cli/duplicate-sass/.gitignore b/test/e2e/externals-pages-bundle/.gitignore similarity index 100% rename from test/integration/cli/duplicate-sass/.gitignore rename to test/e2e/externals-pages-bundle/.gitignore diff --git a/test/e2e/externals-pages-bundle/externals-pages-bundle.test.ts b/test/e2e/externals-pages-bundle/externals-pages-bundle.test.ts new file mode 100644 index 000000000000..7c72c7486e97 --- /dev/null +++ b/test/e2e/externals-pages-bundle/externals-pages-bundle.test.ts @@ -0,0 +1,99 @@ +import fs from 'fs/promises' +import { join } from 'path' +import { nextTestSetup, isNextStart, isNextDev } from 'e2e-utils' + +describe('externals-pages-bundle', () => { + const { next, isTurbopack, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + if (skipped) return + + describe('bundle pages externals with config.bundlePagesRouterDependencies', () => { + if (!isNextStart) { + it('skip for non-production mode', () => {}) + return + } + + beforeAll(async () => { + await next.build() + }) + + it('should have no externals with the config set', async () => { + if (isTurbopack) { + const ssrPath = join(next.testDir, '.next/server/chunks/ssr') + const pageBundleBasenames = (await fs.readdir(ssrPath)).filter((p) => + p.match(/\.js$/) + ) + expect(pageBundleBasenames).not.toBeEmpty() + let allBundles = '' + for (const basename of pageBundleBasenames) { + const output = await fs.readFile(join(ssrPath, basename), 'utf8') + allBundles += output + } + + expect(allBundles).toContain('"external-package content"') + } else { + const output = await next.readFile('.next/server/pages/index.js') + expect(output).not.toContain('require("external-package")') + } + }) + + it('should respect the serverExternalPackages config', async () => { + if (isTurbopack) { + const ssrPath = join(next.testDir, '.next/server/chunks/ssr') + const pageBundleBasenames = (await fs.readdir(ssrPath)).filter((p) => + p.match(/\.js$/) + ) + expect(pageBundleBasenames).not.toBeEmpty() + let allBundles = '' + for (const basename of pageBundleBasenames) { + const output = await fs.readFile(join(ssrPath, basename), 'utf8') + allBundles += output + } + + expect(allBundles).not.toContain('"opted-out-external-package content"') + } else { + const output = await next.readFile('.next/server/pages/index.js') + expect(output).toContain('require("opted-out-external-package")') + } + }) + }) + + describe('default externals (dev mode)', () => { + if (!isNextDev) { + it('skip for non-dev mode', () => {}) + return + } + + beforeAll(async () => { + await next.deleteFile('next.config.js') + await next.start() + }) + + it('should use externals for unvendored node_modules reachable from the project', async () => { + await next.render('/') + if (isTurbopack) { + const ssrPath = join(next.testDir, `${next.distDir}/server/chunks/ssr`) + const pageBundleBasenames = (await fs.readdir(ssrPath)).filter((p) => + p.match(/\.js$/) + ) + expect(pageBundleBasenames).not.toBeEmpty() + let allBundles = '' + for (const basename of pageBundleBasenames) { + const output = await fs.readFile(join(ssrPath, basename), 'utf8') + allBundles += output + } + + expect(allBundles).toMatch(/"external-package(-[0-9a-f]+)?"/) + expect(allBundles).not.toContain('"external-package content"') + } else { + const output = await next.readFile( + `${next.distDir}/server/pages/index.js` + ) + expect(output).toContain('require("external-package")') + } + }) + }) +}) diff --git a/test/integration/externals-pages-bundle/next.config.js b/test/e2e/externals-pages-bundle/next.config.js similarity index 100% rename from test/integration/externals-pages-bundle/next.config.js rename to test/e2e/externals-pages-bundle/next.config.js diff --git a/test/integration/externals-pages-bundle/node_modules/external-package/index.js b/test/e2e/externals-pages-bundle/node_modules/external-package/index.js similarity index 100% rename from test/integration/externals-pages-bundle/node_modules/external-package/index.js rename to test/e2e/externals-pages-bundle/node_modules/external-package/index.js diff --git a/test/integration/externals-pages-bundle/node_modules/external-package/package.json b/test/e2e/externals-pages-bundle/node_modules/external-package/package.json similarity index 100% rename from test/integration/externals-pages-bundle/node_modules/external-package/package.json rename to test/e2e/externals-pages-bundle/node_modules/external-package/package.json diff --git a/test/integration/externals-pages-bundle/node_modules/opted-out-external-package/index.js b/test/e2e/externals-pages-bundle/node_modules/opted-out-external-package/index.js similarity index 100% rename from test/integration/externals-pages-bundle/node_modules/opted-out-external-package/index.js rename to test/e2e/externals-pages-bundle/node_modules/opted-out-external-package/index.js diff --git a/test/integration/externals-pages-bundle/node_modules/opted-out-external-package/package.json b/test/e2e/externals-pages-bundle/node_modules/opted-out-external-package/package.json similarity index 100% rename from test/integration/externals-pages-bundle/node_modules/opted-out-external-package/package.json rename to test/e2e/externals-pages-bundle/node_modules/opted-out-external-package/package.json diff --git a/test/integration/externals-pages-bundle/pages/index.js b/test/e2e/externals-pages-bundle/pages/index.js similarity index 100% rename from test/integration/externals-pages-bundle/pages/index.js rename to test/e2e/externals-pages-bundle/pages/index.js diff --git a/test/integration/fallback-false-rewrite/test/index.test.ts b/test/e2e/fallback-false-rewrite/fallback-false-rewrite.test.ts similarity index 58% rename from test/integration/fallback-false-rewrite/test/index.test.ts rename to test/e2e/fallback-false-rewrite/fallback-false-rewrite.test.ts index f4cb7eadf194..28e376700b9d 100644 --- a/test/integration/fallback-false-rewrite/test/index.test.ts +++ b/test/e2e/fallback-false-rewrite/fallback-false-rewrite.test.ts @@ -1,27 +1,16 @@ -/* eslint-env jest */ - -import fs from 'fs-extra' -import { join } from 'path' +import { nextTestSetup } from 'e2e-utils' import cheerio from 'cheerio' -import webdriver from 'next-webdriver' -import { - killApp, - findPort, - nextBuild, - nextStart, - launchApp, - fetchViaHTTP, -} from 'next-test-utils' - -const appDir = join(__dirname, '../') -let appPort -let app - -const runTests = () => { + +describe('fallback: false rewrite', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + // Assertions don't apply to deploy mode (output differs vs. local Next.js server). + skipDeployment: true, + }) + if (skipped) return + it('should rewrite correctly for path at same level as fallback: false SSR', async () => { - const res = await fetchViaHTTP(appPort, '/hello', undefined, { - redirect: 'manual', - }) + const res = await next.fetch('/hello', { redirect: 'manual' }) expect(res.status).toBe(200) const html = await res.text() @@ -34,9 +23,7 @@ const runTests = () => { }) it('should rewrite correctly for path above fallback: false SSR', async () => { - const res = await fetchViaHTTP(appPort, '/hello/world', undefined, { - redirect: 'manual', - }) + const res = await next.fetch('/hello/world', { redirect: 'manual' }) expect(res.status).toBe(200) const html = await res.text() @@ -49,7 +36,7 @@ const runTests = () => { }) it('should rewrite correctly for path at same level as fallback: false client', async () => { - const browser = await webdriver(appPort, '/hello') + const browser = await next.browser('/hello') expect(await browser.elementByCss('#another').text()).toBe('another') expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ @@ -58,7 +45,7 @@ const runTests = () => { }) it('should rewrite correctly for path above fallback: false client', async () => { - const browser = await webdriver(appPort, '/hello/world') + const browser = await next.browser('/hello/world') expect(await browser.elementByCss('#another').text()).toBe('another') expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ @@ -67,9 +54,7 @@ const runTests = () => { }) it('should not rewrite for path from fallback: false SSR', async () => { - const res = await fetchViaHTTP(appPort, '/first', undefined, { - redirect: 'manual', - }) + const res = await next.fetch('/first', { redirect: 'manual' }) expect(res.status).toBe(200) const html = await res.text() @@ -82,7 +67,7 @@ const runTests = () => { }) it('should not rewrite for path from fallback: false client', async () => { - const browser = await webdriver(appPort, '/second') + const browser = await next.browser('/second') expect(await browser.elementByCss('#slug').text()).toContain('hello') expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ @@ -91,9 +76,7 @@ const runTests = () => { }) it('should behave properly when accessing the dynamic param directly', async () => { - const res = await fetchViaHTTP(appPort, '/[slug]', undefined, { - redirect: 'manual', - }) + const res = await next.fetch('/[slug]', { redirect: 'manual' }) expect(res.status).toBe(200) const html = await res.text() @@ -104,34 +87,4 @@ const runTests = () => { path: ['[slug]'], }) }) -} - -describe('fallback: false rewrite', () => { - ;(process.env.TURBOPACK_BUILD ? describe.skip : describe)( - 'development mode', - () => { - beforeAll(async () => { - await fs.remove(join(appDir, '.next')) - appPort = await findPort() - app = await launchApp(appDir, appPort) - }) - afterAll(() => killApp(app)) - - runTests() - } - ) - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( - 'production mode', - () => { - beforeAll(async () => { - await fs.remove(join(appDir, '.next')) - await nextBuild(appDir, []) - appPort = await findPort() - app = await nextStart(appDir, appPort) - }) - afterAll(() => killApp(app)) - - runTests() - } - ) }) diff --git a/test/integration/fallback-false-rewrite/next.config.js b/test/e2e/fallback-false-rewrite/next.config.js similarity index 100% rename from test/integration/fallback-false-rewrite/next.config.js rename to test/e2e/fallback-false-rewrite/next.config.js diff --git a/test/integration/fallback-false-rewrite/pages/[slug].js b/test/e2e/fallback-false-rewrite/pages/[slug].js similarity index 100% rename from test/integration/fallback-false-rewrite/pages/[slug].js rename to test/e2e/fallback-false-rewrite/pages/[slug].js diff --git a/test/integration/fallback-false-rewrite/pages/another.js b/test/e2e/fallback-false-rewrite/pages/another.js similarity index 100% rename from test/integration/fallback-false-rewrite/pages/another.js rename to test/e2e/fallback-false-rewrite/pages/another.js diff --git a/test/e2e/fallback-route-params/fallback-route-params.test.ts b/test/e2e/fallback-route-params/fallback-route-params.test.ts new file mode 100644 index 000000000000..b8550649bb64 --- /dev/null +++ b/test/e2e/fallback-route-params/fallback-route-params.test.ts @@ -0,0 +1,28 @@ +import { nextTestSetup } from 'e2e-utils' +import cheerio from 'cheerio' + +describe('Fallback Dynamic Route Params', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should have correct fallback query (skeleton)', async () => { + const html = await next.render('/first') + const $ = cheerio.load(html) + const { query } = JSON.parse($('#__NEXT_DATA__').text()) + expect(query).toEqual({}) + }) + + it('should have correct fallback query (hydration)', async () => { + const browser = await next.browser('/second') + const initialSlug = await browser.eval(() => (window as any).initialSlug) + expect(initialSlug).toBeFalsy() + + await browser.waitForElementByCss('#query') + + const hydratedQuery = JSON.parse( + await browser.elementByCss('#query').text() + ) + expect(hydratedQuery).toEqual({ slug: 'second' }) + }) +}) diff --git a/test/integration/fallback-route-params/pages/[slug].js b/test/e2e/fallback-route-params/pages/[slug].js similarity index 100% rename from test/integration/fallback-route-params/pages/[slug].js rename to test/e2e/fallback-route-params/pages/[slug].js diff --git a/test/integration/fetch-polyfill-ky-universal/api-server.js b/test/e2e/fetch-polyfill-ky-universal/api-server.js similarity index 100% rename from test/integration/fetch-polyfill-ky-universal/api-server.js rename to test/e2e/fetch-polyfill-ky-universal/api-server.js diff --git a/test/integration/fetch-polyfill-ky-universal/api/api-route.js b/test/e2e/fetch-polyfill-ky-universal/api/api-route.js similarity index 100% rename from test/integration/fetch-polyfill-ky-universal/api/api-route.js rename to test/e2e/fetch-polyfill-ky-universal/api/api-route.js diff --git a/test/e2e/fetch-polyfill-ky-universal/fetch-polyfill-ky-universal.test.ts b/test/e2e/fetch-polyfill-ky-universal/fetch-polyfill-ky-universal.test.ts new file mode 100644 index 000000000000..fbdac0308b7d --- /dev/null +++ b/test/e2e/fetch-polyfill-ky-universal/fetch-polyfill-ky-universal.test.ts @@ -0,0 +1,93 @@ +import { join } from 'path' +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +import { findPort, initNextServerScript, killApp } from 'next-test-utils' + +describe('Fetch polyfill with ky-universal', () => { + ;(isNextDev ? describe : describe.skip)('development mode', () => { + const { next } = nextTestSetup({ + files: __dirname, + skipStart: true, + dependencies: { 'ky-universal': '0.6.0', ky: '0.19.1' }, + }) + + let apiServerPort: number + let apiServer: any + + beforeAll(async () => { + const scriptPath = join(__dirname, 'api-server.js') + apiServerPort = await findPort() + apiServer = await initNextServerScript( + scriptPath, + /ready on/i, + { ...process.env, PORT: String(apiServerPort) }, + /ReferenceError: options is not defined/ + ) + + next.env.NEXT_PUBLIC_API_PORT = String(apiServerPort) + await next.start() + }) + + afterAll(async () => { + await killApp(apiServer) + }) + + it('includes polyfilled fetch when using getStaticProps (dev)', async () => { + const html = await next.render('/static') + expect(html).toMatch(/bar/) + }) + + it('includes polyfilled fetch when using getServerSideProps (dev)', async () => { + const html = await next.render('/ssr') + expect(html).toMatch(/bar/) + }) + + it('includes polyfilled fetch when using getInitialProps (dev)', async () => { + const html = await next.render('/getinitialprops') + expect(html).toMatch(/bar/) + }) + }) + ;(isNextStart ? describe : describe.skip)('production mode', () => { + const { next } = nextTestSetup({ + files: __dirname, + skipStart: true, + dependencies: { 'ky-universal': '0.6.0', ky: '0.19.1' }, + }) + + let apiServerPort: number + let apiServer: any + + beforeAll(async () => { + const scriptPath = join(__dirname, 'api-server.js') + apiServerPort = await findPort() + apiServer = await initNextServerScript( + scriptPath, + /ready on/i, + { ...process.env, PORT: String(apiServerPort) }, + /ReferenceError: options is not defined/ + ) + + next.env.NEXT_PUBLIC_API_PORT = String(apiServerPort) + await next.build() + await next.start() + }) + + afterAll(async () => { + await killApp(apiServer) + }) + + it('includes polyfilled fetch when using getStaticProps (prod)', async () => { + const html = await next.render('/static') + expect(html).toMatch(/bar/) + }) + + it('includes polyfilled fetch when using getServerSideProps (prod)', async () => { + const html = await next.render('/ssr') + expect(html).toMatch(/bar/) + }) + + it('includes polyfilled fetch when using getInitialProps (prod)', async () => { + const html = await next.render('/getinitialprops') + expect(html).toMatch(/bar/) + }) + }) +}) diff --git a/test/integration/fetch-polyfill-ky-universal/pages/getinitialprops.js b/test/e2e/fetch-polyfill-ky-universal/pages/getinitialprops.js similarity index 100% rename from test/integration/fetch-polyfill-ky-universal/pages/getinitialprops.js rename to test/e2e/fetch-polyfill-ky-universal/pages/getinitialprops.js diff --git a/test/integration/fetch-polyfill-ky-universal/pages/ssr.js b/test/e2e/fetch-polyfill-ky-universal/pages/ssr.js similarity index 100% rename from test/integration/fetch-polyfill-ky-universal/pages/ssr.js rename to test/e2e/fetch-polyfill-ky-universal/pages/ssr.js diff --git a/test/integration/fetch-polyfill-ky-universal/pages/static.js b/test/e2e/fetch-polyfill-ky-universal/pages/static.js similarity index 100% rename from test/integration/fetch-polyfill-ky-universal/pages/static.js rename to test/e2e/fetch-polyfill-ky-universal/pages/static.js diff --git a/test/e2e/fetch-polyfill/fetch-polyfill.test.ts b/test/e2e/fetch-polyfill/fetch-polyfill.test.ts new file mode 100644 index 000000000000..af25396e0047 --- /dev/null +++ b/test/e2e/fetch-polyfill/fetch-polyfill.test.ts @@ -0,0 +1,85 @@ +import http from 'http' +import cheerio from 'cheerio' +import { nextTestSetup, isNextDev } from 'e2e-utils' +import { findPort } from 'next-test-utils' + +describe('Fetch polyfill', () => { + let apiServerPort: number + let apiServer: http.Server + + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + dependencies: { + react: '19.3.0-canary-fef12a01-20260413', + 'react-dom': '19.3.0-canary-fef12a01-20260413', + }, + // Vercel deployment fails to build/deploy this fixture in CI; skip in deploy mode. + skipDeployment: true, + }) + if (skipped) return + + beforeAll(async () => { + apiServerPort = await findPort() + + apiServer = http.createServer((req, res) => { + if (req.url === '/usernames') { + return res.end(JSON.stringify({ usernames: ['a', 'b'] })) + } + if (req.url === '/usernames/a') { + return res.end(JSON.stringify({ from: 'a' })) + } + if (req.url === '/usernames/b') { + return res.end(JSON.stringify({ from: 'b' })) + } + res.end(JSON.stringify({ foo: 'bar' })) + }) + + await new Promise<void>((resolve, reject) => { + apiServer.listen(apiServerPort, () => resolve()) + apiServer.once('error', reject) + }) + + next.env.NEXT_PUBLIC_API_PORT = String(apiServerPort) + + if (!isNextDev) { + await next.build() + } + await next.start() + }) + + afterAll(() => { + apiServer?.close() + }) + + it('includes polyfilled fetch when using getStaticProps', async () => { + const html = await next.render('/static') + expect(html).toMatch(/bar/) + }) + + it('includes polyfilled fetch when using getServerSideProps', async () => { + const html = await next.render('/ssr') + expect(html).toMatch(/bar/) + }) + + it('includes polyfilled fetch when using getInitialProps', async () => { + const html = await next.render('/getinitialprops') + expect(html).toMatch(/bar/) + }) + + it('includes polyfilled fetch when using API routes', async () => { + const res = await next.fetch('/api/api-route') + const json = await res.json() + expect(json.foo).toBe('bar') + }) + + it('includes polyfilled fetch when using getStaticPaths', async () => { + const htmlA = await next.render('/user/a') + const $a = cheerio.load(htmlA) + expect($a('#username').text()).toBe('a') + + const htmlB = await next.render('/user/b') + const $b = cheerio.load(htmlB) + expect($b('#username').text()).toBe('b') + }) +}) diff --git a/test/integration/fetch-polyfill/pages/api/api-route.js b/test/e2e/fetch-polyfill/pages/api/api-route.js similarity index 100% rename from test/integration/fetch-polyfill/pages/api/api-route.js rename to test/e2e/fetch-polyfill/pages/api/api-route.js diff --git a/test/integration/fetch-polyfill/pages/getinitialprops.js b/test/e2e/fetch-polyfill/pages/getinitialprops.js similarity index 100% rename from test/integration/fetch-polyfill/pages/getinitialprops.js rename to test/e2e/fetch-polyfill/pages/getinitialprops.js diff --git a/test/integration/fetch-polyfill/pages/ssr.js b/test/e2e/fetch-polyfill/pages/ssr.js similarity index 100% rename from test/integration/fetch-polyfill/pages/ssr.js rename to test/e2e/fetch-polyfill/pages/ssr.js diff --git a/test/integration/fetch-polyfill/pages/static.js b/test/e2e/fetch-polyfill/pages/static.js similarity index 100% rename from test/integration/fetch-polyfill/pages/static.js rename to test/e2e/fetch-polyfill/pages/static.js diff --git a/test/integration/fetch-polyfill/pages/user/[username].js b/test/e2e/fetch-polyfill/pages/user/[username].js similarity index 100% rename from test/integration/fetch-polyfill/pages/user/[username].js rename to test/e2e/fetch-polyfill/pages/user/[username].js diff --git a/test/integration/file-serving/test/index.test.ts b/test/e2e/file-serving/file-serving.test.ts similarity index 98% rename from test/integration/file-serving/test/index.test.ts rename to test/e2e/file-serving/file-serving.test.ts index 9126f9373ffa..63bb7e8deec5 100644 --- a/test/integration/file-serving/test/index.test.ts +++ b/test/e2e/file-serving/file-serving.test.ts @@ -1,80 +1,129 @@ -/* eslint-env jest */ - /* eslint-disable jest/no-identical-title */ import fs from 'fs-extra' import { join } from 'path' -import { - killApp, - findPort, - nextBuild, - nextStart, - fetchViaHTTP, - launchApp, -} from 'next-test-utils' - -const appDir = join(__dirname, '../') -let appPort -let app - -const expectStatus = async (path) => { - const containRegex = /(This page could not be found|Bad Request)/ - // test base mount point `public/` - const checkRes = async (res) => { - if (res.status === 308) { - const redirectDest = res.headers.get('location') - const parsedUrl = new URL(redirectDest) - expect(parsedUrl.hostname).toBeOneOf(['localhost', '127.0.0.1']) +import { nextTestSetup } from 'e2e-utils' +import { fetchViaHTTP } from 'next-test-utils' + +describe('file-serving', () => { + const { next, isNextDeploy, skipped } = nextTestSetup({ + files: __dirname, + // Vercel's edge rejects malformed URLs (mixed-encoding traversal, + // backslash, double-encoded, etc.) before they reach the runtime, and + // `safeFetch` for those paths uses `localhost:0` which doesn't apply in + // deploy mode. The traversal protection we want to test here is local to + // Next.js's server. + skipDeployment: true, + }) + if (skipped) return + + // Helper to detect malformed URLs that can't be parsed by the URL constructor + const isMalformedUrl = (path) => { + // These are intentionally malformed URLs used for security testing + // They contain backslashes and other characters that make them invalid URLs + + // Check for specific failing patterns + if (path.startsWith('////')) return true // ////%2e%2e%2f... + if (path.indexOf('/\\\\\\%2e%2e%5c') >= 0) return true // /\\\%2e%2e%5c... + if (path.indexOf('/\\..%2f') >= 0) return true // /\..%2f... + + // General patterns + return ( + path.includes('\\') || // literal backslashes + path.includes('%5c') || // URL-encoded backslashes (\) + path.includes('%5C') || // URL-encoded backslashes (uppercase) + path.includes('%2e%2e') || // URL-encoded dots (..) + path.includes('%2E%2E') || // URL-encoded dots (uppercase) + path.includes('%252f') || // double-encoded forward slash + path.includes('%255c') // double-encoded backslash + ) + } + + // Helper to fetch using appropriate method based on URL validity + const safeFetch = async (path, opts) => { + if (isMalformedUrl(path)) { + // Use fetchViaHTTP with numeric port to bypass strict URL validation + // (passing a number avoids getFullUrl's string-branch URL parsing) + return await fetchViaHTTP(Number(next.appPort), path, undefined, opts) } else { - try { - expect(res.status === 400 || res.status === 404).toBe(true) - } catch (err) { - require('console').error({ path, status: res.status }) - throw err - } - expect(await res.text()).toMatch(containRegex) + // Use normal next.fetch for valid URLs + return await next.fetch(path, opts) } } - const res = await fetchViaHTTP(appPort, path, undefined, { - redirect: 'manual', - }) - await checkRes(res) - // test `/_next` mount point - const res2 = await fetchViaHTTP(appPort, `/_next/${path}`, undefined, { - redirect: 'manual', - }) - await checkRes(res2) + const expectStatus = async (path) => { + const containRegex = + /(This page could not be found|Bad Request|bad request|BAD_REQUEST)/ + // test base mount point `public/` + const checkRes = async (res) => { + if (res.status === 308) { + const redirectDest = res.headers.get('location') + const parsedUrl = new URL(redirectDest) + expect(parsedUrl.hostname).toBeOneOf(['localhost', '127.0.0.1']) + } else { + try { + expect(res.status === 400 || res.status === 404).toBe(true) + } catch (err) { + require('console').error({ path, status: res.status }) + throw err + } + expect(await res.text()).toMatch(containRegex) + } + } + + const res = await safeFetch(path, { + redirect: 'manual', + }) + await checkRes(res) + + // test `/_next` mount point + const res2 = await safeFetch(`/_next/${path}`, { + redirect: 'manual', + }) + await checkRes(res2) - // test `/static` mount point - const res3 = await fetchViaHTTP(appPort, `/static/${path}`, undefined, { - redirect: 'manual', + // test `/static` mount point + const res3 = await safeFetch(`/static/${path}`, { + redirect: 'manual', + }) + await checkRes(res3) + } + + beforeAll(async () => { + await fs.copy( + join(next.testDir, 'test-file.txt'), + join(next.testDir, '.next', 'test-file.txt') + ) }) - await checkRes(res3) -} -const runTests = () => { it('should serve file with space correctly from public/', async () => { - const res = await fetchViaHTTP(appPort, '/hello world.txt') + const res = await next.fetch('/hello world.txt') expect(res.status).toBe(200) expect(await res.text()).toBe('hi') }) - it('should serve file with space correctly static/', async () => { - const res = await fetchViaHTTP(appPort, '/static/hello world.txt') - expect(res.status).toBe(200) - expect(await res.text()).toBe('hi') - }) + // Vercel's deploy infrastructure only serves `public/` as static assets, not + // a top-level `static/` directory, so this case is local-only. + ;(isNextDeploy ? it.skip : it)( + 'should serve file with space correctly static/', + async () => { + const res = await next.fetch('/static/hello world.txt') + // eslint-disable-next-line jest/no-standalone-expect + expect(res.status).toBe(200) + // eslint-disable-next-line jest/no-standalone-expect + expect(await res.text()).toBe('hi') + } + ) it('should serve avif image with correct content-type', async () => { // vercel-icon-dark.avif is downloaded from https://vercel.com/design and transformed to avif on avif.io - const res = await fetchViaHTTP(appPort, '/vercel-icon-dark.avif') + const res = await next.fetch('/vercel-icon-dark.avif') expect(res.status).toBe(200) expect(res.headers.get('content-type')).toBe('image/avif') }) it('should serve correct error code', async () => { // vercel-icon-dark.avif is downloaded from https://vercel.com/design and transformed to avif on avif.io - const res = await fetchViaHTTP(appPort, '/vercel-icon-dark.avif', '', { + const res = await next.fetch('/vercel-icon-dark.avif', { headers: { Range: 'bytes=1000000000-', }, @@ -4458,56 +4507,4 @@ const runTests = () => { '/\\..%2f\\..%2f\\..%2f\\..%2f\\..%2f\\..%2f\\..%2f\\..%2ftest-file.txt' ) }) -} - -const copyTestFileToDist = () => - fs.copy(join(appDir, 'test-file.txt'), join(appDir, '.next', 'test-file.txt')) - -describe('File Serving', () => { - ;(process.env.TURBOPACK_BUILD ? describe.skip : describe)( - 'development mode', - () => { - beforeAll(async () => { - appPort = await findPort() - app = await launchApp(appDir, appPort, { - // don't log stdout and stderr as we're going to generate - // a lot of output from resolve mismatches - stdout: false, - stderr: false, - }) - await copyTestFileToDist() - }) - afterAll(async () => { - await killApp(app) - }) - - runTests() - } - ) - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( - 'production mode', - () => { - beforeAll(async () => { - const { code } = await nextBuild(appDir) - - if (code !== 0) { - throw new Error(`Failed to build got code: ${code}`) - } - await copyTestFileToDist() - - appPort = await findPort() - app = await nextStart(appDir, appPort, { - // don't log stdout and stderr as we're going to generate - // a lot of output from resolve mismatches - stdout: false, - stderr: false, - }) - }) - afterAll(async () => { - await killApp(app) - }) - - runTests() - } - ) }) diff --git a/test/integration/errors-on-output-to-public/pages/index.js b/test/e2e/file-serving/pages/index.js similarity index 100% rename from test/integration/errors-on-output-to-public/pages/index.js rename to test/e2e/file-serving/pages/index.js diff --git a/test/integration/file-serving/public/hello world.txt b/test/e2e/file-serving/public/hello world.txt similarity index 100% rename from test/integration/file-serving/public/hello world.txt rename to test/e2e/file-serving/public/hello world.txt diff --git a/test/integration/file-serving/public/vercel-icon-dark.avif b/test/e2e/file-serving/public/vercel-icon-dark.avif similarity index 100% rename from test/integration/file-serving/public/vercel-icon-dark.avif rename to test/e2e/file-serving/public/vercel-icon-dark.avif diff --git a/test/integration/file-serving/static/hello world.txt b/test/e2e/file-serving/static/hello world.txt similarity index 100% rename from test/integration/file-serving/static/hello world.txt rename to test/e2e/file-serving/static/hello world.txt diff --git a/test/integration/file-serving/test-file.txt b/test/e2e/file-serving/test-file.txt similarity index 100% rename from test/integration/file-serving/test-file.txt rename to test/e2e/file-serving/test-file.txt diff --git a/test/e2e/filesystempublicroutes/filesystempublicroutes.test.ts b/test/e2e/filesystempublicroutes/filesystempublicroutes.test.ts new file mode 100644 index 000000000000..3da734ad6216 --- /dev/null +++ b/test/e2e/filesystempublicroutes/filesystempublicroutes.test.ts @@ -0,0 +1,42 @@ +import { nextTestSetup, isNextDev } from 'e2e-utils' + +// `exportPathMap` + `useFileSystemPublicRoutes: false` is a dev-only routing +// behavior; the custom server runs `next({ dev: true })` so production mode is +// not exercised by the original integration test. +;(isNextDev ? describe : describe.skip)('FileSystemPublicRoutes', () => { + const { next } = nextTestSetup({ + files: __dirname, + startCommand: 'node server.js', + serverReadyPattern: /- Local:/, + dependencies: { + 'get-port': '5.1.1', + }, + }) + + it('should not route to the index page', async () => { + const res = await next.fetch('/') + expect(res.status).toBe(404) + const body = await res.text() + expect(body).toMatch(/404/) + }) + + it('should route to exportPathMap defined routes in development', async () => { + const res = await next.fetch('/exportpathmap-route') + expect(res.status).toBe(200) + const body = await res.text() + expect(body).toMatch(/exportpathmap was here/) + }) + + it('should serve JavaScript files correctly', async () => { + const browser = await next.browser('/exportpathmap-route') + const text = await browser.waitForElementByCss('#page-was-loaded').text() + expect(text).toBe('Hello World') + }) + + it('should route to public folder files', async () => { + const res = await next.fetch('/hello.txt') + expect(res.status).toBe(200) + const body = await res.text() + expect(body).toMatch(/hello/) + }) +}) diff --git a/test/integration/filesystempublicroutes/next.config.js b/test/e2e/filesystempublicroutes/next.config.js similarity index 100% rename from test/integration/filesystempublicroutes/next.config.js rename to test/e2e/filesystempublicroutes/next.config.js diff --git a/test/integration/filesystempublicroutes/pages/exportpathmap-route.js b/test/e2e/filesystempublicroutes/pages/exportpathmap-route.js similarity index 100% rename from test/integration/filesystempublicroutes/pages/exportpathmap-route.js rename to test/e2e/filesystempublicroutes/pages/exportpathmap-route.js diff --git a/test/integration/filesystempublicroutes/pages/index.js b/test/e2e/filesystempublicroutes/pages/index.js similarity index 100% rename from test/integration/filesystempublicroutes/pages/index.js rename to test/e2e/filesystempublicroutes/pages/index.js diff --git a/test/integration/filesystempublicroutes/public/hello.txt b/test/e2e/filesystempublicroutes/public/hello.txt similarity index 100% rename from test/integration/filesystempublicroutes/public/hello.txt rename to test/e2e/filesystempublicroutes/public/hello.txt diff --git a/test/integration/filesystempublicroutes/server.js b/test/e2e/filesystempublicroutes/server.js similarity index 64% rename from test/integration/filesystempublicroutes/server.js rename to test/e2e/filesystempublicroutes/server.js index 00131ab832d8..595eec436bcf 100644 --- a/test/integration/filesystempublicroutes/server.js +++ b/test/e2e/filesystempublicroutes/server.js @@ -1,33 +1,35 @@ const http = require('http') const next = require('next') +const getPort = require('get-port') const dev = process.env.NODE_ENV !== 'production' const dir = __dirname -const port = process.env.PORT || 3000 const app = next({ dev, dir }) const handleNextRequests = app.getRequestHandler() -app.prepare().then(() => { +async function main() { + await app.prepare() + const port = await getPort() + const server = new http.Server((req, res) => { if (/setAssetPrefix/.test(req.url)) { app.setAssetPrefix(`http://127.0.0.1:${port}`) } else if (/setEmptyAssetPrefix/.test(req.url)) { app.setAssetPrefix('') } else { - // This is to support multi-zones support in localhost - // and may be in staging deployments app.setAssetPrefix('') } handleNextRequests(req, res) }) - server.listen(port, (err) => { - if (err) { - throw err - } - - console.log(`> Ready on http://localhost:${port}`) + server.listen(port, () => { + console.log(`- Local: http://localhost:${port}`) }) +} + +main().catch((err) => { + console.error(err) + process.exit(1) }) diff --git a/test/e2e/getinitialprops/getinitialprops.test.ts b/test/e2e/getinitialprops/getinitialprops.test.ts new file mode 100644 index 000000000000..661f64a3ea9a --- /dev/null +++ b/test/e2e/getinitialprops/getinitialprops.test.ts @@ -0,0 +1,29 @@ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +;((isNextDev && process.env.TURBOPACK_BUILD) || + (isNextStart && process.env.TURBOPACK_DEV) + ? describe.skip + : describe)('getInitialProps', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should have gip in __NEXT_DATA__', async () => { + const $ = await next.render$('/') + expect(JSON.parse($('#__NEXT_DATA__').text()).gip).toBe(true) + }) + + it('should not have gip in __NEXT_DATA__ for non-GIP page', async () => { + const $ = await next.render$('/normal') + expect('gip' in JSON.parse($('#__NEXT_DATA__').text())).toBe(false) + }) + + it('should have correct router.asPath for direct visit dynamic page', async () => { + const $ = await next.render$('/blog/1') + expect($('#as-path').text()).toBe('/blog/1') + }) + + it('should have correct router.asPath for direct visit dynamic page rewrite direct', async () => { + const $ = await next.render$('/blog/post/1') + expect($('#as-path').text()).toBe('/blog/post/1') + }) +}) diff --git a/test/integration/getinitialprops/next.config.js b/test/e2e/getinitialprops/next.config.js similarity index 100% rename from test/integration/getinitialprops/next.config.js rename to test/e2e/getinitialprops/next.config.js diff --git a/test/integration/getinitialprops/pages/blog/[post].js b/test/e2e/getinitialprops/pages/blog/[post].js similarity index 100% rename from test/integration/getinitialprops/pages/blog/[post].js rename to test/e2e/getinitialprops/pages/blog/[post].js diff --git a/test/integration/getinitialprops/pages/index.js b/test/e2e/getinitialprops/pages/index.js similarity index 100% rename from test/integration/getinitialprops/pages/index.js rename to test/e2e/getinitialprops/pages/index.js diff --git a/test/integration/getinitialprops/pages/normal.js b/test/e2e/getinitialprops/pages/normal.js similarity index 100% rename from test/integration/getinitialprops/pages/normal.js rename to test/e2e/getinitialprops/pages/normal.js diff --git a/test/e2e/getserversideprops-preview/getserversideprops-preview.test.ts b/test/e2e/getserversideprops-preview/getserversideprops-preview.test.ts new file mode 100644 index 000000000000..d980b12b4a81 --- /dev/null +++ b/test/e2e/getserversideprops-preview/getserversideprops-preview.test.ts @@ -0,0 +1,251 @@ +import cheerio from 'cheerio' +import cookie from 'cookie' +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +import qs from 'querystring' + +function getData(html: string) { + const $ = cheerio.load(html) + const nextData = $('#__NEXT_DATA__') + const preEl = $('#props-pre') + const routerData = JSON.parse($('#router').text()) + return { + nextData: JSON.parse(nextData.html()!), + pre: preEl.text(), + routerData, + } +} + +describe('ServerSide Props Preview Mode', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + // Assertions don't apply to deploy mode (output differs vs. local Next.js server). + skipDeployment: true, + }) + if (skipped) return + + if (isNextStart) { + it('should compile successfully', async () => { + expect(next.cliOutput).toMatch(/Compiled successfully/) + expect(next.cliOutput).not.toContain('Build error occurred') + }) + + it('should start production application', async () => { + const res = await next.fetch('/') + expect(res.status).toBe(200) + }) + } + + it('should return page on first request', async () => { + const html = await next.render('/') + const { nextData, pre, routerData } = getData(html) + expect(nextData).toMatchObject({ isFallback: false }) + expect(nextData.isPreview).toBeUndefined() + expect(pre).toBe('false and null') + expect(routerData.isPreview).toBe(false) + }) + + it('should return page on second request', async () => { + const html = await next.render('/') + const { nextData, pre, routerData } = getData(html) + expect(nextData).toMatchObject({ isFallback: false }) + expect(nextData.isPreview).toBeUndefined() + expect(pre).toBe('false and null') + expect(routerData.isPreview).toBe(false) + }) + + let previewCookieString: string + it('should enable preview mode', async () => { + const res = await next.fetch( + '/api/preview?' + qs.stringify({ lets: 'goooo' }) + ) + expect(res.status).toBe(200) + + const originalCookies = res.headers.get('set-cookie')!.split(',') + const cookies = originalCookies.map((rawCookie) => cookie.parse(rawCookie)) + + if (isNextStart) { + expect(originalCookies.every((c) => c.includes('; Secure;'))).toBe(true) + } + + expect(cookies.length).toBe(2) + if (isNextStart) { + expect(cookies[0]).toMatchObject({ Path: '/', SameSite: 'None' }) + } + expect(cookies[0]).toHaveProperty('__prerender_bypass') + expect(cookies[0]).not.toHaveProperty('Max-Age') + if (isNextStart) { + expect(cookies[1]).toMatchObject({ Path: '/', SameSite: 'None' }) + } + expect(cookies[1]).toHaveProperty('__next_preview_data') + expect(cookies[1]).not.toHaveProperty('Max-Age') + + previewCookieString = + cookie.serialize('__prerender_bypass', cookies[0].__prerender_bypass) + + '; ' + + cookie.serialize('__next_preview_data', cookies[1].__next_preview_data) + }) + + it('should not return fallback page on preview request', async () => { + const res = await next.fetch('/', { + headers: { Cookie: previewCookieString }, + }) + const html = await res.text() + + const { nextData, pre, routerData } = getData(html) + if (isNextStart) { + expect(res.headers.get('cache-control')).toBe( + 'private, no-cache, no-store, max-age=0, must-revalidate' + ) + } + expect(nextData).toMatchObject({ isFallback: false, isPreview: true }) + expect(pre).toBe('true and {"lets":"goooo"}') + expect(routerData.isPreview).toBe(true) + }) + + it('should return correct caching headers for data preview request', async () => { + const res = await next.fetch( + `/_next/data/${encodeURI(next.buildId)}/index.json`, + { headers: { Cookie: previewCookieString } } + ) + const json = await res.json() + + if (isNextStart) { + expect(res.headers.get('cache-control')).toBe( + 'private, no-cache, no-store, max-age=0, must-revalidate' + ) + } + expect(json).toMatchObject({ + pageProps: { + preview: true, + previewData: { lets: 'goooo' }, + }, + }) + }) + + it('should return cookies to be expired on reset request', async () => { + const res = await next.fetch('/api/reset', { + headers: { Cookie: previewCookieString }, + }) + expect(res.status).toBe(200) + + const cookies = res.headers + .get('set-cookie')! + .replace(/(=(?!Lax)\w{3}),/g, '$1') + .split(',') + .map((rawCookie) => cookie.parse(rawCookie)) + + expect(cookies.length).toBe(2) + if (isNextStart) { + expect(cookies[0]).toMatchObject({ + Path: '/', + SameSite: 'None', + Expires: 'Thu 01 Jan 1970 00:00:00 GMT', + }) + } + expect(cookies[0]).toHaveProperty('__prerender_bypass') + expect(cookies[0]).not.toHaveProperty('Max-Age') + if (isNextStart) { + expect(cookies[1]).toMatchObject({ + Path: '/', + SameSite: 'None', + Expires: 'Thu 01 Jan 1970 00:00:00 GMT', + }) + } + expect(cookies[1]).toHaveProperty('__next_preview_data') + expect(cookies[1]).not.toHaveProperty('Max-Age') + }) + + it('should throw error when setting too large of preview data', async () => { + const res = await next.fetch('/api/preview?tooBig=true') + expect(res.status).toBe(500) + expect(await res.text()).toBe('too big') + }) + + it('should fetch preview data on SSR via browser', async () => { + const browser = await next.browser( + '/api/preview?' + qs.stringify({ client: 'mode' }) + ) + await browser.get(next.url + '/') + await browser.waitForElementByCss('#props-pre') + expect(await browser.elementById('props-pre').text()).toBe( + 'true and {"client":"mode"}' + ) + }) + + it('should fetch preview data on CST via browser', async () => { + const browser = await next.browser( + '/api/preview?' + qs.stringify({ client: 'mode' }) + ) + await browser.get(next.url + '/to-index') + await browser.waitForElementByCss('#to-index') + await browser.eval('window.itdidnotrefresh = "hello"') + await browser.elementById('to-index').click() + await browser.waitForElementByCss('#props-pre') + expect(await browser.eval('window.itdidnotrefresh')).toBe('hello') + expect(await browser.elementById('props-pre').text()).toBe( + 'true and {"client":"mode"}' + ) + }) + + it('should fetch prerendered data after reset', async () => { + const browser = await next.browser( + '/api/preview?' + qs.stringify({ client: 'mode' }) + ) + await browser.get(next.url + '/api/reset') + await browser.get(next.url + '/') + await browser.waitForElementByCss('#props-pre') + expect(await browser.elementById('props-pre').text()).toBe('false and null') + }) + + if (isNextDev) { + it('should start development application', async () => { + const html = await next.render('/') + expect(html).toBeTruthy() + }) + + it('should enable preview mode in dev', async () => { + const res = await next.fetch( + '/api/preview?' + qs.stringify({ lets: 'goooo' }) + ) + expect(res.status).toBe(200) + + const cookies = res.headers + .get('set-cookie')! + .split(',') + .map((rawCookie) => cookie.parse(rawCookie)) + + expect(cookies.length).toBe(2) + }) + + it('should return cookies to be expired after dev server reboot', async () => { + const res = await next.fetch('/', { + headers: { + Cookie: + '__prerender_bypass=stale-value; __next_preview_data=stale-data', + }, + }) + expect(res.status).toBe(200) + + const body = await res.text() + expect(body).not.toContain('"err"') + expect(body).not.toContain('TypeError') + expect(body).not.toContain('previewModeId') + + const cookies = res.headers + .get('set-cookie')! + .replace(/(=(?!Lax)\w{3}),/g, '$1') + .split(',') + .map((rawCookie) => cookie.parse(rawCookie)) + + expect(cookies.length).toBe(2) + }) + + it('should start the client-side browser', async () => { + const browser = await next.browser( + '/api/preview?' + qs.stringify({ client: 'mode' }) + ) + const url = await browser.url() + expect(url).toContain('/api/preview') + }) + } +}) diff --git a/test/integration/getserversideprops-preview/pages/api/preview.js b/test/e2e/getserversideprops-preview/pages/api/preview.js similarity index 100% rename from test/integration/getserversideprops-preview/pages/api/preview.js rename to test/e2e/getserversideprops-preview/pages/api/preview.js diff --git a/test/integration/getserversideprops-preview/pages/api/reset.js b/test/e2e/getserversideprops-preview/pages/api/reset.js similarity index 100% rename from test/integration/getserversideprops-preview/pages/api/reset.js rename to test/e2e/getserversideprops-preview/pages/api/reset.js diff --git a/test/integration/getserversideprops-preview/pages/index.js b/test/e2e/getserversideprops-preview/pages/index.js similarity index 100% rename from test/integration/getserversideprops-preview/pages/index.js rename to test/e2e/getserversideprops-preview/pages/index.js diff --git a/test/integration/getserversideprops-preview/pages/to-index.js b/test/e2e/getserversideprops-preview/pages/to-index.js similarity index 100% rename from test/integration/getserversideprops-preview/pages/to-index.js rename to test/e2e/getserversideprops-preview/pages/to-index.js diff --git a/test/e2e/gip-identifier/gip-identifier.test.ts b/test/e2e/gip-identifier/gip-identifier.test.ts new file mode 100644 index 000000000000..2abc71ec9081 --- /dev/null +++ b/test/e2e/gip-identifier/gip-identifier.test.ts @@ -0,0 +1,71 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' +import cheerio from 'cheerio' + +describe('gip identifiers', () => { + const { next, isNextDev, skipped } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + }) + if (skipped) return + + const getNextData = async () => { + const html = await next.render('/') + const $ = cheerio.load(html) + return JSON.parse($('#__NEXT_DATA__').text()) + } + + it('should not have gip or appGip in NEXT_DATA for page without getInitialProps', async () => { + const data = await getNextData() + expect(data.gip).toBe(undefined) + expect(data.appGip).toBe(undefined) + }) + + if (isNextDev) { + it('should have gip in NEXT_DATA for page with getInitialProps', async () => { + await next.patchFile( + 'pages/index.js', + ` + const Page = () => 'hi' + Page.getInitialProps = () => ({ hello: 'world' }) + export default Page + ` + ) + await retry(async () => { + const data = await getNextData() + expect(data.gip).toBe(true) + }) + }) + + it('should have gip and appGip in NEXT_DATA for page with getInitialProps and _app with getInitialProps', async () => { + await next.patchFile( + 'pages/_app.js', + ` + const App = ({ Component, pageProps }) => <Component {...pageProps} /> + App.getInitialProps = async (ctx) => { + let pageProps = {} + if (ctx.Component.getInitialProps) { + pageProps = await ctx.Component.getInitialProps(ctx.ctx) + } + return { pageProps } + } + export default App + ` + ) + await retry(async () => { + const data = await getNextData() + expect(data.gip).toBe(true) + expect(data.appGip).toBe(true) + }) + }) + + it('should only have appGip in NEXT_DATA for page without getInitialProps and _app with getInitialProps', async () => { + await next.patchFile('pages/index.js', `export default () => 'hi'\n`) + await retry(async () => { + const data = await getNextData() + expect(data.gip).toBe(undefined) + expect(data.appGip).toBe(true) + }) + }) + } +}) diff --git a/test/integration/errors-on-output-to-static/pages/index.js b/test/e2e/gip-identifier/pages/index.js similarity index 100% rename from test/integration/errors-on-output-to-static/pages/index.js rename to test/e2e/gip-identifier/pages/index.js diff --git a/test/e2e/gssp-pageProps-merge/gssp-pageProps-merge.test.ts b/test/e2e/gssp-pageProps-merge/gssp-pageProps-merge.test.ts new file mode 100644 index 000000000000..583d0410d753 --- /dev/null +++ b/test/e2e/gssp-pageProps-merge/gssp-pageProps-merge.test.ts @@ -0,0 +1,19 @@ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +;((isNextDev && process.env.TURBOPACK_BUILD) || + (isNextStart && process.env.TURBOPACK_DEV) + ? describe.skip + : describe)('pageProps GSSP conflict', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should merge _app pageProps and getServerSideProps props', async () => { + const $ = await next.render$('/gssp') + expect(JSON.parse($('p').text())).toEqual({ hi: 'hi', hello: 'world' }) + }) + + it('should merge _app pageProps and getStaticProps props', async () => { + const $ = await next.render$('/gsp') + expect(JSON.parse($('p').text())).toEqual({ hi: 'hi', hello: 'world' }) + }) +}) diff --git a/test/integration/gssp-pageProps-merge/pages/_app.js b/test/e2e/gssp-pageProps-merge/pages/_app.js similarity index 100% rename from test/integration/gssp-pageProps-merge/pages/_app.js rename to test/e2e/gssp-pageProps-merge/pages/_app.js diff --git a/test/integration/gssp-pageProps-merge/pages/gsp.js b/test/e2e/gssp-pageProps-merge/pages/gsp.js similarity index 100% rename from test/integration/gssp-pageProps-merge/pages/gsp.js rename to test/e2e/gssp-pageProps-merge/pages/gsp.js diff --git a/test/integration/gssp-pageProps-merge/pages/gssp.js b/test/e2e/gssp-pageProps-merge/pages/gssp.js similarity index 100% rename from test/integration/gssp-pageProps-merge/pages/gssp.js rename to test/e2e/gssp-pageProps-merge/pages/gssp.js diff --git a/test/integration/gssp-redirect-base-path/test/index.test.ts b/test/e2e/gssp-redirect-base-path/gssp-redirect-base-path.test.ts similarity index 62% rename from test/integration/gssp-redirect-base-path/test/index.test.ts rename to test/e2e/gssp-redirect-base-path/gssp-redirect-base-path.test.ts index cd7124325e5d..675e7c54396f 100644 --- a/test/integration/gssp-redirect-base-path/test/index.test.ts +++ b/test/e2e/gssp-redirect-base-path/gssp-redirect-base-path.test.ts @@ -1,136 +1,101 @@ -/* eslint-env jest */ -import fs from 'fs-extra' -import webdriver from 'next-webdriver' -import { join } from 'path' -import { - findPort, - launchApp, - killApp, - nextBuild, - nextStart, - fetchViaHTTP, - check, -} from 'next-test-utils' - -const appDir = join(__dirname, '..') - -let app -let appPort +import { nextTestSetup, isNextStart } from 'e2e-utils' +import { retry } from 'next-test-utils' const basePath = '/docs' -const runTests = (isDev: boolean) => { +describe('GS(S)P Redirect with basePath', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + dependencies: { + react: '19.3.0-canary-da9325b5-20260417', + 'react-dom': '19.3.0-canary-da9325b5-20260417', + }, + skipDeployment: true, + }) + if (skipped) return + it('should apply temporary redirect when visited directly for GSSP page', async () => { - const res = await fetchViaHTTP( - appPort, - `${basePath}/gssp-blog/redirect-1`, - undefined, - { - redirect: 'manual', - } - ) + const res = await next.fetch(`${basePath}/gssp-blog/redirect-1`, { + redirect: 'manual', + }) expect(res.status).toBe(307) - const { pathname } = new URL(res.headers.get('location')) - + const { pathname } = new URL(res.headers.get('location')!) expect(pathname).toBe(`${basePath}/404`) }) it('should apply temporary redirect when visited directly basePath false for GSSP page', async () => { - const res = await fetchViaHTTP( - appPort, + const res = await next.fetch( `${basePath}/gssp-blog/redirect-1-no-basepath-`, - undefined, - { - redirect: 'manual', - } + { redirect: 'manual' } ) expect(res.status).toBe(307) const text = await res.text() expect(text).toEqual(`/404`) - const parsedUrl = new URL(res.headers.get('location')) + const parsedUrl = new URL(res.headers.get('location')!) expect(parsedUrl.pathname).toBe(`/404`) - const browser = await webdriver(appPort, `${basePath}`) + const browser = await next.browser(`${basePath}`) await browser.eval(`next.router.push('/gssp-blog/redirect-1-no-basepath-')`) - await check( - () => browser.eval('document.documentElement.innerHTML'), - /oops not found/ - ) + await retry(async () => { + const html = await browser.eval('document.documentElement.innerHTML') + expect(html).toMatch(/oops not found/) + }) const parsedUrl2 = new URL(await browser.eval('window.location.href')) expect(parsedUrl2.pathname).toBe('/404') }) it('should apply permanent redirect when visited directly for GSSP page', async () => { - const res = await fetchViaHTTP( - appPort, - `${basePath}/gssp-blog/redirect-permanent`, - undefined, - { - redirect: 'manual', - } - ) + const res = await next.fetch(`${basePath}/gssp-blog/redirect-permanent`, { + redirect: 'manual', + }) expect(res.status).toBe(308) const text = await res.text() expect(text).toEqual(`${basePath}/404`) - const { pathname } = new URL(res.headers.get('location')) - + const { pathname } = new URL(res.headers.get('location')!) expect(pathname).toBe(`${basePath}/404`) expect(res.headers.get('refresh')).toContain(`url=${basePath}/404`) }) it('should apply statusCode 301 redirect when visited directly for GSSP page', async () => { - const res = await fetchViaHTTP( - appPort, + const res = await next.fetch( `${basePath}/gssp-blog/redirect-statusCode-301`, - undefined, - { - redirect: 'manual', - } + { redirect: 'manual' } ) expect(res.status).toBe(301) const text = await res.text() expect(text).toEqual(`${basePath}/404`) - const { pathname } = new URL(res.headers.get('location')) - + const { pathname } = new URL(res.headers.get('location')!) expect(pathname).toBe(`${basePath}/404`) expect(res.headers.get('refresh')).toBe(null) }) it('should apply statusCode 303 redirect when visited directly for GSSP page', async () => { - const res = await fetchViaHTTP( - appPort, + const res = await next.fetch( `${basePath}/gssp-blog/redirect-statusCode-303`, - undefined, - { - redirect: 'manual', - } + { redirect: 'manual' } ) expect(res.status).toBe(303) const text = await res.text() expect(text).toEqual(`${basePath}/404`) - const { pathname } = new URL(res.headers.get('location')) - + const { pathname } = new URL(res.headers.get('location')!) expect(pathname).toBe(`${basePath}/404`) expect(res.headers.get('refresh')).toBe(null) }) it('should apply redirect when fallback GSP page is visited directly (internal dynamic)', async () => { - const browser = await webdriver( - appPort, + const browser = await next.browser( `${basePath}/gsp-blog/redirect-dest-_gsp-blog_first`, - { - retryWaitHydration: true, - } + { retryWaitHydration: true } ) await browser.waitForElementByCss('#gsp') @@ -146,14 +111,11 @@ const runTests = (isDev: boolean) => { expect(pathname).toBe(`${basePath}/gsp-blog/redirect-dest-_gsp-blog_first`) }) - if (!isDev) { + if (isNextStart) { it('should apply redirect when fallback GSP page is visited directly (internal dynamic) 2nd visit', async () => { - const browser = await webdriver( - appPort, + const browser = await next.browser( `${basePath}/gsp-blog/redirect-dest-_gsp-blog_first`, - { - retryWaitHydration: true, - } + { retryWaitHydration: true } ) await browser.waitForElementByCss('#gsp') @@ -166,20 +128,14 @@ const runTests = (isDev: boolean) => { }) const initialHref = await browser.eval(() => (window as any).initialHref) const { pathname } = new URL(initialHref) - // since it was cached the initial value is now the redirect - // result expect(pathname).toBe(`${basePath}/gsp-blog/first`) }) } it('should apply redirect when fallback GSP page is visited directly (internal normal)', async () => { - const browser = await webdriver( - appPort, - `${basePath}/gsp-blog/redirect-dest-_`, - { - retryWaitHydration: true, - } - ) + const browser = await next.browser(`${basePath}/gsp-blog/redirect-dest-_`, { + retryWaitHydration: true, + }) await browser.waitForElementByCss('#index') @@ -188,14 +144,11 @@ const runTests = (isDev: boolean) => { expect(pathname).toBe(`${basePath}/gsp-blog/redirect-dest-_`) }) - if (!isDev) { + if (isNextStart) { it('should apply redirect when fallback GSP page is visited directly (internal normal) 2nd visit', async () => { - const browser = await webdriver( - appPort, + const browser = await next.browser( `${basePath}/gsp-blog/redirect-dest-_`, - { - retryWaitHydration: true, - } + { retryWaitHydration: true } ) await browser.waitForElementByCss('#index') @@ -207,18 +160,15 @@ const runTests = (isDev: boolean) => { } it('should apply redirect when fallback GSP page is visited directly (external)', async () => { - const browser = await webdriver( - appPort, + const browser = await next.browser( `${basePath}/gsp-blog/redirect-dest-_missing`, - { - retryWaitHydration: true, - } + { retryWaitHydration: true } ) - await check( - () => browser.eval(() => document.documentElement.innerHTML), - /oops not found/ - ) + await retry(async () => { + const html = await browser.eval(() => document.documentElement.innerHTML) + expect(html).toMatch(/oops not found/) + }) const initialHref = await browser.eval(() => (window as any).initialHref) expect(initialHref).toBeFalsy() @@ -229,62 +179,49 @@ const runTests = (isDev: boolean) => { }) it('should apply redirect when fallback GSP page is visited directly (external domain)', async () => { - const browser = await webdriver( - appPort, + const browser = await next.browser( `${basePath}/gsp-blog/redirect-dest-external`, - { - retryWaitHydration: true, - } + { retryWaitHydration: true } ) - await check( - () => browser.eval(() => document.location.hostname), - 'example.vercel.sh' - ) + await retry(async () => { + const hostname = await browser.eval(() => document.location.hostname) + expect(hostname).toBe('example.vercel.sh') + }) const initialHref = await browser.eval(() => (window as any).initialHref) expect(initialHref).toBeFalsy() }) it('should apply redirect when fallback GSSP page is visited directly (external domain)', async () => { - const browser = await webdriver( - appPort, + const browser = await next.browser( `${basePath}/gssp-blog/redirect-dest-external`, - { - retryWaitHydration: true, - } + { retryWaitHydration: true } ) - await check( - () => browser.eval(() => document.location.hostname), - 'example.vercel.sh' - ) + await retry(async () => { + const hostname = await browser.eval(() => document.location.hostname) + expect(hostname).toBe('example.vercel.sh') + }) const initialHref = await browser.eval(() => (window as any).initialHref) expect(initialHref).toBeFalsy() - const res = await fetchViaHTTP( - appPort, + const res = await next.fetch( `${basePath}/gssp-blog/redirect-dest-external`, - undefined, - { - redirect: 'manual', - } + { redirect: 'manual' } ) expect(res.status).toBe(307) - const parsed = new URL(res.headers.get('location')) + const parsed = new URL(res.headers.get('location')!) expect(parsed.hostname).toBe('example.vercel.sh') expect(parsed.pathname).toBe('/') }) it('should apply redirect when GSSP page is navigated to client-side (internal dynamic)', async () => { - const browser = await webdriver( - appPort, + const browser = await next.browser( `${basePath}/gssp-blog/redirect-dest-_gssp-blog_first`, - { - retryWaitHydration: true, - } + { retryWaitHydration: true } ) await browser.waitForElementByCss('#gssp') @@ -298,7 +235,7 @@ const runTests = (isDev: boolean) => { }) it('should apply redirect when GSSP page is navigated to client-side (internal normal)', async () => { - const browser = await webdriver(appPort, `${basePath}`, { + const browser = await next.browser(`${basePath}`, { retryWaitHydration: true, }) @@ -308,12 +245,11 @@ const runTests = (isDev: boolean) => { await browser.waitForElementByCss('#another') const text = await browser.elementByCss('#another').text() - expect(text).toEqual('another Page') }) it('should apply redirect when GSSP page is navigated to client-side (external)', async () => { - const browser = await webdriver(appPort, `${basePath}`, { + const browser = await next.browser(`${basePath}`, { retryWaitHydration: true, }) @@ -323,7 +259,6 @@ const runTests = (isDev: boolean) => { await browser.waitForElementByCss('#gssp') const props = JSON.parse(await browser.elementByCss('#props').text()) - expect(props).toEqual({ params: { post: 'first', @@ -332,7 +267,7 @@ const runTests = (isDev: boolean) => { }) it('should apply redirect when GSP page is navigated to client-side (internal)', async () => { - const browser = await webdriver(appPort, `${basePath}`, { + const browser = await next.browser(`${basePath}`, { retryWaitHydration: true, }) @@ -342,12 +277,11 @@ const runTests = (isDev: boolean) => { await browser.waitForElementByCss('#another') const text = await browser.elementByCss('#another').text() - expect(text).toEqual('another Page') }) it('should apply redirect when GSP page is navigated to client-side (external)', async () => { - const browser = await webdriver(appPort, `${basePath}`, { + const browser = await next.browser(`${basePath}`, { retryWaitHydration: true, }) @@ -357,7 +291,6 @@ const runTests = (isDev: boolean) => { await browser.waitForElementByCss('#gsp') const props = JSON.parse(await browser.elementByCss('#props').text()) - expect(props).toEqual({ params: { post: 'first', @@ -366,13 +299,9 @@ const runTests = (isDev: boolean) => { }) it('should not replace history of the origin page when GSSP page is navigated to client-side (internal normal)', async () => { - const browser = await webdriver( - appPort, - `${basePath}/another?mark_as=root`, - { - retryWaitHydration: true, - } - ) + const browser = await next.browser(`${basePath}/another?mark_as=root`, { + retryWaitHydration: true, + }) await browser.eval(`(function () { window.next.router.push('/') @@ -388,19 +317,17 @@ const runTests = (isDev: boolean) => { window.history.back() })()`) - const curUrl = await browser.url() - const { pathname, search } = new URL(curUrl) - expect(pathname + search).toEqual(`${basePath}`) + await retry(async () => { + const curUrl = await browser.url() + const { pathname, search } = new URL(curUrl) + expect(pathname + search).toEqual(`${basePath}`) + }) }) it('should not replace history of the origin page when GSSP page is navigated to client-side (external)', async () => { - const browser = await webdriver( - appPort, - `${basePath}/another?mark_as=root`, - { - retryWaitHydration: true, - } - ) + const browser = await next.browser(`${basePath}/another?mark_as=root`, { + retryWaitHydration: true, + }) await browser.eval(`(function () { window.next.router.push('/') @@ -416,19 +343,17 @@ const runTests = (isDev: boolean) => { window.history.back() })()`) - const curUrl = await browser.url() - const { pathname, search } = new URL(curUrl) - expect(pathname + search).toEqual(`${basePath}`) + await retry(async () => { + const curUrl = await browser.url() + const { pathname, search } = new URL(curUrl) + expect(pathname + search).toEqual(`${basePath}`) + }) }) it('should not replace history of the origin page when GSP page is navigated to client-side (internal)', async () => { - const browser = await webdriver( - appPort, - `${basePath}/another?mark_as=root`, - { - retryWaitHydration: true, - } - ) + const browser = await next.browser(`${basePath}/another?mark_as=root`, { + retryWaitHydration: true, + }) await browser.eval(`(function () { window.next.router.push('/') @@ -444,19 +369,17 @@ const runTests = (isDev: boolean) => { window.history.back() })()`) - const curUrl = await browser.url() - const { pathname, search } = new URL(curUrl) - expect(pathname + search).toEqual(`${basePath}`) + await retry(async () => { + const curUrl = await browser.url() + const { pathname, search } = new URL(curUrl) + expect(pathname + search).toEqual(`${basePath}`) + }) }) it('should not replace history of the origin page when GSP page is navigated to client-side (external)', async () => { - const browser = await webdriver( - appPort, - `${basePath}/another?mark_as=root`, - { - retryWaitHydration: true, - } - ) + const browser = await next.browser(`${basePath}/another?mark_as=root`, { + retryWaitHydration: true, + }) await browser.eval(`(function () { window.next.router.push('/') @@ -472,43 +395,18 @@ const runTests = (isDev: boolean) => { window.history.back() })()`) - const curUrl = await browser.url() - const { pathname, search } = new URL(curUrl) - expect(pathname + search).toEqual(`${basePath}`) + await retry(async () => { + const curUrl = await browser.url() + const { pathname, search } = new URL(curUrl) + expect(pathname + search).toEqual(`${basePath}`) + }) }) -} - -describe('GS(S)P Redirect Support', () => { - ;(process.env.TURBOPACK_BUILD ? describe.skip : describe)( - 'development mode', - () => { - beforeAll(async () => { - appPort = await findPort() - app = await launchApp(appDir, appPort) - }) - afterAll(() => killApp(app)) - - runTests(true) - } - ) - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( - 'production mode', - () => { - beforeAll(async () => { - await fs.remove(join(appDir, '.next')) - await nextBuild(appDir) - appPort = await findPort() - app = await nextStart(appDir, appPort) - }) - afterAll(() => killApp(app)) - runTests(false) - - it('should error for redirect during prerendering', async () => { - await fs.mkdirp(join(appDir, 'pages/invalid')) - await fs.writeFile( - join(appDir, 'pages', 'invalid', '[slug].js'), - ` + if (isNextStart) { + it('should error for redirect during prerendering', async () => { + await next.patchFile( + 'pages/invalid/[slug].js', + ` export default function Post(props) { return "hi" } @@ -529,18 +427,15 @@ describe('GS(S)P Redirect Support', () => { } } ` - ) - const { stdout, stderr } = await nextBuild(appDir, undefined, { - stdout: true, - stderr: true, - }) - const output = stdout + stderr - await fs.remove(join(appDir, 'pages/invalid')) - - expect(output).toContain( - '`redirect` can not be returned from getStaticProps during prerendering' - ) - }) - } - ) + ) + await next.stop() + const { cliOutput } = await next.build() + await next.deleteFile('pages/invalid/[slug].js') + await next.start() + + expect(cliOutput).toContain( + '`redirect` can not be returned from getStaticProps during prerendering' + ) + }) + } }) diff --git a/test/integration/gssp-redirect-base-path/next.config.js b/test/e2e/gssp-redirect-base-path/next.config.js similarity index 100% rename from test/integration/gssp-redirect-base-path/next.config.js rename to test/e2e/gssp-redirect-base-path/next.config.js diff --git a/test/integration/gssp-redirect-base-path/pages/404.js b/test/e2e/gssp-redirect-base-path/pages/404.js similarity index 100% rename from test/integration/gssp-redirect-base-path/pages/404.js rename to test/e2e/gssp-redirect-base-path/pages/404.js diff --git a/test/integration/gssp-redirect-base-path/pages/another.js b/test/e2e/gssp-redirect-base-path/pages/another.js similarity index 100% rename from test/integration/gssp-redirect-base-path/pages/another.js rename to test/e2e/gssp-redirect-base-path/pages/another.js diff --git a/test/integration/gssp-redirect-base-path/pages/gsp-blog/[post].js b/test/e2e/gssp-redirect-base-path/pages/gsp-blog/[post].js similarity index 100% rename from test/integration/gssp-redirect-base-path/pages/gsp-blog/[post].js rename to test/e2e/gssp-redirect-base-path/pages/gsp-blog/[post].js diff --git a/test/integration/gssp-redirect-base-path/pages/gssp-blog/[post].js b/test/e2e/gssp-redirect-base-path/pages/gssp-blog/[post].js similarity index 100% rename from test/integration/gssp-redirect-base-path/pages/gssp-blog/[post].js rename to test/e2e/gssp-redirect-base-path/pages/gssp-blog/[post].js diff --git a/test/integration/gssp-redirect-base-path/pages/index.js b/test/e2e/gssp-redirect-base-path/pages/index.js similarity index 100% rename from test/integration/gssp-redirect-base-path/pages/index.js rename to test/e2e/gssp-redirect-base-path/pages/index.js diff --git a/test/integration/gssp-redirect/test/index.test.ts b/test/e2e/gssp-redirect/gssp-redirect.test.ts similarity index 56% rename from test/integration/gssp-redirect/test/index.test.ts rename to test/e2e/gssp-redirect/gssp-redirect.test.ts index 90dfdd05c5f0..007ec434ea76 100644 --- a/test/integration/gssp-redirect/test/index.test.ts +++ b/test/e2e/gssp-redirect/gssp-redirect.test.ts @@ -1,97 +1,63 @@ -/* eslint-env jest */ -import fs from 'fs-extra' -import webdriver from 'next-webdriver' -import { join } from 'path' -import { - findPort, - launchApp, - killApp, - nextBuild, - nextStart, - fetchViaHTTP, - check, -} from 'next-test-utils' - -const appDir = join(__dirname, '..') - -let app -let appPort - -const runTests = (isDev: boolean) => { +import { nextTestSetup, isNextStart } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('GS(S)P Redirect Support', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + dependencies: { + react: '19.3.0-canary-fef12a01-20260413', + 'react-dom': '19.3.0-canary-fef12a01-20260413', + }, + skipDeployment: true, + }) + if (skipped) return + it('should apply temporary redirect when visited directly for GSSP page', async () => { - const res = await fetchViaHTTP( - appPort, - '/gssp-blog/redirect-1', - undefined, - { - redirect: 'manual', - } - ) + const res = await next.fetch('/gssp-blog/redirect-1', { + redirect: 'manual', + }) expect(res.status).toBe(307) - const { pathname } = new URL(res.headers.get('location')) - + const { pathname } = new URL(res.headers.get('location')!) expect(pathname).toBe('/404') }) it('should apply permanent redirect when visited directly for GSSP page', async () => { - const res = await fetchViaHTTP( - appPort, - '/gssp-blog/redirect-permanent', - undefined, - { - redirect: 'manual', - } - ) + const res = await next.fetch('/gssp-blog/redirect-permanent', { + redirect: 'manual', + }) expect(res.status).toBe(308) - const { pathname } = new URL(res.headers.get('location')) - + const { pathname } = new URL(res.headers.get('location')!) expect(pathname).toBe('/404') expect(res.headers.get('refresh')).toMatch(/url=\/404/) }) it('should apply statusCode 301 redirect when visited directly for GSSP page', async () => { - const res = await fetchViaHTTP( - appPort, - '/gssp-blog/redirect-statusCode-301', - undefined, - { - redirect: 'manual', - } - ) + const res = await next.fetch('/gssp-blog/redirect-statusCode-301', { + redirect: 'manual', + }) expect(res.status).toBe(301) - const { pathname } = new URL(res.headers.get('location')) - + const { pathname } = new URL(res.headers.get('location')!) expect(pathname).toBe('/404') expect(res.headers.get('refresh')).toBe(null) }) it('should apply statusCode 303 redirect when visited directly for GSSP page', async () => { - const res = await fetchViaHTTP( - appPort, - '/gssp-blog/redirect-statusCode-303', - undefined, - { - redirect: 'manual', - } - ) + const res = await next.fetch('/gssp-blog/redirect-statusCode-303', { + redirect: 'manual', + }) expect(res.status).toBe(303) - const { pathname } = new URL(res.headers.get('location')) - + const { pathname } = new URL(res.headers.get('location')!) expect(pathname).toBe('/404') expect(res.headers.get('refresh')).toBe(null) }) it('should apply redirect when fallback GSP page is visited directly (internal dynamic)', async () => { - const browser = await webdriver( - appPort, - '/gsp-blog/redirect-dest-_gsp-blog_first', - { - retryWaitHydration: true, - } + const browser = await next.browser( + '/gsp-blog/redirect-dest-_gsp-blog_first' ) await browser.waitForElementByCss('#gsp') @@ -102,18 +68,16 @@ const runTests = (isDev: boolean) => { post: 'first', }, }) - const initialHref = await browser.eval(() => (window as any).initialHref) + const initialHref = await browser.eval( + () => (window as any).initialHref as string + ) const { pathname } = new URL(initialHref) expect(pathname).toBe('/gsp-blog/redirect-dest-_gsp-blog_first') }) it('should apply redirect when fallback blocking GSP page is visited directly (internal dynamic)', async () => { - const browser = await webdriver( - appPort, - '/gsp-blog-blocking/redirect-dest-_gsp-blog_first', - { - retryWaitHydration: true, - } + const browser = await next.browser( + '/gsp-blog-blocking/redirect-dest-_gsp-blog_first' ) await browser.waitForElementByCss('#gsp') @@ -124,18 +88,16 @@ const runTests = (isDev: boolean) => { post: 'first', }, }) - const initialHref = await browser.eval(() => (window as any).initialHref) + const initialHref = await browser.eval( + () => (window as any).initialHref as string + ) const { pathname } = new URL(initialHref) expect(pathname).toBe('/gsp-blog/first') }) it('should apply redirect when fallback blocking GSP page is visited directly (internal dynamic) second visit', async () => { - const browser = await webdriver( - appPort, - '/gsp-blog-blocking/redirect-dest-_gsp-blog_first', - { - retryWaitHydration: true, - } + const browser = await next.browser( + '/gsp-blog-blocking/redirect-dest-_gsp-blog_first' ) await browser.waitForElementByCss('#gsp') @@ -146,18 +108,16 @@ const runTests = (isDev: boolean) => { post: 'first', }, }) - const initialHref = await browser.eval(() => (window as any).initialHref) + const initialHref = await browser.eval( + () => (window as any).initialHref as string + ) const { pathname } = new URL(initialHref) expect(pathname).toBe('/gsp-blog/first') }) it('should apply redirect when fallback blocking GSP page is visited directly (internal dynamic) with revalidate', async () => { - const browser = await webdriver( - appPort, - '/gsp-blog-blocking/redirect-revalidate-dest-_gsp-blog_first', - { - retryWaitHydration: true, - } + const browser = await next.browser( + '/gsp-blog-blocking/redirect-revalidate-dest-_gsp-blog_first' ) await browser.waitForElementByCss('#gsp') @@ -168,18 +128,16 @@ const runTests = (isDev: boolean) => { post: 'first', }, }) - const initialHref = await browser.eval(() => (window as any).initialHref) + const initialHref = await browser.eval( + () => (window as any).initialHref as string + ) const { pathname } = new URL(initialHref) expect(pathname).toBe('/gsp-blog/first') }) it('should apply redirect when fallback blocking GSP page is visited directly (internal dynamic) with revalidate second visit', async () => { - const browser = await webdriver( - appPort, - '/gsp-blog-blocking/redirect-revalidate-dest-_gsp-blog_first', - { - retryWaitHydration: true, - } + const browser = await next.browser( + '/gsp-blog-blocking/redirect-revalidate-dest-_gsp-blog_first' ) await browser.waitForElementByCss('#gsp') @@ -190,19 +148,17 @@ const runTests = (isDev: boolean) => { post: 'first', }, }) - const initialHref = await browser.eval(() => (window as any).initialHref) + const initialHref = await browser.eval( + () => (window as any).initialHref as string + ) const { pathname } = new URL(initialHref) expect(pathname).toBe('/gsp-blog/first') }) - if (!isDev) { + if (isNextStart) { it('should apply redirect when fallback GSP page is visited directly (internal dynamic) 2nd visit', async () => { - const browser = await webdriver( - appPort, - '/gsp-blog/redirect-dest-_gsp-blog_first', - { - retryWaitHydration: true, - } + const browser = await next.browser( + '/gsp-blog/redirect-dest-_gsp-blog_first' ) await browser.waitForElementByCss('#gsp') @@ -213,55 +169,51 @@ const runTests = (isDev: boolean) => { post: 'first', }, }) - const initialHref = await browser.eval(() => (window as any).initialHref) + const initialHref = await browser.eval( + () => (window as any).initialHref as string + ) const { pathname } = new URL(initialHref) - // since it was cached the initial value is now the redirect - // result expect(pathname).toBe('/gsp-blog/first') }) } it('should apply redirect when fallback GSP page is visited directly (internal normal)', async () => { - const browser = await webdriver(appPort, '/gsp-blog/redirect-dest-_', { - retryWaitHydration: true, - }) + const browser = await next.browser('/gsp-blog/redirect-dest-_') await browser.waitForElementByCss('#index') - const initialHref = await browser.eval(() => (window as any).initialHref) + const initialHref = await browser.eval( + () => (window as any).initialHref as string + ) const { pathname } = new URL(initialHref) expect(pathname).toBe('/gsp-blog/redirect-dest-_') }) - if (!isDev) { + if (isNextStart) { it('should apply redirect when fallback GSP page is visited directly (internal normal) 2nd visit', async () => { - const browser = await webdriver(appPort, '/gsp-blog/redirect-dest-_', { - retryWaitHydration: true, - }) + const browser = await next.browser('/gsp-blog/redirect-dest-_') await browser.waitForElementByCss('#index') - const initialHref = await browser.eval(() => (window as any).initialHref) + const initialHref = await browser.eval( + () => (window as any).initialHref as string + ) const { pathname } = new URL(initialHref) expect(pathname).toBe('/') }) } it('should apply redirect when fallback GSP page is visited directly (external)', async () => { - const browser = await webdriver( - appPort, - '/gsp-blog/redirect-dest-_missing', - { - retryWaitHydration: true, - } - ) + const browser = await next.browser('/gsp-blog/redirect-dest-_missing') - await check( - () => browser.eval(() => document.documentElement.innerHTML), - /oops not found/ - ) + await retry(async () => { + const html = await browser.eval(() => document.documentElement.innerHTML) + expect(html).toMatch(/oops not found/) + }) - const initialHref = await browser.eval(() => (window as any).initialHref) + const initialHref = await browser.eval( + () => (window as any).initialHref as string + ) expect(initialHref).toBeFalsy() const curUrl = await browser.url() @@ -270,62 +222,45 @@ const runTests = (isDev: boolean) => { }) it('should apply redirect when fallback GSP page is visited directly (external domain)', async () => { - const browser = await webdriver( - appPort, - '/gsp-blog/redirect-dest-external', - { - retryWaitHydration: true, - } - ) + const browser = await next.browser('/gsp-blog/redirect-dest-external') - await check( - () => browser.eval(() => document.location.hostname), - 'example.vercel.sh' - ) + await retry(async () => { + const hostname = await browser.eval(() => document.location.hostname) + expect(hostname).toBe('example.vercel.sh') + }) - const initialHref = await browser.eval(() => (window as any).initialHref) + const initialHref = await browser.eval( + () => (window as any).initialHref as string + ) expect(initialHref).toBeFalsy() }) it('should apply redirect when fallback GSSP page is visited directly (external domain)', async () => { - const browser = await webdriver( - appPort, - '/gssp-blog/redirect-dest-external', - { - retryWaitHydration: true, - } - ) + const browser = await next.browser('/gssp-blog/redirect-dest-external') - await check( - () => browser.eval(() => document.location.hostname), - 'example.vercel.sh' - ) + await retry(async () => { + const hostname = await browser.eval(() => document.location.hostname) + expect(hostname).toBe('example.vercel.sh') + }) - const initialHref = await browser.eval(() => (window as any).initialHref) + const initialHref = await browser.eval( + () => (window as any).initialHref as string + ) expect(initialHref).toBeFalsy() - const res = await fetchViaHTTP( - appPort, - '/gssp-blog/redirect-dest-external', - undefined, - { - redirect: 'manual', - } - ) + const res = await next.fetch('/gssp-blog/redirect-dest-external', { + redirect: 'manual', + }) expect(res.status).toBe(307) - const parsed = new URL(res.headers.get('location')) + const parsed = new URL(res.headers.get('location')!) expect(parsed.hostname).toBe('example.vercel.sh') expect(parsed.pathname).toBe('/') }) it('should apply redirect when GSSP page is navigated to client-side (internal dynamic)', async () => { - const browser = await webdriver( - appPort, - '/gssp-blog/redirect-dest-_gssp-blog_first', - { - retryWaitHydration: true, - } + const browser = await next.browser( + '/gssp-blog/redirect-dest-_gssp-blog_first' ) await browser.waitForElementByCss('#gssp') @@ -339,9 +274,7 @@ const runTests = (isDev: boolean) => { }) it('should apply redirect when GSSP page is navigated to client-side (internal normal)', async () => { - const browser = await webdriver(appPort, '/', { - retryWaitHydration: true, - }) + const browser = await next.browser('/') await browser.eval(`(function () { window.next.router.push('/gssp-blog/redirect-dest-_another') @@ -349,14 +282,11 @@ const runTests = (isDev: boolean) => { await browser.waitForElementByCss('#another') const text = await browser.elementByCss('#another').text() - expect(text).toEqual('another Page') }) it('should apply redirect when GSSP page is navigated to client-side (external)', async () => { - const browser = await webdriver(appPort, '/', { - retryWaitHydration: true, - }) + const browser = await next.browser('/') await browser.eval(`(function () { window.next.router.push('/gssp-blog/redirect-dest-_gssp-blog_first') @@ -364,7 +294,6 @@ const runTests = (isDev: boolean) => { await browser.waitForElementByCss('#gssp') const props = JSON.parse(await browser.elementByCss('#props').text()) - expect(props).toEqual({ params: { post: 'first', @@ -373,9 +302,7 @@ const runTests = (isDev: boolean) => { }) it('should apply redirect when GSP page is navigated to client-side (internal)', async () => { - const browser = await webdriver(appPort, '/', { - retryWaitHydration: true, - }) + const browser = await next.browser('/') await browser.eval(`(function () { window.next.router.push('/gsp-blog/redirect-dest-_another') @@ -383,14 +310,11 @@ const runTests = (isDev: boolean) => { await browser.waitForElementByCss('#another') const text = await browser.elementByCss('#another').text() - expect(text).toEqual('another Page') }) it('should apply redirect when GSP page is navigated to client-side (external)', async () => { - const browser = await webdriver(appPort, '/', { - retryWaitHydration: true, - }) + const browser = await next.browser('/') await browser.eval(`(function () { window.next.router.push('/gsp-blog/redirect-dest-_gsp-blog_first') @@ -398,7 +322,6 @@ const runTests = (isDev: boolean) => { await browser.waitForElementByCss('#gsp') const props = JSON.parse(await browser.elementByCss('#props').text()) - expect(props).toEqual({ params: { post: 'first', @@ -407,9 +330,7 @@ const runTests = (isDev: boolean) => { }) it('should not replace history of the origin page when GSSP page is navigated to client-side (internal normal)', async () => { - const browser = await webdriver(appPort, '/another?mark_as=root', { - retryWaitHydration: true, - }) + const browser = await next.browser('/another?mark_as=root') await browser.eval(`(function () { window.next.router.push('/') @@ -425,15 +346,15 @@ const runTests = (isDev: boolean) => { window.history.back() })()`) - const curUrl = await browser.url() - const { pathname, search } = new URL(curUrl) - expect(pathname + search).toEqual('/') + await retry(async () => { + const curUrl = await browser.url() + const { pathname, search } = new URL(curUrl) + expect(pathname + search).toEqual('/') + }) }) it('should not replace history of the origin page when GSSP page is navigated to client-side (external)', async () => { - const browser = await webdriver(appPort, '/another?mark_as=root', { - retryWaitHydration: true, - }) + const browser = await next.browser('/another?mark_as=root') await browser.eval(`(function () { window.next.router.push('/') @@ -449,15 +370,15 @@ const runTests = (isDev: boolean) => { window.history.back() })()`) - const curUrl = await browser.url() - const { pathname, search } = new URL(curUrl) - expect(pathname + search).toEqual('/') + await retry(async () => { + const curUrl = await browser.url() + const { pathname, search } = new URL(curUrl) + expect(pathname + search).toEqual('/') + }) }) it('should not replace history of the origin page when GSP page is navigated to client-side (internal)', async () => { - const browser = await webdriver(appPort, '/another?mark_as=root', { - retryWaitHydration: true, - }) + const browser = await next.browser('/another?mark_as=root') await browser.eval(`(function () { window.next.router.push('/') @@ -473,15 +394,15 @@ const runTests = (isDev: boolean) => { window.history.back() })()`) - const curUrl = await browser.url() - const { pathname, search } = new URL(curUrl) - expect(pathname + search).toEqual('/') + await retry(async () => { + const curUrl = await browser.url() + const { pathname, search } = new URL(curUrl) + expect(pathname + search).toEqual('/') + }) }) it('should not replace history of the origin page when GSP page is navigated to client-side (external)', async () => { - const browser = await webdriver(appPort, '/another?mark_as=root', { - retryWaitHydration: true, - }) + const browser = await next.browser('/another?mark_as=root') await browser.eval(`(function () { window.next.router.push('/') @@ -497,56 +418,22 @@ const runTests = (isDev: boolean) => { window.history.back() })()`) - const curUrl = await browser.url() - const { pathname, search } = new URL(curUrl) - expect(pathname + search).toEqual('/') + await retry(async () => { + const curUrl = await browser.url() + const { pathname, search } = new URL(curUrl) + expect(pathname + search).toEqual('/') + }) }) -} - -describe('GS(S)P Redirect Support', () => { - ;(process.env.TURBOPACK_BUILD ? describe.skip : describe)( - 'development mode', - () => { - beforeAll(async () => { - appPort = await findPort() - app = await launchApp(appDir, appPort) - }) - afterAll(() => killApp(app)) - - runTests(true) - } - ) - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( - 'production mode', - () => { - let output = '' - - beforeAll(async () => { - await fs.remove(join(appDir, '.next')) - await nextBuild(appDir) - appPort = await findPort() - app = await nextStart(appDir, appPort, { - onStdout(msg) { - output += msg - }, - onStderr(msg) { - output += msg - }, - }) - }) - afterAll(() => killApp(app)) - runTests(false) - - it('should not have errors in output', async () => { - expect(output).not.toContain('Failed to update prerender files') - }) + if (isNextStart) { + it('should not have errors in output', async () => { + expect(next.cliOutput).not.toContain('Failed to update prerender files') + }) - it('should error for redirect during prerendering', async () => { - await fs.mkdirp(join(appDir, 'pages/invalid')) - await fs.writeFile( - join(appDir, 'pages', 'invalid', '[slug].js'), - ` + it('should error for redirect during prerendering', async () => { + await next.patchFile( + 'pages/invalid/[slug].js', + ` export default function Post(props) { return "hi" } @@ -567,18 +454,15 @@ describe('GS(S)P Redirect Support', () => { } } ` - ) - const { stdout, stderr } = await nextBuild(appDir, undefined, { - stdout: true, - stderr: true, - }) - const output = stdout + stderr - await fs.remove(join(appDir, 'pages/invalid')) - - expect(output).toContain( - '`redirect` can not be returned from getStaticProps during prerendering' - ) - }) - } - ) + ) + await next.stop() + const { cliOutput } = await next.build() + await next.deleteFile('pages/invalid/[slug].js') + await next.start() + + expect(cliOutput).toContain( + '`redirect` can not be returned from getStaticProps during prerendering' + ) + }) + } }) diff --git a/test/integration/gssp-redirect/pages/404.js b/test/e2e/gssp-redirect/pages/404.js similarity index 100% rename from test/integration/gssp-redirect/pages/404.js rename to test/e2e/gssp-redirect/pages/404.js diff --git a/test/integration/gssp-redirect/pages/another.js b/test/e2e/gssp-redirect/pages/another.js similarity index 100% rename from test/integration/gssp-redirect/pages/another.js rename to test/e2e/gssp-redirect/pages/another.js diff --git a/test/integration/gssp-redirect/pages/gsp-blog-blocking/[post].js b/test/e2e/gssp-redirect/pages/gsp-blog-blocking/[post].js similarity index 100% rename from test/integration/gssp-redirect/pages/gsp-blog-blocking/[post].js rename to test/e2e/gssp-redirect/pages/gsp-blog-blocking/[post].js diff --git a/test/integration/gssp-redirect/pages/gsp-blog/[post].js b/test/e2e/gssp-redirect/pages/gsp-blog/[post].js similarity index 100% rename from test/integration/gssp-redirect/pages/gsp-blog/[post].js rename to test/e2e/gssp-redirect/pages/gsp-blog/[post].js diff --git a/test/integration/gssp-redirect/pages/gssp-blog/[post].js b/test/e2e/gssp-redirect/pages/gssp-blog/[post].js similarity index 100% rename from test/integration/gssp-redirect/pages/gssp-blog/[post].js rename to test/e2e/gssp-redirect/pages/gssp-blog/[post].js diff --git a/test/integration/gssp-redirect/pages/index.js b/test/e2e/gssp-redirect/pages/index.js similarity index 100% rename from test/integration/gssp-redirect/pages/index.js rename to test/e2e/gssp-redirect/pages/index.js diff --git a/test/e2e/hashbang/hashbang.test.ts b/test/e2e/hashbang/hashbang.test.ts new file mode 100644 index 000000000000..1e8b2084dadd --- /dev/null +++ b/test/e2e/hashbang/hashbang.test.ts @@ -0,0 +1,24 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('hashbang', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + describe('first-line hashbang (#!) parse', () => { + it('should work for .js files', async () => { + const html = await next.render('/') + expect(html).toMatch('JS: 123') + }) + + it('should work for .mjs files', async () => { + const html = await next.render('/') + expect(html).toMatch('MJS: 456') + }) + + it('should work for .cjs files', async () => { + const html = await next.render('/') + expect(html).toMatch('CJS: 789') + }) + }) +}) diff --git a/test/integration/hashbang/src/cases/cjs.cjs b/test/e2e/hashbang/src/cases/cjs.cjs similarity index 100% rename from test/integration/hashbang/src/cases/cjs.cjs rename to test/e2e/hashbang/src/cases/cjs.cjs diff --git a/test/integration/hashbang/src/cases/js.js b/test/e2e/hashbang/src/cases/js.js similarity index 100% rename from test/integration/hashbang/src/cases/js.js rename to test/e2e/hashbang/src/cases/js.js diff --git a/test/integration/hashbang/src/cases/mjs.mjs b/test/e2e/hashbang/src/cases/mjs.mjs similarity index 100% rename from test/integration/hashbang/src/cases/mjs.mjs rename to test/e2e/hashbang/src/cases/mjs.mjs diff --git a/test/integration/hashbang/src/pages/index.js b/test/e2e/hashbang/src/pages/index.js similarity index 100% rename from test/integration/hashbang/src/pages/index.js rename to test/e2e/hashbang/src/pages/index.js diff --git a/test/e2e/hydration/hydration.test.ts b/test/e2e/hydration/hydration.test.ts new file mode 100644 index 000000000000..caac7317cf4c --- /dev/null +++ b/test/e2e/hydration/hydration.test.ts @@ -0,0 +1,29 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('Hydration', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('hydrates correctly for normal page', async () => { + const browser = await next.browser('/') + expect(await browser.eval('window.didHydrate')).toBe(true) + }) + + it('hydrates correctly for //', async () => { + const browser = await next.browser('//') + expect(await browser.eval('window.didHydrate')).toBe(true) + }) + + it('should be able to navigate after loading //', async () => { + const browser = await next.browser('//') + await browser.eval('window.beforeNav = true') + await browser.eval('window.next.router.push("/details")') + await retry(async () => { + const html = await browser.eval('document.documentElement.innerHTML') + expect(html).toMatch(/details/) + }) + expect(await browser.eval('window.beforeNav')).toBe(true) + }) +}) diff --git a/test/integration/hydration/pages/404.js b/test/e2e/hydration/pages/404.js similarity index 100% rename from test/integration/hydration/pages/404.js rename to test/e2e/hydration/pages/404.js diff --git a/test/integration/hydration/pages/_app.js b/test/e2e/hydration/pages/_app.js similarity index 100% rename from test/integration/hydration/pages/_app.js rename to test/e2e/hydration/pages/_app.js diff --git a/test/integration/hydration/pages/_document.js b/test/e2e/hydration/pages/_document.js similarity index 100% rename from test/integration/hydration/pages/_document.js rename to test/e2e/hydration/pages/_document.js diff --git a/test/integration/hydration/pages/details.js b/test/e2e/hydration/pages/details.js similarity index 100% rename from test/integration/hydration/pages/details.js rename to test/e2e/hydration/pages/details.js diff --git a/test/integration/hydration/pages/index.js b/test/e2e/hydration/pages/index.js similarity index 100% rename from test/integration/hydration/pages/index.js rename to test/e2e/hydration/pages/index.js diff --git a/test/e2e/i18n-support-base-path/i18n-support-base-path.test.ts b/test/e2e/i18n-support-base-path/i18n-support-base-path.test.ts new file mode 100644 index 000000000000..77c3f2b7e5b9 --- /dev/null +++ b/test/e2e/i18n-support-base-path/i18n-support-base-path.test.ts @@ -0,0 +1,161 @@ +import http from 'http' +import { join } from 'path' +import cheerio from 'cheerio' +import { runTests, locales } from '../i18n-support/shared' +import { findPort, fetchViaHTTP } from 'next-test-utils' +import { nextTestSetup, isNextDev } from 'e2e-utils' + +describe('i18n Support basePath', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + if (skipped) return + + const ctx: Record<string, any> = { + basePath: '/docs', + isDev: isNextDev, + } + + let externalServer: http.Server + let externalPort: number + + beforeAll(async () => { + externalPort = await findPort() + externalServer = http.createServer((req, res) => { + res.statusCode = 200 + res.end(JSON.stringify({ url: req.url, external: true })) + }) + await new Promise<void>((resolve, reject) => { + externalServer.listen(externalPort, (err?: Error) => + err ? reject(err) : resolve() + ) + }) + + await next.patchFile('next.config.js', (content) => + content.replace(/__EXTERNAL_PORT__/g, String(externalPort)) + ) + + if (!isNextDev) { + await next.build() + } + await next.start() + + ctx.appDir = next.testDir + ctx.appPort = Number(new URL(next.url).port) + if (!isNextDev) { + ctx.buildId = (await next.readFile('.next/BUILD_ID')).trim() + ctx.buildPagesDir = join(next.testDir, '.next/server/pages') + } else { + ctx.buildId = 'development' + } + }) + + afterAll(() => { + externalServer?.close() + }) + ;(isNextDev ? describe : describe.skip)('development mode', () => { + runTests(ctx) + }) + ;(!isNextDev ? describe : describe.skip)('production mode', () => { + runTests(ctx) + }) + + describe('with localeDetection disabled', () => { + if (!isNextDev) { + beforeAll(async () => { + await next.stop() + await next.patchFile('next.config.js', (content) => + content.replace('// localeDetection', 'localeDetection') + ) + await next.build() + await next.start() + ctx.appPort = Number(new URL(next.url).port) + }) + + it('should have localeDetection in routes-manifest', async () => { + const routesManifest = JSON.parse( + await next.readFile('.next/routes-manifest.json') + ) + + expect(routesManifest.i18n).toEqual({ + localeDetection: false, + locales: [ + 'en-US', + 'nl-NL', + 'nl-BE', + 'nl', + 'fr-BE', + 'fr', + 'en', + 'go', + 'go-BE', + 'do', + 'do-BE', + ], + defaultLocale: 'en-US', + domains: [ + { + http: true, + domain: 'example.do', + defaultLocale: 'do', + locales: ['do-BE'], + }, + { + domain: 'example.com', + defaultLocale: 'go', + locales: ['go-BE'], + }, + ], + }) + }) + + it('should not detect locale from accept-language', async () => { + const res = await fetchViaHTTP( + ctx.appPort, + `${ctx.basePath || '/'}`, + {}, + { + redirect: 'manual', + headers: { + 'accept-language': 'fr', + }, + } + ) + + expect(res.status).toBe(200) + const $ = cheerio.load(await res.text()) + expect($('html').attr('lang')).toBe('en-US') + expect($('#router-locale').text()).toBe('en-US') + expect(JSON.parse($('#router-locales').text())).toEqual(locales) + expect($('#router-pathname').text()).toBe('/') + expect($('#router-as-path').text()).toBe('/') + }) + + it('should set locale from detected path', async () => { + for (const locale of locales) { + const res = await fetchViaHTTP( + ctx.appPort, + `${ctx.basePath}/${locale}`, + {}, + { + redirect: 'manual', + headers: { + 'accept-language': 'en-US,en;q=0.9', + }, + } + ) + + expect(res.status).toBe(200) + const $ = cheerio.load(await res.text()) + expect($('html').attr('lang')).toBe(locale) + expect($('#router-locale').text()).toBe(locale) + expect(JSON.parse($('#router-locales').text())).toEqual(locales) + expect($('#router-pathname').text()).toBe('/') + expect($('#router-as-path').text()).toBe('/') + } + }) + } + }) +}) diff --git a/test/integration/i18n-support-base-path/next.config.js b/test/e2e/i18n-support-base-path/next.config.js similarity index 100% rename from test/integration/i18n-support-base-path/next.config.js rename to test/e2e/i18n-support-base-path/next.config.js diff --git a/test/integration/i18n-support-base-path/pages/404.js b/test/e2e/i18n-support-base-path/pages/404.js similarity index 100% rename from test/integration/i18n-support-base-path/pages/404.js rename to test/e2e/i18n-support-base-path/pages/404.js diff --git a/test/integration/i18n-support-base-path/pages/[post]/[comment].js b/test/e2e/i18n-support-base-path/pages/[post]/[comment].js similarity index 100% rename from test/integration/i18n-support-base-path/pages/[post]/[comment].js rename to test/e2e/i18n-support-base-path/pages/[post]/[comment].js diff --git a/test/integration/i18n-support-base-path/pages/[post]/index.js b/test/e2e/i18n-support-base-path/pages/[post]/index.js similarity index 100% rename from test/integration/i18n-support-base-path/pages/[post]/index.js rename to test/e2e/i18n-support-base-path/pages/[post]/index.js diff --git a/test/integration/i18n-support-base-path/pages/_app.js b/test/e2e/i18n-support-base-path/pages/_app.js similarity index 100% rename from test/integration/i18n-support-base-path/pages/_app.js rename to test/e2e/i18n-support-base-path/pages/_app.js diff --git a/test/integration/i18n-support-base-path/pages/another.js b/test/e2e/i18n-support-base-path/pages/another.js similarity index 100% rename from test/integration/i18n-support-base-path/pages/another.js rename to test/e2e/i18n-support-base-path/pages/another.js diff --git a/test/integration/i18n-support-base-path/pages/api/hello.js b/test/e2e/i18n-support-base-path/pages/api/hello.js similarity index 100% rename from test/integration/i18n-support-base-path/pages/api/hello.js rename to test/e2e/i18n-support-base-path/pages/api/hello.js diff --git a/test/integration/i18n-support-base-path/pages/api/post/[slug].js b/test/e2e/i18n-support-base-path/pages/api/post/[slug].js similarity index 100% rename from test/integration/i18n-support-base-path/pages/api/post/[slug].js rename to test/e2e/i18n-support-base-path/pages/api/post/[slug].js diff --git a/test/integration/i18n-support-base-path/pages/auto-export/index.js b/test/e2e/i18n-support-base-path/pages/auto-export/index.js similarity index 100% rename from test/integration/i18n-support-base-path/pages/auto-export/index.js rename to test/e2e/i18n-support-base-path/pages/auto-export/index.js diff --git a/test/integration/i18n-support-base-path/pages/developments/index.js b/test/e2e/i18n-support-base-path/pages/developments/index.js similarity index 100% rename from test/integration/i18n-support-base-path/pages/developments/index.js rename to test/e2e/i18n-support-base-path/pages/developments/index.js diff --git a/test/integration/i18n-support-base-path/pages/dynamic/[slug].js b/test/e2e/i18n-support-base-path/pages/dynamic/[slug].js similarity index 100% rename from test/integration/i18n-support-base-path/pages/dynamic/[slug].js rename to test/e2e/i18n-support-base-path/pages/dynamic/[slug].js diff --git a/test/integration/i18n-support-base-path/pages/frank.js b/test/e2e/i18n-support-base-path/pages/frank.js similarity index 100% rename from test/integration/i18n-support-base-path/pages/frank.js rename to test/e2e/i18n-support-base-path/pages/frank.js diff --git a/test/integration/i18n-support-base-path/pages/gsp/fallback/[slug].js b/test/e2e/i18n-support-base-path/pages/gsp/fallback/[slug].js similarity index 100% rename from test/integration/i18n-support-base-path/pages/gsp/fallback/[slug].js rename to test/e2e/i18n-support-base-path/pages/gsp/fallback/[slug].js diff --git a/test/integration/i18n-support-base-path/pages/gsp/index.js b/test/e2e/i18n-support-base-path/pages/gsp/index.js similarity index 100% rename from test/integration/i18n-support-base-path/pages/gsp/index.js rename to test/e2e/i18n-support-base-path/pages/gsp/index.js diff --git a/test/integration/i18n-support-base-path/pages/gsp/no-fallback/[slug].js b/test/e2e/i18n-support-base-path/pages/gsp/no-fallback/[slug].js similarity index 100% rename from test/integration/i18n-support-base-path/pages/gsp/no-fallback/[slug].js rename to test/e2e/i18n-support-base-path/pages/gsp/no-fallback/[slug].js diff --git a/test/integration/i18n-support-base-path/pages/gssp/[slug].js b/test/e2e/i18n-support-base-path/pages/gssp/[slug].js similarity index 100% rename from test/integration/i18n-support-base-path/pages/gssp/[slug].js rename to test/e2e/i18n-support-base-path/pages/gssp/[slug].js diff --git a/test/integration/i18n-support-base-path/pages/gssp/index.js b/test/e2e/i18n-support-base-path/pages/gssp/index.js similarity index 100% rename from test/integration/i18n-support-base-path/pages/gssp/index.js rename to test/e2e/i18n-support-base-path/pages/gssp/index.js diff --git a/test/integration/i18n-support-base-path/pages/index.js b/test/e2e/i18n-support-base-path/pages/index.js similarity index 100% rename from test/integration/i18n-support-base-path/pages/index.js rename to test/e2e/i18n-support-base-path/pages/index.js diff --git a/test/integration/i18n-support-base-path/pages/links.js b/test/e2e/i18n-support-base-path/pages/links.js similarity index 100% rename from test/integration/i18n-support-base-path/pages/links.js rename to test/e2e/i18n-support-base-path/pages/links.js diff --git a/test/integration/i18n-support-base-path/pages/locale-false.js b/test/e2e/i18n-support-base-path/pages/locale-false.js similarity index 100% rename from test/integration/i18n-support-base-path/pages/locale-false.js rename to test/e2e/i18n-support-base-path/pages/locale-false.js diff --git a/test/integration/i18n-support-base-path/pages/mixed.js b/test/e2e/i18n-support-base-path/pages/mixed.js similarity index 100% rename from test/integration/i18n-support-base-path/pages/mixed.js rename to test/e2e/i18n-support-base-path/pages/mixed.js diff --git a/test/integration/i18n-support-base-path/pages/not-found/blocking-fallback/[slug].js b/test/e2e/i18n-support-base-path/pages/not-found/blocking-fallback/[slug].js similarity index 100% rename from test/integration/i18n-support-base-path/pages/not-found/blocking-fallback/[slug].js rename to test/e2e/i18n-support-base-path/pages/not-found/blocking-fallback/[slug].js diff --git a/test/integration/i18n-support-base-path/pages/not-found/fallback/[slug].js b/test/e2e/i18n-support-base-path/pages/not-found/fallback/[slug].js similarity index 100% rename from test/integration/i18n-support-base-path/pages/not-found/fallback/[slug].js rename to test/e2e/i18n-support-base-path/pages/not-found/fallback/[slug].js diff --git a/test/integration/i18n-support-base-path/pages/not-found/index.js b/test/e2e/i18n-support-base-path/pages/not-found/index.js similarity index 100% rename from test/integration/i18n-support-base-path/pages/not-found/index.js rename to test/e2e/i18n-support-base-path/pages/not-found/index.js diff --git a/test/integration/i18n-support-base-path/public/files/texts/file.txt b/test/e2e/i18n-support-base-path/public/files/texts/file.txt similarity index 100% rename from test/integration/i18n-support-base-path/public/files/texts/file.txt rename to test/e2e/i18n-support-base-path/public/files/texts/file.txt diff --git a/test/integration/i18n-support-catchall/test/index.test.ts b/test/e2e/i18n-support-catchall/i18n-support-catchall.test.ts similarity index 70% rename from test/integration/i18n-support-catchall/test/index.test.ts rename to test/e2e/i18n-support-catchall/i18n-support-catchall.test.ts index 27a1d4270ce1..d4d52cab9702 100644 --- a/test/integration/i18n-support-catchall/test/index.test.ts +++ b/test/e2e/i18n-support-catchall/i18n-support-catchall.test.ts @@ -1,44 +1,21 @@ -/* eslint-env jest */ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' import assert from 'assert' -import fs from 'fs-extra' -import cheerio from 'cheerio' +import fs from 'fs' import { join } from 'path' -import webdriver from 'next-webdriver' -import { - fetchViaHTTP, - findPort, - killApp, - launchApp, - nextBuild, - nextStart, - check, -} from 'next-test-utils' - -const appDir = join(__dirname, '../') -let app -let appPort -let buildPagesDir - -const locales = ['en-US', 'nl-NL', 'nl-BE', 'nl', 'fr-BE', 'fr', 'en'] - -function runTests(isDev: boolean) { - if (!isDev) { - it('should output prerendered index routes correctly', async () => { - expect(fs.existsSync(join(buildPagesDir, 'pages/en-US.html'))).toBe(true) - expect(fs.existsSync(join(buildPagesDir, 'pages/en-US.json'))).toBe(true) - expect(fs.existsSync(join(buildPagesDir, 'pages/fr.html'))).toBe(true) - expect(fs.existsSync(join(buildPagesDir, 'pages/fr.json'))).toBe(true) - }) - } + +describe('i18n Support Root Catch-all', () => { + const { next, isNextStart } = nextTestSetup({ + files: __dirname, + }) + + const locales = ['en-US', 'nl-NL', 'nl-BE', 'nl', 'fr-BE', 'fr', 'en'] it('should load the index route correctly SSR', async () => { - const res = await fetchViaHTTP(appPort, '/', undefined, { - redirect: 'manual', - }) + const res = await next.fetch('/', { redirect: 'manual' }) expect(res.status).toBe(200) - const html = await res.text() - const $ = cheerio.load(html) + const $ = await next.render$('/') expect($('#router-locale').text()).toBe('en-US') expect($('#router-default-locale').text()).toBe('en-US') @@ -54,7 +31,7 @@ function runTests(isDev: boolean) { }) it('should load the index route correctly CSR', async () => { - const browser = await webdriver(appPort, '/') + const browser = await next.browser('/') expect(await browser.elementByCss('#router-locale').text()).toBe('en-US') expect(await browser.elementByCss('#router-default-locale').text()).toBe( @@ -76,11 +53,13 @@ function runTests(isDev: boolean) { }) it('should navigate to other locale index and back', async () => { - const browser = await webdriver(appPort, '/') + const browser = await next.browser('/') await browser.elementByCss('#to-locale-index').click() - await check(() => browser.eval('window.location.pathname'), '/nl-NL') + await retry(async () => { + expect(await browser.eval('window.location.pathname')).toBe('/nl-NL') + }) expect(await browser.elementByCss('#router-locale').text()).toBe('nl-NL') expect(await browser.elementByCss('#router-default-locale').text()).toBe( @@ -102,9 +81,10 @@ function runTests(isDev: boolean) { await browser.back() - await check(() => browser.elementByCss('#router-locale').text(), 'en-US') + await retry(async () => { + expect(await browser.elementByCss('#router-locale').text()).toBe('en-US') + }) - expect(await browser.elementByCss('#router-locale').text()).toBe('en-US') expect(await browser.elementByCss('#router-default-locale').text()).toBe( 'en-US' ) @@ -124,16 +104,22 @@ function runTests(isDev: boolean) { }) it('should navigate to other locale page and back', async () => { - const browser = await webdriver(appPort, '/') + const browser = await next.browser('/') await browser.elementByCss('#to-locale-another').click() - await check( - () => browser.eval('window.location.pathname'), - '/nl-NL/another' - ) + await retry(async () => { + expect(await browser.eval('window.location.pathname')).toBe( + '/nl-NL/another' + ) + }) - expect(await browser.elementByCss('#router-locale').text()).toBe('nl-NL') + // The URL can update before the component re-renders with the new locale + // (observed with webpack production builds). Wait for the render to + // reflect the new locale before asserting the rest of the router state. + await retry(async () => { + expect(await browser.elementByCss('#router-locale').text()).toBe('nl-NL') + }) expect(await browser.elementByCss('#router-default-locale').text()).toBe( 'en-US' ) @@ -157,9 +143,10 @@ function runTests(isDev: boolean) { await browser.back() - await check(() => browser.elementByCss('#router-locale').text(), 'en-US') + await retry(async () => { + expect(await browser.elementByCss('#router-locale').text()).toBe('en-US') + }) - expect(await browser.elementByCss('#router-locale').text()).toBe('en-US') expect(await browser.elementByCss('#router-default-locale').text()).toBe( 'en-US' ) @@ -178,9 +165,17 @@ function runTests(isDev: boolean) { }) }) - if (!isDev) { + if (isNextStart) { + it('should output prerendered index routes correctly', async () => { + const buildPagesDir = join(next.testDir, '.next/server') + expect(fs.existsSync(join(buildPagesDir, 'pages/en-US.html'))).toBe(true) + expect(fs.existsSync(join(buildPagesDir, 'pages/en-US.json'))).toBe(true) + expect(fs.existsSync(join(buildPagesDir, 'pages/fr.html'))).toBe(true) + expect(fs.existsSync(join(buildPagesDir, 'pages/fr.json'))).toBe(true) + }) + it('should preload data correctly', async () => { - const browser = await webdriver(appPort, '/') + const browser = await next.browser('/') await browser.eval(`(function() { document.querySelector('#to-def-locale-index').scrollIntoView() @@ -191,12 +186,10 @@ function runTests(isDev: boolean) { document.querySelector('#to-fr-locale-index').scrollIntoView() })()`) - await check(async () => { + await retry(async () => { const hrefs = await browser.eval(`Object.keys(window.next.router.sdc)`) hrefs.sort() - console.log({ hrefs }) - assert.deepEqual( hrefs.map((href) => new URL(href).pathname.replace(/^\/_next\/data\/[^/]+/, '') @@ -210,39 +203,7 @@ function runTests(isDev: boolean) { '/nl-NL/another.json', ] ) - return 'yes' - }, 'yes') + }) }) } -} - -describe('i18n Support Root Catch-all', () => { - ;(process.env.TURBOPACK_BUILD ? describe.skip : describe)( - 'development mode', - () => { - beforeAll(async () => { - await fs.remove(join(appDir, '.next')) - appPort = await findPort() - app = await launchApp(appDir, appPort) - }) - afterAll(() => killApp(app)) - - runTests(true) - } - ) - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( - 'production mode', - () => { - beforeAll(async () => { - await fs.remove(join(appDir, '.next')) - await nextBuild(appDir) - appPort = await findPort() - app = await nextStart(appDir, appPort) - buildPagesDir = join(appDir, '.next/server') - }) - afterAll(() => killApp(app)) - - runTests(false) - } - ) }) diff --git a/test/integration/i18n-support-catchall/next.config.js b/test/e2e/i18n-support-catchall/next.config.js similarity index 100% rename from test/integration/i18n-support-catchall/next.config.js rename to test/e2e/i18n-support-catchall/next.config.js diff --git a/test/integration/i18n-support-catchall/pages/[[...slug]].js b/test/e2e/i18n-support-catchall/pages/[[...slug]].js similarity index 100% rename from test/integration/i18n-support-catchall/pages/[[...slug]].js rename to test/e2e/i18n-support-catchall/pages/[[...slug]].js diff --git a/test/integration/i18n-support-custom-error/test/index.test.ts b/test/e2e/i18n-support-custom-error/i18n-support-custom-error.test.ts similarity index 55% rename from test/integration/i18n-support-custom-error/test/index.test.ts rename to test/e2e/i18n-support-custom-error/i18n-support-custom-error.test.ts index db93b515acd2..d15581310be4 100644 --- a/test/integration/i18n-support-custom-error/test/index.test.ts +++ b/test/e2e/i18n-support-custom-error/i18n-support-custom-error.test.ts @@ -1,25 +1,19 @@ -/* eslint-env jest */ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' -import { join } from 'path' -import webdriver from 'next-webdriver' -import { - launchApp, - killApp, - findPort, - nextBuild, - nextStart, -} from 'next-test-utils' +describe('Custom routes i18n custom error', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + // Assertions don't apply to deploy mode (output differs vs. local Next.js server). + skipDeployment: true, + }) + if (skipped) return -const appDir = join(__dirname, '..') -const locales = ['en', 'fr', 'de', 'it'] -let appPort -let app + const locales = ['en', 'fr', 'de', 'it'] -const runTests = () => { it('should localized [slug] routes render correctly', async () => { for (const locale of locales) { - const browser = await webdriver( - appPort, + const browser = await next.browser( `${locale === 'en' ? '' : `/${locale}`}/my-custom-path-1` ) @@ -35,8 +29,7 @@ const runTests = () => { it('handle custom http status maintaining locale props in custom _error page', async () => { for (const locale of locales) { - const browser = await webdriver( - appPort, + const browser = await next.browser( `${locale === 'en' ? '' : `/${locale}`}/my-custom-gone-path` ) @@ -53,8 +46,7 @@ const runTests = () => { it('handle default http status maintaining locale props in custom _error page', async () => { for (const locale of locales) { - const browser = await webdriver( - appPort, + const browser = await next.browser( `${locale === 'en' ? '' : `/${locale}`}/my-custom-gone-path/other-path` ) @@ -71,8 +63,7 @@ const runTests = () => { it('should work also on client side routing', async () => { for (const locale of locales) { - const browser = await webdriver( - appPort, + const browser = await next.browser( `${locale === 'en' ? '' : `/${locale}`}/my-custom-path-1` ) @@ -86,49 +77,27 @@ const runTests = () => { await browser.eval('window.next.router.push("/my-custom-path-2")') - expect(JSON.parse(await browser.elementByCss('#props').text())).toEqual( - expect.objectContaining({ - locale, - params: { slug: 'my-custom-path-2' }, - title: 'my-custom-path-2', - }) - ) + await retry(async () => { + expect(JSON.parse(await browser.elementByCss('#props').text())).toEqual( + expect.objectContaining({ + locale, + params: { slug: 'my-custom-path-2' }, + title: 'my-custom-path-2', + }) + ) + }) await browser.eval('window.next.router.push("/my-custom-gone-path")') - expect( - JSON.parse(await browser.elementByCss('#error-props').text()) - ).toEqual( - expect.objectContaining({ - locale, - }) - ) - } - }) -} - -describe('Custom routes i18n custom error', () => { - ;(process.env.TURBOPACK_BUILD ? describe.skip : describe)( - 'development mode', - () => { - beforeAll(async () => { - appPort = await findPort() - app = await launchApp(appDir, appPort) + await retry(async () => { + expect( + JSON.parse(await browser.elementByCss('#error-props').text()) + ).toEqual( + expect.objectContaining({ + locale, + }) + ) }) - afterAll(() => killApp(app)) - runTests() } - ) - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( - 'production mode', - () => { - beforeAll(async () => { - await nextBuild(appDir) - appPort = await findPort() - app = await nextStart(appDir, appPort) - }) - afterAll(() => killApp(app)) - runTests() - } - ) + }) }) diff --git a/test/integration/i18n-support-custom-error/next.config.js b/test/e2e/i18n-support-custom-error/next.config.js similarity index 100% rename from test/integration/i18n-support-custom-error/next.config.js rename to test/e2e/i18n-support-custom-error/next.config.js diff --git a/test/integration/i18n-support-custom-error/pages/[slug].js b/test/e2e/i18n-support-custom-error/pages/[slug].js similarity index 100% rename from test/integration/i18n-support-custom-error/pages/[slug].js rename to test/e2e/i18n-support-custom-error/pages/[slug].js diff --git a/test/integration/i18n-support-custom-error/pages/_error.js b/test/e2e/i18n-support-custom-error/pages/_error.js similarity index 100% rename from test/integration/i18n-support-custom-error/pages/_error.js rename to test/e2e/i18n-support-custom-error/pages/_error.js diff --git a/test/integration/i18n-support-custom-error/pages/index.js b/test/e2e/i18n-support-custom-error/pages/index.js similarity index 100% rename from test/integration/i18n-support-custom-error/pages/index.js rename to test/e2e/i18n-support-custom-error/pages/index.js diff --git a/test/e2e/i18n-support-fallback-rewrite-legacy/i18n-support-fallback-rewrite-legacy.test.ts b/test/e2e/i18n-support-fallback-rewrite-legacy/i18n-support-fallback-rewrite-legacy.test.ts new file mode 100644 index 000000000000..41b59425b5cb --- /dev/null +++ b/test/e2e/i18n-support-fallback-rewrite-legacy/i18n-support-fallback-rewrite-legacy.test.ts @@ -0,0 +1,79 @@ +import url from 'url' +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('i18n Support Fallback Rewrite Legacy', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should not rewrite for index page', async () => { + for (const [pathname, query] of [ + ['/', {}], + ['/en', {}], + ['/fr', {}], + ['/', { hello: 'world' }], + ['/en', { hello: 'world' }], + ['/fr', { hello: 'world' }], + ] as const) { + const asPath = url.format({ pathname, query }) + const browser = await next.browser(asPath) + + expect(JSON.parse(await browser.elementByCss('#router').text())).toEqual({ + index: true, + pathname: '/', + asPath: url.format({ pathname: '/', query }), + query, + }) + + await retry(async () => { + expect( + JSON.parse(await browser.elementByCss('#router').text()) + ).toEqual({ + index: true, + pathname: '/', + asPath: url.format({ pathname: '/', query }), + query, + }) + }) + } + }) + + it('should not rewrite for dynamic page', async () => { + for (const [pathname, query] of [ + ['/dynamic/first', {}], + ['/en/dynamic/first', {}], + ['/fr/dynamic/first', {}], + ['/dynamic/first', { hello: 'world' }], + ['/en/dynamic/first', { hello: 'world' }], + ['/fr/dynamic/first', { hello: 'world' }], + ] as const) { + const asPath = url.format({ pathname, query }) + const browser = await next.browser(asPath) + + expect(JSON.parse(await browser.elementByCss('#router').text())).toEqual({ + dynamic: true, + pathname: '/dynamic/[slug]', + asPath: url.format({ pathname: '/dynamic/first', query }), + query: { + ...query, + slug: 'first', + }, + }) + + await retry(async () => { + expect( + JSON.parse(await browser.elementByCss('#router').text()) + ).toEqual({ + dynamic: true, + pathname: '/dynamic/[slug]', + asPath: url.format({ pathname: '/dynamic/first', query }), + query: { + ...query, + slug: 'first', + }, + }) + }) + } + }) +}) diff --git a/test/integration/i18n-support-fallback-rewrite-legacy/next.config.js b/test/e2e/i18n-support-fallback-rewrite-legacy/next.config.js similarity index 100% rename from test/integration/i18n-support-fallback-rewrite-legacy/next.config.js rename to test/e2e/i18n-support-fallback-rewrite-legacy/next.config.js diff --git a/test/integration/i18n-support-fallback-rewrite-legacy/pages/another.js b/test/e2e/i18n-support-fallback-rewrite-legacy/pages/another.js similarity index 100% rename from test/integration/i18n-support-fallback-rewrite-legacy/pages/another.js rename to test/e2e/i18n-support-fallback-rewrite-legacy/pages/another.js diff --git a/test/integration/i18n-support-fallback-rewrite-legacy/pages/dynamic/[slug].js b/test/e2e/i18n-support-fallback-rewrite-legacy/pages/dynamic/[slug].js similarity index 100% rename from test/integration/i18n-support-fallback-rewrite-legacy/pages/dynamic/[slug].js rename to test/e2e/i18n-support-fallback-rewrite-legacy/pages/dynamic/[slug].js diff --git a/test/integration/i18n-support-fallback-rewrite-legacy/pages/index.js b/test/e2e/i18n-support-fallback-rewrite-legacy/pages/index.js similarity index 100% rename from test/integration/i18n-support-fallback-rewrite-legacy/pages/index.js rename to test/e2e/i18n-support-fallback-rewrite-legacy/pages/index.js diff --git a/test/e2e/i18n-support-fallback-rewrite/i18n-support-fallback-rewrite.test.ts b/test/e2e/i18n-support-fallback-rewrite/i18n-support-fallback-rewrite.test.ts new file mode 100644 index 000000000000..28383f71e829 --- /dev/null +++ b/test/e2e/i18n-support-fallback-rewrite/i18n-support-fallback-rewrite.test.ts @@ -0,0 +1,79 @@ +import url from 'url' +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('i18n Support Fallback Rewrite', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should not rewrite for index page', async () => { + for (const [pathname, query] of [ + ['/', {}], + ['/en', {}], + ['/fr', {}], + ['/', { hello: 'world' }], + ['/en', { hello: 'world' }], + ['/fr', { hello: 'world' }], + ] as const) { + const asPath = url.format({ pathname, query }) + const browser = await next.browser(asPath) + + expect(JSON.parse(await browser.elementByCss('#router').text())).toEqual({ + index: true, + pathname: '/', + asPath: url.format({ pathname: '/', query }), + query, + }) + + await retry(async () => { + expect( + JSON.parse(await browser.elementByCss('#router').text()) + ).toEqual({ + index: true, + pathname: '/', + asPath: url.format({ pathname: '/', query }), + query, + }) + }) + } + }) + + it('should not rewrite for dynamic page', async () => { + for (const [pathname, query] of [ + ['/dynamic/first', {}], + ['/en/dynamic/first', {}], + ['/fr/dynamic/first', {}], + ['/dynamic/first', { hello: 'world' }], + ['/en/dynamic/first', { hello: 'world' }], + ['/fr/dynamic/first', { hello: 'world' }], + ] as const) { + const asPath = url.format({ pathname, query }) + const browser = await next.browser(asPath) + + expect(JSON.parse(await browser.elementByCss('#router').text())).toEqual({ + dynamic: true, + pathname: '/dynamic/[slug]', + asPath: url.format({ pathname: '/dynamic/first', query }), + query: { + ...query, + slug: 'first', + }, + }) + + await retry(async () => { + expect( + JSON.parse(await browser.elementByCss('#router').text()) + ).toEqual({ + dynamic: true, + pathname: '/dynamic/[slug]', + asPath: url.format({ pathname: '/dynamic/first', query }), + query: { + ...query, + slug: 'first', + }, + }) + }) + } + }) +}) diff --git a/test/integration/i18n-support-fallback-rewrite/next.config.js b/test/e2e/i18n-support-fallback-rewrite/next.config.js similarity index 100% rename from test/integration/i18n-support-fallback-rewrite/next.config.js rename to test/e2e/i18n-support-fallback-rewrite/next.config.js diff --git a/test/integration/i18n-support-fallback-rewrite/pages/another.js b/test/e2e/i18n-support-fallback-rewrite/pages/another.js similarity index 100% rename from test/integration/i18n-support-fallback-rewrite/pages/another.js rename to test/e2e/i18n-support-fallback-rewrite/pages/another.js diff --git a/test/integration/i18n-support-fallback-rewrite/pages/dynamic/[slug].js b/test/e2e/i18n-support-fallback-rewrite/pages/dynamic/[slug].js similarity index 100% rename from test/integration/i18n-support-fallback-rewrite/pages/dynamic/[slug].js rename to test/e2e/i18n-support-fallback-rewrite/pages/dynamic/[slug].js diff --git a/test/integration/i18n-support-fallback-rewrite/pages/index.js b/test/e2e/i18n-support-fallback-rewrite/pages/index.js similarity index 100% rename from test/integration/i18n-support-fallback-rewrite/pages/index.js rename to test/e2e/i18n-support-fallback-rewrite/pages/index.js diff --git a/test/integration/i18n-support-index-rewrite/test/index.test.ts b/test/e2e/i18n-support-index-rewrite/i18n-support-index-rewrite.test.ts similarity index 52% rename from test/integration/i18n-support-index-rewrite/test/index.test.ts rename to test/e2e/i18n-support-index-rewrite/i18n-support-index-rewrite.test.ts index 9f42e27aa948..527e27eaae89 100644 --- a/test/integration/i18n-support-index-rewrite/test/index.test.ts +++ b/test/e2e/i18n-support-index-rewrite/i18n-support-index-rewrite.test.ts @@ -1,31 +1,18 @@ -/* eslint-env jest */ - -import { join } from 'path' import assert from 'assert' import cheerio from 'cheerio' -import webdriver from 'next-webdriver' -import { - launchApp, - killApp, - findPort, - nextBuild, - nextStart, - renderViaHTTP, - check, -} from 'next-test-utils' +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('Custom routes i18n support index rewrite', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) -const appDir = join(__dirname, '..') -const locales = ['nl-NL', 'nl-BE', 'nl', 'fr-BE', 'fr', 'en'] -let appPort -let app + const locales = ['nl-NL', 'nl-BE', 'nl', 'fr-BE', 'fr', 'en'] -const runTests = () => { it('should rewrite index route correctly', async () => { for (const locale of locales) { - const html = await renderViaHTTP( - appPort, - `/${locale === 'en' ? '' : locale}` - ) + const html = await next.render(`/${locale === 'en' ? '' : locale}`) const $ = cheerio.load(html) expect(JSON.parse($('#props').text())).toEqual({ @@ -40,8 +27,7 @@ const runTests = () => { it('should handle index rewrite on client correctly', async () => { for (const locale of locales) { - const browser = await webdriver( - appPort, + const browser = await next.browser( `${locale === 'en' ? '' : `/${locale}`}/hello` ) @@ -57,7 +43,7 @@ const runTests = () => { window.next.router.push('/') })()`) - await check(async () => { + await retry(async () => { const html = await browser.eval('document.documentElement.innerHTML') const props = JSON.parse(cheerio.load(html)('#props').text()) assert.deepEqual(props, { @@ -67,36 +53,9 @@ const runTests = () => { locale, hello: 'world', }) - return 'success' - }, 'success') + }) expect(await browser.eval('window.beforeNav')).toBe(1) } }) -} - -describe('Custom routes i18n support index rewrite', () => { - ;(process.env.TURBOPACK_BUILD ? describe.skip : describe)( - 'development mode', - () => { - beforeAll(async () => { - appPort = await findPort() - app = await launchApp(appDir, appPort) - }) - afterAll(() => killApp(app)) - runTests() - } - ) - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( - 'production mode', - () => { - beforeAll(async () => { - await nextBuild(appDir) - appPort = await findPort() - app = await nextStart(appDir, appPort) - }) - afterAll(() => killApp(app)) - runTests() - } - ) }) diff --git a/test/integration/i18n-support-index-rewrite/next.config.js b/test/e2e/i18n-support-index-rewrite/next.config.js similarity index 100% rename from test/integration/i18n-support-index-rewrite/next.config.js rename to test/e2e/i18n-support-index-rewrite/next.config.js diff --git a/test/integration/i18n-support-index-rewrite/pages/[...slug].js b/test/e2e/i18n-support-index-rewrite/pages/[...slug].js similarity index 100% rename from test/integration/i18n-support-index-rewrite/pages/[...slug].js rename to test/e2e/i18n-support-index-rewrite/pages/[...slug].js diff --git a/test/e2e/i18n-support-same-page-hash-change/i18n-support-same-page-hash-change.test.ts b/test/e2e/i18n-support-same-page-hash-change/i18n-support-same-page-hash-change.test.ts new file mode 100644 index 000000000000..988217df4322 --- /dev/null +++ b/test/e2e/i18n-support-same-page-hash-change/i18n-support-same-page-hash-change.test.ts @@ -0,0 +1,92 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('Hash changes i18n', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should update props on locale change with same hash', async () => { + const browser = await next.browser('/about#hash') + + await browser.elementByCss('#change-locale').click() + + await retry(async () => { + expect(await browser.eval('window.location.pathname')).toBe('/fr/about') + }) + await retry(async () => { + expect(await browser.eval('window.location.hash')).toBe('#hash') + }) + + expect(await browser.elementByCss('#router-locale').text()).toBe('fr') + expect(await browser.elementByCss('#props-locale').text()).toBe('fr') + + await browser.elementByCss('#change-locale').click() + + await retry(async () => { + expect(await browser.eval('window.location.pathname')).toBe('/about') + }) + await retry(async () => { + expect(await browser.eval('window.location.hash')).toBe('#hash') + }) + + expect(await browser.elementByCss('#router-locale').text()).toBe('en') + expect(await browser.elementByCss('#props-locale').text()).toBe('en') + }) + + it('should update props on locale change with same hash (dynamic page)', async () => { + const browser = await next.browser('/posts/a#hash') + + await browser.elementByCss('#change-locale').click() + + await retry(async () => { + expect(await browser.eval('window.location.pathname')).toBe('/fr/posts/a') + }) + await retry(async () => { + expect(await browser.eval('window.location.hash')).toBe('#hash') + }) + + // The URL can update before the component re-renders with the new locale + // (observed with webpack production builds). Wait for the render to + // reflect the new locale before asserting the remaining render state. + await retry(async () => { + expect(await browser.elementByCss('#router-locale').text()).toBe('fr') + }) + expect(await browser.elementByCss('#props-locale').text()).toBe('fr') + + await browser.elementByCss('#change-locale').click() + + await retry(async () => { + expect(await browser.eval('window.location.pathname')).toBe('/posts/a') + }) + await retry(async () => { + expect(await browser.eval('window.location.hash')).toBe('#hash') + }) + + await retry(async () => { + expect(await browser.elementByCss('#router-locale').text()).toBe('en') + }) + expect(await browser.elementByCss('#props-locale').text()).toBe('en') + }) + + it('should trigger hash change events', async () => { + const browser = await next.browser('/about#hash') + + await retry(async () => { + expect(await browser.eval('window.location.hash')).toBe('#hash') + }) + + await browser.elementByCss('#hash-change').click() + + await retry(async () => { + expect(await browser.eval('window.hashChangeStart')).toBe('yes') + }) + await retry(async () => { + expect(await browser.eval('window.hashChangeComplete')).toBe('yes') + }) + + await retry(async () => { + expect(await browser.eval('window.location.hash')).toBe('#newhash') + }) + }) +}) diff --git a/test/integration/i18n-support-same-page-hash-change/next.config.js b/test/e2e/i18n-support-same-page-hash-change/next.config.js similarity index 100% rename from test/integration/i18n-support-same-page-hash-change/next.config.js rename to test/e2e/i18n-support-same-page-hash-change/next.config.js diff --git a/test/integration/i18n-support-same-page-hash-change/pages/about.js b/test/e2e/i18n-support-same-page-hash-change/pages/about.js similarity index 100% rename from test/integration/i18n-support-same-page-hash-change/pages/about.js rename to test/e2e/i18n-support-same-page-hash-change/pages/about.js diff --git a/test/integration/i18n-support-same-page-hash-change/pages/posts/[...slug].js b/test/e2e/i18n-support-same-page-hash-change/pages/posts/[...slug].js similarity index 100% rename from test/integration/i18n-support-same-page-hash-change/pages/posts/[...slug].js rename to test/e2e/i18n-support-same-page-hash-change/pages/posts/[...slug].js diff --git a/test/integration/i18n-support/test/index.test.ts b/test/e2e/i18n-support/i18n-support.test.ts similarity index 55% rename from test/integration/i18n-support/test/index.test.ts rename to test/e2e/i18n-support/i18n-support.test.ts index 1265148d64a2..ea296ec83357 100644 --- a/test/integration/i18n-support/test/index.test.ts +++ b/test/e2e/i18n-support/i18n-support.test.ts @@ -4,216 +4,210 @@ import { join } from 'path' import cheerio from 'cheerio' import { runTests, locales, nonDomainLocales } from './shared' import webdriver from 'next-webdriver' -import { - nextBuild, - nextStart, - findPort, - killApp, - fetchViaHTTP, - File, - launchApp, - check, -} from 'next-test-utils' +import { findPort, fetchViaHTTP, retry } from 'next-test-utils' +import { nextTestSetup, isNextDev } from 'e2e-utils' import assert from 'assert' -const appDir = join(__dirname, '../') -const nextConfig = new File(join(appDir, 'next.config.js')) -const ctx: Record<string, any> = { - basePath: '', - appDir, -} - describe('i18n Support', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + if (skipped) return + + const ctx: Record<string, any> = { + basePath: '', + isDev: isNextDev, + } + + let externalServer: http.Server + let externalPort: number + let origConfigContent: string + beforeAll(async () => { - ctx.externalPort = await findPort() - ctx.externalApp = http.createServer((req, res) => { + externalPort = await findPort() + externalServer = http.createServer((req, res) => { res.statusCode = 200 res.end(JSON.stringify({ url: req.url, external: true })) }) await new Promise<void>((resolve, reject) => { - ctx.externalApp.listen(ctx.externalPort, (err) => + externalServer.listen(externalPort, (err?: Error) => err ? reject(err) : resolve() ) }) + + await next.patchFile('next.config.js', (content) => + content.replace(/__EXTERNAL_PORT__/g, String(externalPort)) + ) + origConfigContent = await next.readFile('next.config.js') + + if (!isNextDev) { + await next.build() + } + await next.start() + + ctx.appDir = next.testDir + ctx.appPort = new URL(next.url).port + if (!isNextDev) { + ctx.buildId = (await next.readFile('.next/BUILD_ID')).trim() + ctx.buildPagesDir = join(next.testDir, '.next/server/pages') + } else { + ctx.buildId = 'development' + } + }) + + afterAll(() => { + externalServer?.close() }) - afterAll(() => ctx.externalApp.close()) - ;(process.env.TURBOPACK_BUILD ? describe.skip : describe)( - 'development mode', - () => { - const curCtx: Record<string, any> = { - ...ctx, - isDev: true, + + runTests(ctx) + + if (!isNextDev) { + it('should have pre-rendered /500 correctly', async () => { + for (const locale of locales) { + const content = await fs.readFile( + join(next.testDir, '.next/server/pages/', locale, '500.html'), + 'utf8' + ) + expect(content).toContain('500') + expect(content).toMatch(/Internal Server Error/i) } - beforeAll(async () => { - await fs.remove(join(appDir, '.next')) - nextConfig.replace(/__EXTERNAL_PORT__/g, ctx.externalPort) - curCtx.appPort = await findPort() - curCtx.app = await launchApp(appDir, curCtx.appPort) - curCtx.buildId = 'development' - }) - afterAll(async () => { - await killApp(curCtx.app) - nextConfig.restore() - }) + }) + } - runTests(curCtx) - } - ) - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( - 'production mode', - () => { + describe('with localeDetection disabled', () => { + if (!isNextDev) { beforeAll(async () => { - await fs.remove(join(appDir, '.next')) - nextConfig.replace(/__EXTERNAL_PORT__/g, ctx.externalPort) - await nextBuild(appDir) - ctx.appPort = await findPort() - ctx.app = await nextStart(appDir, ctx.appPort) - ctx.buildPagesDir = join(appDir, '.next/server/pages') - ctx.buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') + await next.stop() + await next.patchFile('next.config.js', (content) => + content.replace('// localeDetection', 'localeDetection') + ) + await next.build() + await next.start() + ctx.appPort = new URL(next.url).port }) + afterAll(async () => { - await killApp(ctx.app) - nextConfig.restore() + await next.stop() + await next.patchFile('next.config.js', origConfigContent) + await next.build() + await next.start() + ctx.appPort = new URL(next.url).port }) - runTests(ctx) - - it('should have pre-rendered /500 correctly', async () => { - for (const locale of locales) { - const content = await fs.readFile( - join(appDir, '.next/server/pages/', locale, '500.html'), - 'utf8' - ) - expect(content).toContain('500') - expect(content).toMatch(/Internal Server Error/i) - } - }) - } - ) + it('should have localeDetection in routes-manifest', async () => { + const routesManifest = JSON.parse( + await next.readFile('.next/routes-manifest.json') + ) - describe('with localeDetection disabled', () => { - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( - 'production mode', - () => { - beforeAll(async () => { - await fs.remove(join(appDir, '.next')) - nextConfig.replace('// localeDetection', 'localeDetection') - - await nextBuild(appDir) - ctx.appPort = await findPort() - ctx.app = await nextStart(appDir, ctx.appPort) - }) - afterAll(async () => { - nextConfig.restore() - await killApp(ctx.app) + expect(routesManifest.i18n).toEqual({ + localeDetection: false, + locales: [ + 'en-US', + 'nl-NL', + 'nl-BE', + 'nl', + 'fr-BE', + 'fr', + 'en', + 'go', + 'go-BE', + 'do', + 'do-BE', + ], + defaultLocale: 'en-US', + domains: [ + { + http: true, + domain: 'example.do', + defaultLocale: 'do', + locales: ['do-BE'], + }, + { + domain: 'example.com', + defaultLocale: 'go', + locales: ['go-BE'], + }, + ], }) + }) - it('should have localeDetection in routes-manifest', async () => { - const routesManifest = await fs.readJSON( - join(appDir, '.next/routes-manifest.json') - ) + it('should not detect locale from accept-language', async () => { + const res = await fetchViaHTTP( + ctx.appPort, + '/', + {}, + { + redirect: 'manual', + headers: { + 'accept-language': 'fr', + }, + } + ) - expect(routesManifest.i18n).toEqual({ - localeDetection: false, - locales: [ - 'en-US', - 'nl-NL', - 'nl-BE', - 'nl', - 'fr-BE', - 'fr', - 'en', - 'go', - 'go-BE', - 'do', - 'do-BE', - ], - defaultLocale: 'en-US', - domains: [ - { - http: true, - domain: 'example.do', - defaultLocale: 'do', - locales: ['do-BE'], - }, - { - domain: 'example.com', - defaultLocale: 'go', - locales: ['go-BE'], - }, - ], - }) - }) + expect(res.status).toBe(200) + const $ = cheerio.load(await res.text()) + expect($('html').attr('lang')).toBe('en-US') + expect($('#router-locale').text()).toBe('en-US') + expect(JSON.parse($('#router-locales').text())).toEqual(locales) + expect($('#router-pathname').text()).toBe('/') + expect($('#router-as-path').text()).toBe('/') + }) - it('should not detect locale from accept-language', async () => { - const res = await fetchViaHTTP( - ctx.appPort, - '/', - {}, - { - redirect: 'manual', - headers: { - 'accept-language': 'fr', - }, - } - ) + it('should ignore the invalid accept-language header', async () => { + await next.patchFile('next.config.js', (content) => + content.replace('localeDetection: false', 'localeDetection: true') + ) + await next.stop() + await next.build() + await next.start() + ctx.appPort = new URL(next.url).port + + const res = await fetchViaHTTP( + ctx.appPort, + '/', + {}, + { + headers: { + 'accept-language': 'ldfir;', + }, + } + ) - expect(res.status).toBe(200) - const $ = cheerio.load(await res.text()) - expect($('html').attr('lang')).toBe('en-US') - expect($('#router-locale').text()).toBe('en-US') - expect(JSON.parse($('#router-locales').text())).toEqual(locales) - expect($('#router-pathname').text()).toBe('/') - expect($('#router-as-path').text()).toBe('/') - }) + expect(res.status).toBe(200) + const $ = cheerio.load(await res.text()) + expect($('html').attr('lang')).toBe('en-US') + expect($('#router-locale').text()).toBe('en-US') + expect(JSON.parse($('#router-locales').text())).toEqual(locales) + expect($('#router-pathname').text()).toBe('/') + expect($('#router-as-path').text()).toBe('/') + }) - it('should ignore the invalid accept-language header', async () => { - nextConfig.replace('localeDetection: false', 'localeDetection: true') + it('should set locale from detected path', async () => { + for (const locale of nonDomainLocales) { const res = await fetchViaHTTP( ctx.appPort, - '/', + `/${locale}`, {}, { + redirect: 'manual', headers: { - 'accept-language': 'ldfir;', + 'accept-language': 'en-US,en;q=0.9', }, } ) expect(res.status).toBe(200) const $ = cheerio.load(await res.text()) - expect($('html').attr('lang')).toBe('en-US') - expect($('#router-locale').text()).toBe('en-US') + expect($('html').attr('lang')).toBe(locale) + expect($('#router-locale').text()).toBe(locale) expect(JSON.parse($('#router-locales').text())).toEqual(locales) expect($('#router-pathname').text()).toBe('/') expect($('#router-as-path').text()).toBe('/') - }) - - it('should set locale from detected path', async () => { - for (const locale of nonDomainLocales) { - const res = await fetchViaHTTP( - ctx.appPort, - `/${locale}`, - {}, - { - redirect: 'manual', - headers: { - 'accept-language': 'en-US,en;q=0.9', - }, - } - ) - - expect(res.status).toBe(200) - const $ = cheerio.load(await res.text()) - expect($('html').attr('lang')).toBe(locale) - expect($('#router-locale').text()).toBe(locale) - expect(JSON.parse($('#router-locales').text())).toEqual(locales) - expect($('#router-pathname').text()).toBe('/') - expect($('#router-as-path').text()).toBe('/') - } - }) - } - ) + } + }) + } }) describe('with trailingSlash: true', () => { @@ -231,7 +225,7 @@ describe('i18n Support', () => { document.querySelector('#to-gsp-fr').scrollIntoView() })()`) - await check(async () => { + await retry(async () => { const hrefs = await browser.eval( `Object.keys(window.next.router.sdc)` ) @@ -246,8 +240,7 @@ describe('i18n Support', () => { ), ['/en-US/gsp.json', '/fr.json', '/fr/gsp.json', '/nl-NL/gsp.json'] ) - return 'yes' - }, 'yes') + }) }) it('should have correct locale domain hrefs', async () => { @@ -425,50 +418,24 @@ describe('i18n Support', () => { }) } - ;(process.env.TURBOPACK_BUILD ? describe.skip : describe)( - 'development mode', - () => { - const curCtx: Record<string, any> = { - ...ctx, - isDev: true, - } - beforeAll(async () => { - await fs.remove(join(appDir, '.next')) - nextConfig.replace('// trailingSlash', 'trailingSlash') - - curCtx.appPort = await findPort() - curCtx.app = await launchApp(appDir, curCtx.appPort) - }) - afterAll(async () => { - nextConfig.restore() - await killApp(curCtx.app) - }) - - runSlashTests(curCtx) + beforeAll(async () => { + await next.stop() + await next.patchFile('next.config.js', (content) => + content.replace('// trailingSlash', 'trailingSlash') + ) + if (!isNextDev) { + await next.build() } - ) - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( - 'production mode', - () => { - const curCtx = { - ...ctx, - } - beforeAll(async () => { - await fs.remove(join(appDir, '.next')) - nextConfig.replace('// trailingSlash', 'trailingSlash') + await next.start() + ctx.appPort = new URL(next.url).port + }) - await nextBuild(appDir) - curCtx.appPort = await findPort() - curCtx.app = await nextStart(appDir, curCtx.appPort) - }) - afterAll(async () => { - nextConfig.restore() - await killApp(curCtx.app) - }) + afterAll(async () => { + await next.stop() + await next.patchFile('next.config.js', origConfigContent) + }) - runSlashTests(curCtx) - } - ) + runSlashTests(ctx) }) describe('with trailingSlash: false', () => { @@ -497,54 +464,34 @@ describe('i18n Support', () => { }) } - ;(process.env.TURBOPACK_BUILD ? describe.skip : describe)( - 'development mode', - () => { - const curCtx: Record<string, any> = { - ...ctx, - isDev: true, - } - beforeAll(async () => { - await fs.remove(join(appDir, '.next')) - nextConfig.replace('// trailingSlash: true', 'trailingSlash: false') - - curCtx.appPort = await findPort() - curCtx.app = await launchApp(appDir, curCtx.appPort) - }) - afterAll(async () => { - nextConfig.restore() - await killApp(curCtx.app) - }) - - runSlashTests(curCtx) + beforeAll(async () => { + await next.stop() + await next.patchFile('next.config.js', (content) => + content.replace('// trailingSlash: true', 'trailingSlash: false') + ) + if (!isNextDev) { + await next.build() } - ) - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( - 'production mode', - () => { - const curCtx = { ...ctx } - beforeAll(async () => { - await fs.remove(join(appDir, '.next')) - nextConfig.replace('// trailingSlash: true', 'trailingSlash: false') - - await nextBuild(appDir) - curCtx.appPort = await findPort() - curCtx.app = await nextStart(appDir, curCtx.appPort) - }) - afterAll(async () => { - nextConfig.restore() - await killApp(curCtx.app) - }) + await next.start() + ctx.appPort = new URL(next.url).port + }) - runSlashTests(curCtx) - } - ) + afterAll(async () => { + await next.stop() + await next.patchFile('next.config.js', origConfigContent) + }) + + runSlashTests(ctx) }) - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( - 'production mode', - () => { + + if (!isNextDev) { + describe('error configs', () => { it('should show proper error for duplicate defaultLocales', async () => { - nextConfig.write(` + await next.stop() + const origContent = await next.readFile('next.config.js') + await next.patchFile( + 'next.config.js', + ` module.exports = { i18n: { locales: ['en', 'fr', 'nl'], @@ -565,41 +512,45 @@ describe('i18n Support', () => { ] } } - `) + ` + ) - const { code, stderr } = await nextBuild(appDir, undefined, { - stderr: true, - }) - nextConfig.restore() - expect(code).toBe(1) - expect(stderr).toContain( + const { exitCode } = await next.build() + expect(exitCode).toBe(1) + expect(next.cliOutput).toContain( 'Both fr.example.com and french.example.com configured the defaultLocale fr but only one can' ) + await next.patchFile('next.config.js', origContent) }) it('should show proper error for duplicate locales', async () => { - nextConfig.write(` + const origContent = await next.readFile('next.config.js') + await next.patchFile( + 'next.config.js', + ` module.exports = { i18n: { locales: ['en', 'fr', 'nl', 'eN', 'fr'], defaultLocale: 'en', } } - `) + ` + ) - const { code, stderr } = await nextBuild(appDir, undefined, { - stderr: true, - }) - nextConfig.restore() - expect(code).toBe(1) - expect(stderr).toContain( + const { exitCode } = await next.build() + expect(exitCode).toBe(1) + expect(next.cliOutput).toContain( 'Specified i18n.locales contains the following duplicate locales:' ) - expect(stderr).toContain(`eN, fr`) + expect(next.cliOutput).toContain(`eN, fr`) + await next.patchFile('next.config.js', origContent) }) it('should show proper error for invalid locale domain', async () => { - nextConfig.write(` + const origContent = await next.readFile('next.config.js') + await next.patchFile( + 'next.config.js', + ` module.exports = { i18n: { locales: ['en', 'fr', 'nl', 'eN', 'fr'], @@ -612,17 +563,16 @@ describe('i18n Support', () => { defaultLocale: 'en', } } - `) + ` + ) - const { code, stderr } = await nextBuild(appDir, undefined, { - stderr: true, - }) - nextConfig.restore() - expect(code).toBe(1) - expect(stderr).toContain( + const { exitCode } = await next.build() + expect(exitCode).toBe(1) + expect(next.cliOutput).toContain( `i18n domain: "hello:3000" is invalid it should be a valid domain without protocol (https://) or port (:3000) e.g. example.vercel.sh` ) + await next.patchFile('next.config.js', origContent) }) - } - ) + }) + } }) diff --git a/test/integration/i18n-support/next.config.js b/test/e2e/i18n-support/next.config.js similarity index 100% rename from test/integration/i18n-support/next.config.js rename to test/e2e/i18n-support/next.config.js diff --git a/test/integration/i18n-support/pages/404.js b/test/e2e/i18n-support/pages/404.js similarity index 100% rename from test/integration/i18n-support/pages/404.js rename to test/e2e/i18n-support/pages/404.js diff --git a/test/integration/i18n-support/pages/[post]/[comment].js b/test/e2e/i18n-support/pages/[post]/[comment].js similarity index 100% rename from test/integration/i18n-support/pages/[post]/[comment].js rename to test/e2e/i18n-support/pages/[post]/[comment].js diff --git a/test/integration/i18n-support/pages/[post]/index.js b/test/e2e/i18n-support/pages/[post]/index.js similarity index 100% rename from test/integration/i18n-support/pages/[post]/index.js rename to test/e2e/i18n-support/pages/[post]/index.js diff --git a/test/integration/i18n-support/pages/_app.js b/test/e2e/i18n-support/pages/_app.js similarity index 53% rename from test/integration/i18n-support/pages/_app.js rename to test/e2e/i18n-support/pages/_app.js index 51785b1cc8ca..b4030d42163c 100644 --- a/test/integration/i18n-support/pages/_app.js +++ b/test/e2e/i18n-support/pages/_app.js @@ -4,12 +4,23 @@ if (typeof window !== 'undefined') { const origWarn = window.console.warn const origError = window.console.error + const isHmrNoise = (msg) => + msg.includes('[HMR]') || + msg.includes('handleStaticIndicator') || + msg.includes('isrManifest') + window.console.warn = function (...args) { - window.caughtWarns.push(args.join(' ')) + const msg = args.join(' ') + if (!isHmrNoise(msg)) { + window.caughtWarns.push(msg) + } origWarn(...args) } window.console.error = function (...args) { - window.caughtWarns.push(args.join(' ')) + const msg = args.join(' ') + if (!isHmrNoise(msg)) { + window.caughtWarns.push(msg) + } origError(...args) } } diff --git a/test/integration/i18n-support/pages/another.js b/test/e2e/i18n-support/pages/another.js similarity index 100% rename from test/integration/i18n-support/pages/another.js rename to test/e2e/i18n-support/pages/another.js diff --git a/test/integration/i18n-support/pages/api/hello.js b/test/e2e/i18n-support/pages/api/hello.js similarity index 100% rename from test/integration/i18n-support/pages/api/hello.js rename to test/e2e/i18n-support/pages/api/hello.js diff --git a/test/integration/i18n-support/pages/api/post/[slug].js b/test/e2e/i18n-support/pages/api/post/[slug].js similarity index 100% rename from test/integration/i18n-support/pages/api/post/[slug].js rename to test/e2e/i18n-support/pages/api/post/[slug].js diff --git a/test/integration/i18n-support/pages/auto-export/index.js b/test/e2e/i18n-support/pages/auto-export/index.js similarity index 100% rename from test/integration/i18n-support/pages/auto-export/index.js rename to test/e2e/i18n-support/pages/auto-export/index.js diff --git a/test/integration/i18n-support/pages/developments/index.js b/test/e2e/i18n-support/pages/developments/index.js similarity index 100% rename from test/integration/i18n-support/pages/developments/index.js rename to test/e2e/i18n-support/pages/developments/index.js diff --git a/test/integration/i18n-support/pages/dynamic/[slug].js b/test/e2e/i18n-support/pages/dynamic/[slug].js similarity index 100% rename from test/integration/i18n-support/pages/dynamic/[slug].js rename to test/e2e/i18n-support/pages/dynamic/[slug].js diff --git a/test/integration/i18n-support/pages/frank.js b/test/e2e/i18n-support/pages/frank.js similarity index 100% rename from test/integration/i18n-support/pages/frank.js rename to test/e2e/i18n-support/pages/frank.js diff --git a/test/integration/i18n-support/pages/gsp/fallback/[slug].js b/test/e2e/i18n-support/pages/gsp/fallback/[slug].js similarity index 100% rename from test/integration/i18n-support/pages/gsp/fallback/[slug].js rename to test/e2e/i18n-support/pages/gsp/fallback/[slug].js diff --git a/test/integration/i18n-support/pages/gsp/index.js b/test/e2e/i18n-support/pages/gsp/index.js similarity index 100% rename from test/integration/i18n-support/pages/gsp/index.js rename to test/e2e/i18n-support/pages/gsp/index.js diff --git a/test/integration/i18n-support/pages/gsp/no-fallback/[slug].js b/test/e2e/i18n-support/pages/gsp/no-fallback/[slug].js similarity index 100% rename from test/integration/i18n-support/pages/gsp/no-fallback/[slug].js rename to test/e2e/i18n-support/pages/gsp/no-fallback/[slug].js diff --git a/test/integration/i18n-support/pages/gssp/[slug].js b/test/e2e/i18n-support/pages/gssp/[slug].js similarity index 100% rename from test/integration/i18n-support/pages/gssp/[slug].js rename to test/e2e/i18n-support/pages/gssp/[slug].js diff --git a/test/integration/i18n-support/pages/gssp/index.js b/test/e2e/i18n-support/pages/gssp/index.js similarity index 100% rename from test/integration/i18n-support/pages/gssp/index.js rename to test/e2e/i18n-support/pages/gssp/index.js diff --git a/test/integration/i18n-support/pages/index.js b/test/e2e/i18n-support/pages/index.js similarity index 100% rename from test/integration/i18n-support/pages/index.js rename to test/e2e/i18n-support/pages/index.js diff --git a/test/integration/i18n-support/pages/links.js b/test/e2e/i18n-support/pages/links.js similarity index 100% rename from test/integration/i18n-support/pages/links.js rename to test/e2e/i18n-support/pages/links.js diff --git a/test/integration/i18n-support/pages/locale-false.js b/test/e2e/i18n-support/pages/locale-false.js similarity index 100% rename from test/integration/i18n-support/pages/locale-false.js rename to test/e2e/i18n-support/pages/locale-false.js diff --git a/test/integration/i18n-support/pages/mixed.js b/test/e2e/i18n-support/pages/mixed.js similarity index 100% rename from test/integration/i18n-support/pages/mixed.js rename to test/e2e/i18n-support/pages/mixed.js diff --git a/test/integration/i18n-support/pages/not-found/blocking-fallback/[slug].js b/test/e2e/i18n-support/pages/not-found/blocking-fallback/[slug].js similarity index 100% rename from test/integration/i18n-support/pages/not-found/blocking-fallback/[slug].js rename to test/e2e/i18n-support/pages/not-found/blocking-fallback/[slug].js diff --git a/test/integration/i18n-support/pages/not-found/fallback/[slug].js b/test/e2e/i18n-support/pages/not-found/fallback/[slug].js similarity index 100% rename from test/integration/i18n-support/pages/not-found/fallback/[slug].js rename to test/e2e/i18n-support/pages/not-found/fallback/[slug].js diff --git a/test/integration/i18n-support/pages/not-found/index.js b/test/e2e/i18n-support/pages/not-found/index.js similarity index 100% rename from test/integration/i18n-support/pages/not-found/index.js rename to test/e2e/i18n-support/pages/not-found/index.js diff --git a/test/integration/i18n-support/public/files/texts/file.txt b/test/e2e/i18n-support/public/files/texts/file.txt similarity index 100% rename from test/integration/i18n-support/public/files/texts/file.txt rename to test/e2e/i18n-support/public/files/texts/file.txt diff --git a/test/integration/i18n-support/test/shared.ts b/test/e2e/i18n-support/shared.ts similarity index 99% rename from test/integration/i18n-support/test/shared.ts rename to test/e2e/i18n-support/shared.ts index 4600ef053908..9195211f2302 100644 --- a/test/integration/i18n-support/test/shared.ts +++ b/test/e2e/i18n-support/shared.ts @@ -12,7 +12,7 @@ import { renderViaHTTP, waitFor, normalizeRegEx, - check, + retry, getDeploymentId, } from 'next-test-utils' @@ -373,7 +373,11 @@ export function runTests(ctx) { ) })()`) - await check(() => browser.eval('window.location.hostname'), /example\.com/) + await retry(async () => { + expect(await browser.eval('window.location.hostname')).toMatch( + /example\.com/ + ) + }) expect(await browser.eval('window.location.pathname')).toBe( ctx.basePath || '/' ) @@ -394,7 +398,11 @@ export function runTests(ctx) { ) })()`) - await check(() => browser.eval('window.location.hostname'), /example\.com/) + await retry(async () => { + expect(await browser.eval('window.location.hostname')).toMatch( + /example\.com/ + ) + }) expect(await browser.eval('window.location.pathname')).toBe( `${ctx.basePath || ''}/go-BE/gssp` ) @@ -540,7 +548,9 @@ export function runTests(ctx) { ) })()`) - await check(() => browser.elementByCss('#router-locale').text(), 'nl') + await retry(async () => { + expect(await browser.elementByCss('#router-locale').text()).toBe('nl') + }) expect(await browser.eval('window.beforeNav')).toBe(1) await browser.eval(`(function() { @@ -549,7 +559,7 @@ export function runTests(ctx) { ) })()`) - await check(async () => { + await retry(async () => { const html = await browser.eval('document.documentElement.innerHTML') const props = JSON.parse(cheerio.load(html)('#props').text()) @@ -559,8 +569,7 @@ export function runTests(ctx) { defaultLocale: 'en-US', query: { page: '1' }, }) - return 'success' - }, 'success') + }) await browser .back() @@ -626,7 +635,7 @@ export function runTests(ctx) { document.querySelector('#to-gsp-default').scrollIntoView() })()`) - await check(async () => { + await retry(async () => { const hrefs = await browser.eval(`Object.keys(window.next.router.sdc)`) hrefs.sort() @@ -643,8 +652,7 @@ export function runTests(ctx) { '/fr/gsp/fallback/hello.json', ] ) - return 'yes' - }, 'yes') + }) } expect(await browser.eval('window.beforeNav')).toBe(1) @@ -671,7 +679,7 @@ export function runTests(ctx) { document.querySelector('#to-gsp-fr').scrollIntoView() })()`) - await check(async () => { + await retry(async () => { const hrefs = await browser.eval(`Object.keys(window.next.router.sdc)`) hrefs.sort() @@ -683,8 +691,7 @@ export function runTests(ctx) { ), ['/en-US/gsp.json', '/fr.json', '/fr/gsp.json', '/nl-NL/gsp.json'] ) - return 'yes' - }, 'yes') + }) }) } @@ -2159,7 +2166,7 @@ export function runTests(ctx) { document.querySelector('#to-no-fallback-first').scrollIntoView() })()`) - await check(async () => { + await retry(async () => { const hrefs = await browser.eval(`Object.keys(window.next.router.sdc)`) hrefs.sort() @@ -2175,8 +2182,7 @@ export function runTests(ctx) { '/fr/gsp/fallback/hello.json', ] ) - return 'yes' - }, 'yes') + }) } expect(await browser.elementByCss('#router-pathname').text()).toBe('/links') @@ -2374,7 +2380,7 @@ export function runTests(ctx) { document.querySelector('#to-no-fallback-first').scrollIntoView() })()`) - await check(async () => { + await retry(async () => { const hrefs = await browser.eval(`Object.keys(window.next.router.sdc)`) hrefs.sort() @@ -2391,8 +2397,7 @@ export function runTests(ctx) { '/fr/gsp/fallback/hello.json', ] ) - return 'yes' - }, 'yes') + }) } expect(await browser.elementByCss('#router-pathname').text()).toBe( diff --git a/test/integration/image-optimizer/app/.gitignore b/test/e2e/image-optimizer/app/.gitignore similarity index 100% rename from test/integration/image-optimizer/app/.gitignore rename to test/e2e/image-optimizer/app/.gitignore diff --git a/test/integration/image-optimizer/app/pages/api/application.svg.js b/test/e2e/image-optimizer/app/pages/api/application.svg.js similarity index 100% rename from test/integration/image-optimizer/app/pages/api/application.svg.js rename to test/e2e/image-optimizer/app/pages/api/application.svg.js diff --git a/test/integration/image-optimizer/app/pages/api/comma.svg.js b/test/e2e/image-optimizer/app/pages/api/comma.svg.js similarity index 100% rename from test/integration/image-optimizer/app/pages/api/comma.svg.js rename to test/e2e/image-optimizer/app/pages/api/comma.svg.js diff --git a/test/integration/image-optimizer/app/pages/api/conditional-cookie.js b/test/e2e/image-optimizer/app/pages/api/conditional-cookie.js similarity index 100% rename from test/integration/image-optimizer/app/pages/api/conditional-cookie.js rename to test/e2e/image-optimizer/app/pages/api/conditional-cookie.js diff --git a/test/integration/image-optimizer/app/pages/api/no-header.js b/test/e2e/image-optimizer/app/pages/api/no-header.js similarity index 100% rename from test/integration/image-optimizer/app/pages/api/no-header.js rename to test/e2e/image-optimizer/app/pages/api/no-header.js diff --git a/test/integration/image-optimizer/app/pages/api/stateful/test.png.js b/test/e2e/image-optimizer/app/pages/api/stateful/test.png.js similarity index 100% rename from test/integration/image-optimizer/app/pages/api/stateful/test.png.js rename to test/e2e/image-optimizer/app/pages/api/stateful/test.png.js diff --git a/test/integration/image-optimizer/app/pages/api/uppercase.svg.js b/test/e2e/image-optimizer/app/pages/api/uppercase.svg.js similarity index 100% rename from test/integration/image-optimizer/app/pages/api/uppercase.svg.js rename to test/e2e/image-optimizer/app/pages/api/uppercase.svg.js diff --git a/test/integration/image-optimizer/app/pages/api/wrong-header.svg.js b/test/e2e/image-optimizer/app/pages/api/wrong-header.svg.js similarity index 100% rename from test/integration/image-optimizer/app/pages/api/wrong-header.svg.js rename to test/e2e/image-optimizer/app/pages/api/wrong-header.svg.js diff --git a/test/integration/image-optimizer/app/pages/index.js b/test/e2e/image-optimizer/app/pages/index.js similarity index 100% rename from test/integration/image-optimizer/app/pages/index.js rename to test/e2e/image-optimizer/app/pages/index.js diff --git a/test/integration/image-optimizer/app/public/animated.gif b/test/e2e/image-optimizer/app/public/animated.gif similarity index 100% rename from test/integration/image-optimizer/app/public/animated.gif rename to test/e2e/image-optimizer/app/public/animated.gif diff --git a/test/integration/image-optimizer/app/public/animated.png b/test/e2e/image-optimizer/app/public/animated.png similarity index 100% rename from test/integration/image-optimizer/app/public/animated.png rename to test/e2e/image-optimizer/app/public/animated.png diff --git a/test/integration/image-optimizer/app/public/animated.webp b/test/e2e/image-optimizer/app/public/animated.webp similarity index 100% rename from test/integration/image-optimizer/app/public/animated.webp rename to test/e2e/image-optimizer/app/public/animated.webp diff --git a/test/integration/image-optimizer/app/public/animated2.png b/test/e2e/image-optimizer/app/public/animated2.png similarity index 100% rename from test/integration/image-optimizer/app/public/animated2.png rename to test/e2e/image-optimizer/app/public/animated2.png diff --git a/test/integration/image-optimizer/app/public/grayscale.png b/test/e2e/image-optimizer/app/public/grayscale.png similarity index 100% rename from test/integration/image-optimizer/app/public/grayscale.png rename to test/e2e/image-optimizer/app/public/grayscale.png diff --git a/test/integration/image-optimizer/app/public/mountains.jpg b/test/e2e/image-optimizer/app/public/mountains.jpg similarity index 100% rename from test/integration/image-optimizer/app/public/mountains.jpg rename to test/e2e/image-optimizer/app/public/mountains.jpg diff --git a/test/integration/image-optimizer/app/public/png-as-octet-stream b/test/e2e/image-optimizer/app/public/png-as-octet-stream similarity index 100% rename from test/integration/image-optimizer/app/public/png-as-octet-stream rename to test/e2e/image-optimizer/app/public/png-as-octet-stream diff --git a/test/integration/image-optimizer/app/public/test.avif b/test/e2e/image-optimizer/app/public/test.avif similarity index 100% rename from test/integration/image-optimizer/app/public/test.avif rename to test/e2e/image-optimizer/app/public/test.avif diff --git a/test/integration/image-optimizer/app/public/test.bmp b/test/e2e/image-optimizer/app/public/test.bmp similarity index 100% rename from test/integration/image-optimizer/app/public/test.bmp rename to test/e2e/image-optimizer/app/public/test.bmp diff --git a/test/integration/image-optimizer/app/public/test.gif b/test/e2e/image-optimizer/app/public/test.gif similarity index 100% rename from test/integration/image-optimizer/app/public/test.gif rename to test/e2e/image-optimizer/app/public/test.gif diff --git a/test/integration/image-optimizer/app/public/test.heic b/test/e2e/image-optimizer/app/public/test.heic similarity index 100% rename from test/integration/image-optimizer/app/public/test.heic rename to test/e2e/image-optimizer/app/public/test.heic diff --git a/test/integration/image-optimizer/app/public/test.icns b/test/e2e/image-optimizer/app/public/test.icns similarity index 100% rename from test/integration/image-optimizer/app/public/test.icns rename to test/e2e/image-optimizer/app/public/test.icns diff --git a/test/integration/image-optimizer/app/public/test.ico b/test/e2e/image-optimizer/app/public/test.ico similarity index 100% rename from test/integration/image-optimizer/app/public/test.ico rename to test/e2e/image-optimizer/app/public/test.ico diff --git a/test/integration/image-optimizer/app/public/test.jp2 b/test/e2e/image-optimizer/app/public/test.jp2 similarity index 100% rename from test/integration/image-optimizer/app/public/test.jp2 rename to test/e2e/image-optimizer/app/public/test.jp2 diff --git a/test/integration/build-trace-extra-entries-turbo/app/public/test.jpg b/test/e2e/image-optimizer/app/public/test.jpg similarity index 100% rename from test/integration/build-trace-extra-entries-turbo/app/public/test.jpg rename to test/e2e/image-optimizer/app/public/test.jpg diff --git a/test/integration/image-optimizer/app/public/test.jxl b/test/e2e/image-optimizer/app/public/test.jxl similarity index 100% rename from test/integration/image-optimizer/app/public/test.jxl rename to test/e2e/image-optimizer/app/public/test.jxl diff --git a/test/integration/image-optimizer/app/public/test.pdf b/test/e2e/image-optimizer/app/public/test.pdf similarity index 100% rename from test/integration/image-optimizer/app/public/test.pdf rename to test/e2e/image-optimizer/app/public/test.pdf diff --git a/test/integration/image-optimizer/app/public/test.pic b/test/e2e/image-optimizer/app/public/test.pic similarity index 100% rename from test/integration/image-optimizer/app/public/test.pic rename to test/e2e/image-optimizer/app/public/test.pic diff --git a/test/integration/image-optimizer/app/public/test.png b/test/e2e/image-optimizer/app/public/test.png similarity index 100% rename from test/integration/image-optimizer/app/public/test.png rename to test/e2e/image-optimizer/app/public/test.png diff --git a/test/integration/image-optimizer/app/public/test.svg b/test/e2e/image-optimizer/app/public/test.svg similarity index 100% rename from test/integration/image-optimizer/app/public/test.svg rename to test/e2e/image-optimizer/app/public/test.svg diff --git a/test/integration/image-optimizer/app/public/test.tiff b/test/e2e/image-optimizer/app/public/test.tiff similarity index 100% rename from test/integration/image-optimizer/app/public/test.tiff rename to test/e2e/image-optimizer/app/public/test.tiff diff --git a/test/integration/next-image-legacy/base-path/public/test.webp b/test/e2e/image-optimizer/app/public/test.webp similarity index 100% rename from test/integration/next-image-legacy/base-path/public/test.webp rename to test/e2e/image-optimizer/app/public/test.webp diff --git a/test/integration/image-optimizer/app/public/text.txt b/test/e2e/image-optimizer/app/public/text.txt similarity index 100% rename from test/integration/image-optimizer/app/public/text.txt rename to test/e2e/image-optimizer/app/public/text.txt diff --git "a/test/integration/image-optimizer/app/public/\303\244\303\266\303\274\305\241\304\215\305\231\303\255.png" "b/test/e2e/image-optimizer/app/public/\303\244\303\266\303\274\305\241\304\215\305\231\303\255.png" similarity index 100% rename from "test/integration/image-optimizer/app/public/\303\244\303\266\303\274\305\241\304\215\305\231\303\255.png" rename to "test/e2e/image-optimizer/app/public/\303\244\303\266\303\274\305\241\304\215\305\231\303\255.png" diff --git a/test/integration/image-optimizer/test/content-disposition-type.test.ts b/test/e2e/image-optimizer/content-disposition-type.test.ts similarity index 68% rename from test/integration/image-optimizer/test/content-disposition-type.test.ts rename to test/e2e/image-optimizer/content-disposition-type.test.ts index 3e99e56f4ff9..97b859bda4be 100644 --- a/test/integration/image-optimizer/test/content-disposition-type.test.ts +++ b/test/e2e/image-optimizer/content-disposition-type.test.ts @@ -1,11 +1,7 @@ -import { join } from 'path' import { setupTests } from './util' -const appDir = join(__dirname, '../app') - describe('with contentDispositionType inline', () => { setupTests({ nextConfigImages: { contentDispositionType: 'inline' }, - appDir, }) }) diff --git a/test/integration/image-optimizer/test/dangerously-allow-svg.test.ts b/test/e2e/image-optimizer/dangerously-allow-svg.test.ts similarity index 66% rename from test/integration/image-optimizer/test/dangerously-allow-svg.test.ts rename to test/e2e/image-optimizer/dangerously-allow-svg.test.ts index 059840526bc9..7ee8a95613f6 100644 --- a/test/integration/image-optimizer/test/dangerously-allow-svg.test.ts +++ b/test/e2e/image-optimizer/dangerously-allow-svg.test.ts @@ -1,11 +1,7 @@ -import { join } from 'path' import { setupTests } from './util' -const appDir = join(__dirname, '../app') - describe('with dangerouslyAllowSVG config', () => { setupTests({ nextConfigImages: { dangerouslyAllowSVG: true }, - appDir, }) }) diff --git a/test/integration/image-optimizer/test/disable-write-to-cache-dir.test.ts b/test/e2e/image-optimizer/disable-write-to-cache-dir.test.ts similarity index 67% rename from test/integration/image-optimizer/test/disable-write-to-cache-dir.test.ts rename to test/e2e/image-optimizer/disable-write-to-cache-dir.test.ts index 2d5efd9c188a..2e80c0a53e47 100644 --- a/test/integration/image-optimizer/test/disable-write-to-cache-dir.test.ts +++ b/test/e2e/image-optimizer/disable-write-to-cache-dir.test.ts @@ -1,11 +1,7 @@ -import { join } from 'path' import { setupTests } from './util' -const appDir = join(__dirname, '../app') - describe('with isrFlushToDisk false config', () => { setupTests({ - appDir, nextConfigExperimental: { isrFlushToDisk: false }, }) }) diff --git a/test/e2e/image-optimizer/image-optimizer.test.ts b/test/e2e/image-optimizer/image-optimizer.test.ts new file mode 100644 index 000000000000..cda4b5eb996f --- /dev/null +++ b/test/e2e/image-optimizer/image-optimizer.test.ts @@ -0,0 +1,534 @@ +import { join } from 'path' +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +import { check } from 'next-test-utils' +import { cleanImagesDir, expectWidth, fsToJson } from './util' + +function toQueryString(query: Record<string, any>): string { + const params = new URLSearchParams() + for (const [k, v] of Object.entries(query)) { + if (v !== undefined && v !== null) params.set(k, String(v)) + } + return params.toString() +} + +const largeSize = 1080 + +describe('Image Optimizer', () => { + describe('config checks', () => { + const { next, skipped } = nextTestSetup({ + files: join(__dirname, 'app'), + skipStart: true, + skipDeployment: true, + }) + if (skipped) return + + const configChecks: Array<{ + name: string + config: string + expected: string | string[] + }> = [ + { + name: 'should error when domains length exceeds 50', + config: JSON.stringify({ + images: { domains: new Array(51).fill('google.com') }, + }), + expected: + 'Array must contain at most 50 element(s) at "images.domains"', + }, + { + name: 'should error when localPatterns length exceeds 25', + config: JSON.stringify({ + images: { + localPatterns: Array.from({ length: 26 }).map(() => ({ + pathname: '/foo/**', + })), + }, + }), + expected: + 'Array must contain at most 25 element(s) at "images.localPatterns"', + }, + { + name: 'should error when localPatterns has invalid prop', + config: JSON.stringify({ + images: { + localPatterns: [{ pathname: '/foo/**', foo: 'bar' }], + }, + }), + expected: `Unrecognized key(s) in object: 'foo' at "images.localPatterns[0]"`, + }, + { + name: 'should error when remotePatterns length exceeds 50', + config: JSON.stringify({ + images: { + remotePatterns: Array.from({ length: 51 }).map(() => ({ + hostname: 'example.com', + })), + }, + }), + expected: + 'Array must contain at most 50 element(s) at "images.remotePatterns"', + }, + { + name: 'should error when remotePatterns has invalid prop', + config: JSON.stringify({ + images: { + remotePatterns: [{ hostname: 'example.com', foo: 'bar' }], + }, + }), + expected: `Unrecognized key(s) in object: 'foo' at "images.remotePatterns[0]"`, + }, + { + name: 'should error when remotePatterns is missing hostname', + config: JSON.stringify({ + images: { remotePatterns: [{ protocol: 'https' }] }, + }), + expected: `"images.remotePatterns[0].hostname" is missing, expected string`, + }, + { + name: 'should error when sizes length exceeds 25', + config: JSON.stringify({ + images: { deviceSizes: new Array(51).fill(1024) }, + }), + expected: + 'Array must contain at most 25 element(s) at "images.deviceSizes"', + }, + { + name: 'should error when deviceSizes contains invalid widths', + config: JSON.stringify({ + images: { deviceSizes: [0, 12000, 64, 128, 256] }, + }), + expected: [ + 'Number must be greater than or equal to 1 at "images.deviceSizes[0]"', + 'Number must be less than or equal to 10000 at "images.deviceSizes[1]"', + ], + }, + { + name: 'should error when imageSizes contains invalid widths', + config: JSON.stringify({ + images: { imageSizes: [0, 16, 64, 12000] }, + }), + expected: [ + 'Number must be greater than or equal to 1 at "images.imageSizes[0]"', + 'Number must be less than or equal to 10000 at "images.imageSizes[3]"', + ], + }, + { + name: 'should error when qualities length exceeds 20', + config: JSON.stringify({ + images: { + qualities: [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, + ], + }, + }), + expected: + 'Array must contain at most 20 element(s) at "images.qualities"', + }, + { + name: 'should error when qualities array has a value thats not an integer', + config: JSON.stringify({ + images: { qualities: [1, 2, 3, 9.9] }, + }), + expected: 'Expected integer, received float at "images.qualities[3]"', + }, + { + name: 'should error when qualities array is empty', + config: JSON.stringify({ + images: { qualities: [] }, + }), + expected: + 'Array must contain at least 1 element(s) at "images.qualities"', + }, + { + name: 'should error when loader contains invalid value', + config: JSON.stringify({ + images: { loader: 'notreal' }, + }), + expected: `Expected 'default' | 'imgix' | 'cloudinary' | 'akamai' | 'custom', received 'notreal' at "images.loader"`, + }, + { + name: 'should error when images.formats contains invalid values', + config: JSON.stringify({ + images: { formats: ['image/avif', 'jpeg'] }, + }), + expected: `Expected 'image/avif' | 'image/webp', received 'jpeg' at "images.formats[1]"`, + }, + { + name: 'should error when images.loader is assigned but images.path is not', + config: JSON.stringify({ + images: { loader: 'imgix' }, + }), + expected: + 'Specified images.loader property (imgix) also requires images.path property to be assigned to a URL prefix.', + }, + { + name: 'should error when images.loader and images.loaderFile are both assigned', + config: JSON.stringify({ + images: { + loader: 'imgix', + path: 'https://example.com', + loaderFile: './dummy.js', + }, + }), + expected: + 'Specified images.loader property (imgix) cannot be used with images.loaderFile property. Please set images.loader to "custom".', + }, + { + name: 'should error when images.loaderFile does not exist', + config: JSON.stringify({ + images: { loaderFile: './fakefile.js' }, + }), + expected: 'Specified images.loaderFile does not exist at', + }, + { + name: 'should error when images.dangerouslyAllowSVG is not a boolean', + config: JSON.stringify({ + images: { dangerouslyAllowSVG: 'foo' }, + }), + expected: + 'Expected boolean, received string at "images.dangerouslyAllowSVG"', + }, + { + name: 'should error when images.contentSecurityPolicy is not a string', + config: JSON.stringify({ + images: { contentSecurityPolicy: 1 }, + }), + expected: + 'Expected string, received number at "images.contentSecurityPolicy"', + }, + { + name: 'should error when assetPrefix is provided but is invalid', + config: JSON.stringify({ + assetPrefix: 'httpbad', + images: { formats: ['image/webp'] }, + }), + expected: [ + 'Invalid assetPrefix provided. Original error:', + 'Invalid URL', + ], + }, + { + name: 'should error when images.remotePatterns is invalid', + config: JSON.stringify({ + images: { remotePatterns: 'testing' }, + }), + expected: 'Expected array, received string at "images.remotePatterns"', + }, + { + name: 'should error when images.remotePatterns URL has invalid protocol', + config: `{ images: { remotePatterns: [new URL('file://example.com/**')] } }`, + expected: + 'Specified images.remotePatterns must have protocol "http" or "https" received "file"', + }, + { + name: 'should error when images.contentDispositionType is not valid', + config: JSON.stringify({ + images: { contentDispositionType: 'nope' }, + }), + expected: `Expected 'inline' | 'attachment', received 'nope' at "images.contentDispositionType"`, + }, + { + name: 'should error when images.minimumCacheTTL is not valid', + config: JSON.stringify({ + images: { minimumCacheTTL: -1 }, + }), + expected: + 'Number must be greater than or equal to 0 at "images.minimumCacheTTL"', + }, + { + name: 'should error when images.unoptimized is not a boolean', + config: JSON.stringify({ + images: { unoptimized: 'yup' }, + }), + expected: 'Expected boolean, received string at "images.unoptimized"', + }, + ] + + for (const { name, config, expected } of configChecks) { + it(name, async () => { + const outputBefore = next.cliOutput.length + await next.patchFile('next.config.js', `module.exports = ${config}`) + await next.build() + const newOutput = next.cliOutput.slice(outputBefore) + const expectations = Array.isArray(expected) ? expected : [expected] + for (const exp of expectations) { + expect(newOutput).toContain(exp) + } + await next.patchFile( + 'next.config.js', + '// prettier-ignore\nmodule.exports = {}' + ) + }) + } + }) + describe('Server support for trailingSlash in next.config.js', () => { + const { next, skipped } = nextTestSetup({ + files: join(__dirname, 'app'), + nextConfig: { + trailingSlash: true, + images: { + imageSizes: [8, 16, 32, 48, 64, 96, 128, 256, 384], + 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 } + const res = await next.fetch(`/_next/image/?${toQueryString(query)}`) + expect(res.status).toBe(200) + }) + }) + ;(isNextStart ? describe : describe.skip)( + 'Server support for headers in next.config.js', + () => { + const size = 96 + const { next, skipped } = nextTestSetup({ + files: join(__dirname, 'app'), + skipDeployment: true, + }) + if (skipped) return + + beforeAll(async () => { + await next.patchFile( + 'next.config.js', + `module.exports = { + async headers() { + return [ + { + source: '/test.png', + headers: [ + { + key: 'Cache-Control', + value: 'public, max-age=14400, must-revalidate', + }, + ], + }, + ] + }, + }` + ) + }) + afterAll(async () => { + await next.patchFile( + 'next.config.js', + '// prettier-ignore\nmodule.exports = { /* replaceme */ }' + ) + }) + + it('should set max-age header', async () => { + const query = { url: '/test.png', w: size, q: 75 } + const opts = { headers: { accept: 'image/webp' } } + const res = await next.fetch( + `/_next/image?${toQueryString(query)}`, + opts + ) + expect(res.status).toBe(200) + expect(res.headers.get('Cache-Control')).toBe( + 'public, max-age=14400, must-revalidate' + ) + expect(res.headers.get('Content-Disposition')).toBe( + 'attachment; filename="test.webp"' + ) + + const imagesDir = join(next.testDir, '.next', 'cache', 'images') + await check(async () => { + const files = await fsToJson(imagesDir) + let found = false + const maxAge = '14400' + Object.keys(files).forEach((dir) => { + if ( + Object.keys(files[dir]).some((file) => + file.includes(`${maxAge}.`) + ) + ) { + found = true + } + }) + return found ? 'success' : 'failed' + }, 'success') + }) + + it('should not set max-age header when not matching next.config.js', async () => { + const query = { url: '/test.jpg', w: size, q: 75 } + const opts = { headers: { accept: 'image/webp' } } + const res = await next.fetch( + `/_next/image?${toQueryString(query)}`, + opts + ) + expect(res.status).toBe(200) + expect(res.headers.get('Cache-Control')).toBe( + 'public, max-age=14400, must-revalidate' + ) + expect(res.headers.get('Content-Disposition')).toBe( + 'attachment; filename="test.webp"' + ) + }) + } + ) + ;(isNextDev ? describe : describe.skip)( + 'dev support next.config.js cloudinary loader', + () => { + const { next, skipped } = nextTestSetup({ + files: join(__dirname, 'app'), + nextConfig: { + images: { + loader: 'cloudinary', + path: 'https://example.com/act123/', + }, + }, + skipDeployment: true, + }) + if (skipped) return + + it('should 404 when loader is not default', async () => { + const size = 384 + const query = { w: size, q: 90, url: '/test.svg' } + const opts = { headers: { accept: 'image/webp' } } + const res = await next.fetch( + `/_next/image?${toQueryString(query)}`, + opts + ) + expect(res.status).toBe(404) + }) + } + ) + ;(isNextDev ? describe : describe.skip)( + 'images.unoptimized in next.config.js', + () => { + const { next, skipped } = nextTestSetup({ + files: join(__dirname, 'app'), + nextConfig: { + images: { unoptimized: true }, + }, + skipDeployment: true, + }) + if (skipped) return + + it('should 404 when unoptimized', async () => { + const size = 384 + const query = { w: size, q: 75, url: '/test.jpg' } + const opts = { headers: { accept: 'image/webp' } } + const res = await next.fetch( + `/_next/image?${toQueryString(query)}`, + opts + ) + expect(res.status).toBe(404) + }) + } + ) + ;(isNextDev ? describe : describe.skip)( + 'experimental.imgOptMaxInputPixels in next.config.js', + () => { + const { next, skipped } = nextTestSetup({ + files: join(__dirname, 'app'), + nextConfig: { + experimental: { imgOptMaxInputPixels: 100 }, + }, + skipDeployment: true, + }) + if (skipped) return + + it('should fallback to source image when input exceeds imgOptMaxInputPixels', async () => { + const size = 256 + const query = { w: size, q: 75, url: '/test.jpg' } + const opts = { headers: { accept: 'image/webp' } } + const res = await next.fetch( + `/_next/image?${toQueryString(query)}`, + opts + ) + expect(res.status).toBe(200) + expect(res.headers.get('Content-Type')).toBe('image/jpeg') + }) + } + ) + ;(isNextStart ? describe : describe.skip)( + 'External rewrite support with for serving static content in images', + () => { + const { next, skipped } = nextTestSetup({ + files: join(__dirname, 'app'), + nextConfig: { + async rewrites() { + return [ + { + source: '/:base(next-js)/:rest*', + destination: + 'https://assets.vercel.com/image/upload/v1538361091/repositories/:base/:rest*', + }, + ] + }, + }, + skipDeployment: true, + }) + if (skipped) return + + it('should return response when image is served from an external rewrite', async () => { + const imagesDir = join(next.testDir, '.next', 'cache', 'images') + await cleanImagesDir(imagesDir) + + const query = { url: '/next-js/next-js-bg.png', w: 64, q: 75 } + const opts = { headers: { accept: 'image/webp' } } + const res = await next.fetch( + `/_next/image?${toQueryString(query)}`, + opts + ) + expect(res.status).toBe(200) + expect(res.headers.get('Content-Type')).toBe('image/webp') + expect(res.headers.get('Cache-Control')).toBe( + 'public, max-age=31536000, must-revalidate' + ) + expect(res.headers.get('Vary')).toBe('Accept') + expect(res.headers.get('Content-Disposition')).toBe( + 'attachment; filename="next-js-bg.webp"' + ) + + await check(async () => { + const files = await fsToJson(imagesDir) + let found = false + const maxAge = '31536000' + Object.keys(files).forEach((dir) => { + if ( + Object.keys(files[dir]).some((file) => + file.includes(`${maxAge}.`) + ) + ) { + found = true + } + }) + return found ? 'success' : 'failed' + }, 'success') + await expectWidth(res, 64) + }) + } + ) + ;(isNextDev ? describe : describe.skip)( + 'dev support for dynamic blur placeholder', + () => { + const { next, skipped } = nextTestSetup({ + files: join(__dirname, 'app'), + nextConfig: { + images: { + deviceSizes: [largeSize], + imageSizes: [], + }, + }, + skipDeployment: true, + }) + if (skipped) return + + it('should support width 8 per BLUR_IMG_SIZE with next dev', async () => { + const query = { url: '/test.png', w: 8, q: 70 } + const opts = { headers: { accept: 'image/webp' } } + const res = await next.fetch( + `/_next/image?${toQueryString(query)}`, + opts + ) + expect(res.status).toBe(200) + await expectWidth(res, 320) + }) + } + ) +}) diff --git a/test/integration/image-optimizer/test/max-disk-size-cache-85kb.test.ts b/test/e2e/image-optimizer/max-disk-size-cache-85kb.test.ts similarity index 69% rename from test/integration/image-optimizer/test/max-disk-size-cache-85kb.test.ts rename to test/e2e/image-optimizer/max-disk-size-cache-85kb.test.ts index b9fc0d6f76be..3c4eebd1613c 100644 --- a/test/integration/image-optimizer/test/max-disk-size-cache-85kb.test.ts +++ b/test/e2e/image-optimizer/max-disk-size-cache-85kb.test.ts @@ -1,11 +1,7 @@ -import { join } from 'path' import { setupTests } from './util' -const appDir = join(__dirname, '../app') - describe('with maximumDiskCacheSize 85KB config', () => { setupTests({ - appDir, nextConfigImages: { maximumDiskCacheSize: 85_000, }, diff --git a/test/integration/image-optimizer/test/max-disk-size-cache-zero.test.ts b/test/e2e/image-optimizer/max-disk-size-cache-zero.test.ts similarity index 68% rename from test/integration/image-optimizer/test/max-disk-size-cache-zero.test.ts rename to test/e2e/image-optimizer/max-disk-size-cache-zero.test.ts index 415279848e74..4e9240f92245 100644 --- a/test/integration/image-optimizer/test/max-disk-size-cache-zero.test.ts +++ b/test/e2e/image-optimizer/max-disk-size-cache-zero.test.ts @@ -1,11 +1,7 @@ -import { join } from 'path' import { setupTests } from './util' -const appDir = join(__dirname, '../app') - describe('with maximumDiskCacheSize zero config', () => { setupTests({ - appDir, nextConfigImages: { maximumDiskCacheSize: 0, }, diff --git a/test/integration/image-optimizer/test/maximum-redirects-0.test.ts b/test/e2e/image-optimizer/maximum-redirects-0.test.ts similarity index 66% rename from test/integration/image-optimizer/test/maximum-redirects-0.test.ts rename to test/e2e/image-optimizer/maximum-redirects-0.test.ts index e78fa6c67202..a069b5b1dc2c 100644 --- a/test/integration/image-optimizer/test/maximum-redirects-0.test.ts +++ b/test/e2e/image-optimizer/maximum-redirects-0.test.ts @@ -1,13 +1,9 @@ -import { join } from 'path' import { setupTests } from './util' -const appDir = join(__dirname, '../app') - describe('with maximumRedirects 0', () => { setupTests({ nextConfigImages: { dangerouslyAllowLocalIP: true, - // Configure external domains so we can try out external redirects domains: [ 'localhost', '127.0.0.1', @@ -15,9 +11,7 @@ describe('with maximumRedirects 0', () => { 'assets.vercel.com', 'image-optimization-test.vercel.app', ], - // Prevent redirects maximumRedirects: 0, }, - appDir, }) }) diff --git a/test/integration/image-optimizer/test/maximum-redirects-1.test.ts b/test/e2e/image-optimizer/maximum-redirects-1.test.ts similarity index 66% rename from test/integration/image-optimizer/test/maximum-redirects-1.test.ts rename to test/e2e/image-optimizer/maximum-redirects-1.test.ts index 2a774169d592..c1f4434d31c4 100644 --- a/test/integration/image-optimizer/test/maximum-redirects-1.test.ts +++ b/test/e2e/image-optimizer/maximum-redirects-1.test.ts @@ -1,13 +1,9 @@ -import { join } from 'path' import { setupTests } from './util' -const appDir = join(__dirname, '../app') - describe('with maximumRedirects 1', () => { setupTests({ nextConfigImages: { dangerouslyAllowLocalIP: true, - // Configure external domains so we can try out external redirects domains: [ 'localhost', '127.0.0.1', @@ -15,9 +11,7 @@ describe('with maximumRedirects 1', () => { 'assets.vercel.com', 'image-optimization-test.vercel.app', ], - // Only one redirect maximumRedirects: 1, }, - appDir, }) }) diff --git a/test/integration/image-optimizer/test/minimum-cache-ttl.test.ts b/test/e2e/image-optimizer/minimum-cache-ttl.test.ts similarity index 54% rename from test/integration/image-optimizer/test/minimum-cache-ttl.test.ts rename to test/e2e/image-optimizer/minimum-cache-ttl.test.ts index ebe1ef4fc7ab..727902d57547 100644 --- a/test/integration/image-optimizer/test/minimum-cache-ttl.test.ts +++ b/test/e2e/image-optimizer/minimum-cache-ttl.test.ts @@ -1,14 +1,9 @@ -import { join } from 'path' import { setupTests } from './util' -const appDir = join(__dirname, '../app') - describe('with minimumCacheTTL of 5 sec', () => { setupTests({ nextConfigImages: { dangerouslyAllowLocalIP: true, - // Configure external domains so we can try out - // variations of the upstream Cache-Control header. domains: [ 'localhost', '127.0.0.1', @@ -16,10 +11,7 @@ describe('with minimumCacheTTL of 5 sec', () => { 'assets.vercel.com', 'image-optimization-test.vercel.app', ], - // Reduce to 5 seconds so tests dont dont need to - // wait too long before testing stale responses. minimumCacheTTL: 5, }, - appDir, }) }) diff --git a/test/e2e/image-optimizer/sharp.test.ts b/test/e2e/image-optimizer/sharp.test.ts new file mode 100644 index 000000000000..fd03aabda713 --- /dev/null +++ b/test/e2e/image-optimizer/sharp.test.ts @@ -0,0 +1,5 @@ +import { setupTests } from './util' + +describe('with latest sharp', () => { + setupTests({}) +}) diff --git a/test/integration/image-optimizer/test/util.ts b/test/e2e/image-optimizer/util.ts similarity index 76% rename from test/integration/image-optimizer/test/util.ts rename to test/e2e/image-optimizer/util.ts index e1c1ad991ebc..cf12a7c81a15 100644 --- a/test/integration/image-optimizer/test/util.ts +++ b/test/e2e/image-optimizer/util.ts @@ -5,40 +5,42 @@ import assert from 'assert' import sizeOf from 'image-size' import { check, - fetchViaHTTP, - File, findPort, getDeploymentId, getDistDir, - killApp, - launchApp, listClientChunks, - nextBuild, - nextStart, retry, + shouldUseTurbopack, waitFor, } from 'next-test-utils' +import { nextTestSetup, isNextDev } from 'e2e-utils' import isAnimated from 'next/dist/compiled/is-animated' -import type { RequestInit } from 'node-fetch' -import type { NextConfig } from 'next' +import type { NextInstance } from '../../lib/next-modes/base' + +function toQueryString(query: Record<string, any> | null | undefined): string { + if (!query) return '' + const params = new URLSearchParams() + for (const [k, v] of Object.entries(query)) { + if (v !== undefined && v !== null) params.set(k, String(v)) + } + return params.toString() +} type SetupTestsCtx = { - appDir: string nextConfigImages?: Partial<import('next').NextConfig['images']> nextConfigExperimental?: Partial<import('next').NextConfig['experimental']> - isDev?: boolean } -type RunTestsCtx = SetupTestsCtx & { +type RunTestsCtx = { + next: NextInstance w: number q: number - imagesDir: string - app?: import('child_process').ChildProcess - appDir?: string - appPort?: number - nextOutput?: string + nextConfigImages?: Partial<import('next').NextConfig['images']> + nextConfigExperimental?: Partial<import('next').NextConfig['experimental']> } +type NextFetchOptions = Parameters<NextInstance['fetch']>[1] + let infiniteRedirect = 0 const largeSize = 1080 // defaults defined in server/config.ts const animatedWarnText = @@ -78,7 +80,7 @@ export async function serveSlowImage() { return } res.setHeader('content-type', 'image/png') - res.end(await fs.readFile(join(__dirname, '../app/public/test.png'))) + res.end(await fs.readFile(join(__dirname, 'app/public/test.png'))) }) await new Promise((resolve) => { @@ -112,7 +114,7 @@ export async function fsToJson(dir: string, output = {}) { } export async function expectWidth(res, w, { expectAnimated = false } = {}) { - const buffer = await res.buffer() + const buffer = Buffer.from(await res.arrayBuffer()) const d = sizeOf(buffer) expect(d.width).toBe(w) const lengthStr = res.headers.get('Content-Length') @@ -142,59 +144,56 @@ async function getDirSize(dir: string): Promise<number> { } async function expectAvifSmallerThanWebp( + next: NextInstance, w: number, - q: number, - appPort: number + q: number ) { - const query = { url: '/mountains.jpg', w, q } - const res1 = await fetchViaHTTP(appPort, '/_next/image', query, { - headers: { - accept: 'image/avif', - }, + const qs = toQueryString({ url: '/mountains.jpg', w, q }) + const res1 = await next.fetch(`/_next/image?${qs}`, { + headers: { accept: 'image/avif' }, }) expect(res1.status).toBe(200) expect(res1.headers.get('Content-Type')).toBe('image/avif') - const res2 = await fetchViaHTTP(appPort, '/_next/image', query, { - headers: { - accept: 'image/webp', - }, + const res2 = await next.fetch(`/_next/image?${qs}`, { + headers: { accept: 'image/webp' }, }) expect(res2.status).toBe(200) expect(res2.headers.get('Content-Type')).toBe('image/webp') - const res3 = await fetchViaHTTP(appPort, '/_next/image', query, { - headers: { - accept: 'image/jpeg', - }, + const res3 = await next.fetch(`/_next/image?${qs}`, { + headers: { accept: 'image/jpeg' }, }) expect(res3.status).toBe(200) expect(res3.headers.get('Content-Type')).toBe('image/jpeg') - const avif = (await res1.buffer()).byteLength - const webp = (await res2.buffer()).byteLength - const jpeg = (await res3.buffer()).byteLength + const avif = (await res1.arrayBuffer()).byteLength + const webp = (await res2.arrayBuffer()).byteLength + const jpeg = (await res3.arrayBuffer()).byteLength expect(webp).toBeLessThan(jpeg) expect(avif).toBeLessThanOrEqual(webp) } async function fetchWithDuration( - appPort: string | number, + next: NextInstance, pathname: string, - query?: Record<string, any> | string, - opts?: RequestInit + query?: Record<string, any>, + opts?: NextFetchOptions ) { console.warn('Fetching', pathname, query) + const qs = toQueryString(query) + const url = qs ? `${pathname}?${qs}` : pathname const start = Date.now() - const res = await fetchViaHTTP(appPort, pathname, query, opts) - const buffer = await res.buffer() + const res = await next.fetch(url, opts) + const buffer = Buffer.from(await res.arrayBuffer()) const duration = Date.now() - start return { duration, buffer, res } } export function runTests(ctx: RunTestsCtx) { - const { isDev, nextConfigImages, imagesDir } = ctx + const { next, nextConfigImages } = ctx + const isDev = isNextDev const { contentDispositionType = 'attachment', domains = [], @@ -205,8 +204,10 @@ export function runTests(ctx: RunTestsCtx) { } = nextConfigImages || {} const avifEnabled = formats[0] === 'image/avif' let slowImageServer: Awaited<ReturnType<typeof serveSlowImage>> + let imagesDir: string beforeAll(async () => { slowImageServer = await serveSlowImage() + imagesDir = join(next.testDir, getDistDir(), 'cache', 'images') }) afterAll(async () => { slowImageServer.stop() @@ -216,19 +217,19 @@ export function runTests(ctx: RunTestsCtx) { it('should normalize invalid status codes', async () => { const url = `http://localhost:${slowImageServer.port}/slow.png?status=399` const query = { url, w: ctx.w, q: ctx.q } - const opts: RequestInit = { + const opts: NonNullable<NextFetchOptions> = { headers: { accept: 'image/webp' }, redirect: 'manual', } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(500) }) it('should timeout for upstream image exceeding 7 seconds', async () => { const url = `http://localhost:${slowImageServer.port}/slow.png?delay=${8000}` const query = { url, w: ctx.w, q: ctx.q } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(504) }) } @@ -237,14 +238,14 @@ export function runTests(ctx: RunTestsCtx) { it('should follow redirect from http to https when maximumRedirects > 0', async () => { const url = `http://image-optimization-test.vercel.app/frog.png` const query = { url, w: ctx.w, q: ctx.q } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(maximumRedirects > 0 ? 200 : 508) }) it('should follow redirect when dangerouslyAllowLocalIP enabled', async () => { const url = `http://localhost:${slowImageServer.port}?status=301&location=%2Fslow.png` const query = { url, w: ctx.w, q: ctx.q } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) let expectedStatus = dangerouslyAllowLocalIP ? 200 : 400 if (maximumRedirects === 0) { expectedStatus = 508 @@ -256,26 +257,26 @@ export function runTests(ctx: RunTestsCtx) { infiniteRedirect = 1 const url = `http://localhost:${slowImageServer.port}` const query = { url, w: ctx.w, q: ctx.q } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(508) infiniteRedirect = 0 }) } it('should return home page', async () => { - const res = await fetchViaHTTP(ctx.appPort, '/', null, {}) + const res = await next.fetch('/') expect(await res.text()).toMatch(/Image Optimizer Home/m) }) it('should handle non-ascii characters in image url', async () => { const query = { w: ctx.w, q: ctx.q, url: '/äöüščří.png' } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(200) }) it('should maintain icns', async () => { const query = { w: ctx.w, q: ctx.q, url: '/test.icns' } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toContain('image/x-icns') expect(res.headers.get('Cache-Control')).toBe( @@ -291,7 +292,7 @@ export function runTests(ctx: RunTestsCtx) { it('should maintain jxl', async () => { const query = { w: ctx.w, q: ctx.q, url: '/test.jxl' } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toContain('image/jxl') expect(res.headers.get('Cache-Control')).toBe( @@ -307,7 +308,7 @@ export function runTests(ctx: RunTestsCtx) { it('should maintain heic', async () => { const query = { w: ctx.w, q: ctx.q, url: '/test.heic' } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toContain('image/heic') expect(res.headers.get('Cache-Control')).toBe( @@ -323,7 +324,7 @@ export function runTests(ctx: RunTestsCtx) { it('should maintain jp2', async () => { const query = { w: ctx.w, q: ctx.q, url: '/test.jp2' } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toContain('image/jp2') expect(res.headers.get('Cache-Control')).toBe( @@ -339,7 +340,7 @@ export function runTests(ctx: RunTestsCtx) { it('should maintain animated gif', async () => { const query = { w: ctx.w, q: ctx.q, url: '/animated.gif' } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(200) expect(res.headers.get('content-type')).toContain('image/gif') expect(res.headers.get('Cache-Control')).toBe( @@ -351,12 +352,12 @@ export function runTests(ctx: RunTestsCtx) { `${contentDispositionType}; filename="animated.gif"` ) await expectWidth(res, 50, { expectAnimated: true }) - expect(ctx.nextOutput).toContain(animatedWarnText) + expect(next.cliOutput).toContain(animatedWarnText) }) it('should maintain animated png', async () => { const query = { w: ctx.w, q: ctx.q, url: '/animated.png' } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(200) expect(res.headers.get('content-type')).toContain('image/png') expect(res.headers.get('Cache-Control')).toBe( @@ -368,12 +369,12 @@ export function runTests(ctx: RunTestsCtx) { `${contentDispositionType}; filename="animated.png"` ) await expectWidth(res, 100, { expectAnimated: true }) - expect(ctx.nextOutput).toContain(animatedWarnText) + expect(next.cliOutput).toContain(animatedWarnText) }) it('should maintain animated png 2', async () => { const query = { w: ctx.w, q: ctx.q, url: '/animated2.png' } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(200) expect(res.headers.get('content-type')).toContain('image/png') expect(res.headers.get('Cache-Control')).toBe( @@ -385,12 +386,12 @@ export function runTests(ctx: RunTestsCtx) { `${contentDispositionType}; filename="animated2.png"` ) await expectWidth(res, 1105, { expectAnimated: true }) - expect(ctx.nextOutput).toContain(animatedWarnText) + expect(next.cliOutput).toContain(animatedWarnText) }) it('should maintain animated webp', async () => { const query = { w: ctx.w, q: ctx.q, url: '/animated.webp' } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(200) expect(res.headers.get('content-type')).toContain('image/webp') expect(res.headers.get('Cache-Control')).toBe( @@ -402,13 +403,13 @@ export function runTests(ctx: RunTestsCtx) { `${contentDispositionType}; filename="animated.webp"` ) await expectWidth(res, 400, { expectAnimated: true }) - expect(ctx.nextOutput).toContain(animatedWarnText) + expect(next.cliOutput).toContain(animatedWarnText) }) it('should not forward cookie header', async () => { const query = { w: ctx.w, q: ctx.q, url: '/api/conditional-cookie' } const opts = { headers: { accept: 'image/webp', cookie: '1' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(400) }) @@ -416,7 +417,7 @@ export function runTests(ctx: RunTestsCtx) { it('should maintain vector svg', async () => { const query = { w: ctx.w, q: ctx.q, url: '/test.svg' } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Length')).toBe('603') expect(res.headers.get('Content-Type')).toContain('image/svg+xml') @@ -432,7 +433,7 @@ export function runTests(ctx: RunTestsCtx) { ) const actual = await res.text() const expected = await fs.readFile( - join(ctx.appDir, 'public', 'test.svg'), + join(next.testDir, 'public', 'test.svg'), 'utf8' ) expect(actual).toMatch(expected) @@ -441,7 +442,7 @@ export function runTests(ctx: RunTestsCtx) { it('should not allow vector svg', async () => { const query = { w: ctx.w, q: ctx.q, url: '/test.svg' } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(400) expect(await res.text()).toContain('valid but image type is not allowed') }) @@ -449,7 +450,7 @@ export function runTests(ctx: RunTestsCtx) { it('should not allow svg with application header', async () => { const query = { w: ctx.w, q: ctx.q, url: '/api/application.svg' } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(400) expect(await res.text()).toContain( "The requested resource isn't a valid image" @@ -459,7 +460,7 @@ export function runTests(ctx: RunTestsCtx) { it('should not allow svg with comma header', async () => { const query = { w: ctx.w, q: ctx.q, url: '/api/comma.svg' } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(400) expect(await res.text()).toContain( "The requested resource isn't a valid image" @@ -469,7 +470,7 @@ export function runTests(ctx: RunTestsCtx) { it('should not allow svg with uppercase header', async () => { const query = { w: ctx.w, q: ctx.q, url: '/api/uppercase.svg' } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(400) expect(await res.text()).toContain( "The requested resource isn't a valid image" @@ -479,7 +480,7 @@ export function runTests(ctx: RunTestsCtx) { it('should not allow svg with wrong header', async () => { const query = { w: ctx.w, q: ctx.q, url: '/api/wrong-header.svg' } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(400) expect(await res.text()).toContain( '"url" parameter is valid but image type is not allowed' @@ -490,7 +491,7 @@ export function runTests(ctx: RunTestsCtx) { it('should not allow pdf format', async () => { const query = { w: ctx.w, q: ctx.q, url: '/test.pdf' } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(400) expect(await res.text()).toContain( "The requested resource isn't a valid image" @@ -500,7 +501,7 @@ export function runTests(ctx: RunTestsCtx) { it('should maintain ico format', async () => { const query = { w: ctx.w, q: ctx.q, url: `/test.ico` } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toContain('image/x-icon') expect(res.headers.get('Cache-Control')).toBe( @@ -513,7 +514,7 @@ export function runTests(ctx: RunTestsCtx) { ) const actual = await res.text() const expected = await fs.readFile( - join(ctx.appDir, 'public', 'test.ico'), + join(next.testDir, 'public', 'test.ico'), 'utf8' ) expect(actual).toMatch(expected) @@ -524,7 +525,7 @@ export function runTests(ctx: RunTestsCtx) { 'image/png,image/svg+xml,image/*;q=0.8,video/*;q=0.8,*/*;q=0.5' const query = { w: ctx.w, q: ctx.q, url: '/test.jpg' } const opts = { headers: { accept } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toContain('image/jpeg') expect(res.headers.get('Cache-Control')).toBe( @@ -542,7 +543,7 @@ export function runTests(ctx: RunTestsCtx) { 'image/png,image/svg+xml,image/*;q=0.8,video/*;q=0.8,*/*;q=0.5' const query = { w: ctx.w, q: ctx.q, url: '/test.png' } const opts = { headers: { accept } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toContain('image/png') expect(res.headers.get('Cache-Control')).toBe( @@ -560,7 +561,7 @@ export function runTests(ctx: RunTestsCtx) { 'image/png,image/svg+xml,image/*;q=0.8,video/*;q=0.8,*/*;q=0.5' const query = { w: ctx.w, q: ctx.q, url: '/test.webp' } const opts = { headers: { accept } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toContain('image/jpeg') expect(res.headers.get('Cache-Control')).toBe( @@ -579,7 +580,7 @@ export function runTests(ctx: RunTestsCtx) { 'image/png,image/svg+xml,image/*;q=0.8,video/*;q=0.8,*/*;q=0.5' const query = { w: ctx.w, q: ctx.q, url: '/test.avif' } const opts = { headers: { accept } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toContain('image/jpeg') expect(res.headers.get('Cache-Control')).toBe( @@ -595,28 +596,28 @@ export function runTests(ctx: RunTestsCtx) { it('should fail when url is missing', async () => { const query = { w: ctx.w, q: ctx.q } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(400) expect(await res.text()).toBe(`"url" parameter is required`) }) it('should fail when w is missing', async () => { const query = { url: '/test.png', q: ctx.q } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(400) expect(await res.text()).toBe(`"w" parameter (width) is required`) }) it('should fail when q is missing', async () => { const query = { url: '/test.png', w: ctx.w } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(400) expect(await res.text()).toBe(`"q" parameter (quality) is required`) }) it('should fail when q is greater than 100', async () => { const query = { url: '/test.png', w: ctx.w, q: 101 } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(400) expect(await res.text()).toBe( `"q" parameter (quality) must be an integer between 1 and 100` @@ -625,7 +626,7 @@ export function runTests(ctx: RunTestsCtx) { it('should fail when q is less than 1', async () => { const query = { url: '/test.png', w: ctx.w, q: 0 } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(400) expect(await res.text()).toBe( `"q" parameter (quality) must be an integer between 1 and 100` @@ -635,7 +636,7 @@ export function runTests(ctx: RunTestsCtx) { if (ctx?.nextConfigImages?.qualities) { it('should fail when q is not in config', async () => { const query = { url: '/test.png', w: ctx.w, q: 13 } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(400) expect(await res.text()).toBe( `"q" parameter (quality) of 13 is not allowed` @@ -645,7 +646,7 @@ export function runTests(ctx: RunTestsCtx) { it('should fail when w is 0', async () => { const query = { url: '/test.png', w: 0, q: ctx.q } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(400) expect(await res.text()).toBe( `"w" parameter (width) must be an integer greater than 0` @@ -654,7 +655,7 @@ export function runTests(ctx: RunTestsCtx) { it('should fail when w is less than 0', async () => { const query = { url: '/test.png', w: -100, q: ctx.q } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(400) expect(await res.text()).toBe( `"w" parameter (width) must be an integer greater than 0` @@ -663,7 +664,7 @@ export function runTests(ctx: RunTestsCtx) { it('should fail when w is not a number', async () => { const query = { url: '/test.png', w: 'foo', q: ctx.q } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(400) expect(await res.text()).toBe( `"w" parameter (width) must be an integer greater than 0` @@ -672,7 +673,7 @@ export function runTests(ctx: RunTestsCtx) { it('should fail when w is not an integer', async () => { const query = { url: '/test.png', w: 99.9, q: ctx.q } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(400) expect(await res.text()).toBe( `"w" parameter (width) must be an integer greater than 0` @@ -681,7 +682,7 @@ export function runTests(ctx: RunTestsCtx) { it('should fail when q is not a number', async () => { const query = { url: '/test.png', w: ctx.w, q: 'foo' } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(400) expect(await res.text()).toBe( `"q" parameter (quality) must be an integer between 1 and 100` @@ -690,7 +691,7 @@ export function runTests(ctx: RunTestsCtx) { it('should fail when q is not an integer', async () => { const query = { url: '/test.png', w: ctx.w, q: 99.9 } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(400) expect(await res.text()).toBe( `"q" parameter (quality) must be an integer between 1 and 100` @@ -701,7 +702,7 @@ export function runTests(ctx: RunTestsCtx) { const url = `http://vercel.com/button` const query = { url, w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(400) expect(await res.text()).toBe(`"url" parameter is not allowed`) }) @@ -709,7 +710,7 @@ export function runTests(ctx: RunTestsCtx) { it('should fail when width is not in next.config.js', async () => { const query = { url: '/test.png', w: 1000, q: ctx.q } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(400) expect(await res.text()).toBe( `"w" parameter (width) of 1000 is not allowed` @@ -719,7 +720,7 @@ export function runTests(ctx: RunTestsCtx) { it('should emit blur svg when width is 8 in dev but not prod', async () => { const query = { url: '/test.png', w: 8, q: 70 } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) if (isDev) { expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('image/svg+xml') @@ -735,7 +736,7 @@ export function runTests(ctx: RunTestsCtx) { it('should emit blur svg when width is less than 8 in dev but not prod', async () => { const query = { url: '/test.png', w: 3, q: 70 } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) if (isDev) { expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('image/svg+xml') @@ -751,7 +752,7 @@ export function runTests(ctx: RunTestsCtx) { it('should resize relative url and webp Firefox accept header', async () => { const query = { url: '/test.png', w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/webp,*/*' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('image/webp') expect(res.headers.get('Cache-Control')).toBe( @@ -768,7 +769,7 @@ export function runTests(ctx: RunTestsCtx) { it('should resize relative url and png accept header', async () => { const query = { url: '/test.png', w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/png' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('image/png') expect(res.headers.get('Cache-Control')).toBe( @@ -785,7 +786,7 @@ export function runTests(ctx: RunTestsCtx) { it('should resize relative url with invalid accept header as png', async () => { const query = { url: '/test.png', w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/invalid' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('image/png') expect(res.headers.get('Cache-Control')).toBe( @@ -802,7 +803,7 @@ export function runTests(ctx: RunTestsCtx) { it('should resize relative url with invalid accept header as gif', async () => { const query = { url: '/test.gif', w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/invalid' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('image/gif') expect(res.headers.get('Cache-Control')).toBe( @@ -819,7 +820,7 @@ export function runTests(ctx: RunTestsCtx) { it('should resize relative url with invalid accept header as tiff', async () => { const query = { url: '/test.tiff', w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/invalid' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('image/tiff') expect(res.headers.get('Cache-Control')).toBe( @@ -836,7 +837,7 @@ export function runTests(ctx: RunTestsCtx) { it('should resize gif (not animated)', async () => { const query = { url: '/test.gif', w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('image/webp') expect(res.headers.get('Cache-Control')).toBe( @@ -853,7 +854,7 @@ export function runTests(ctx: RunTestsCtx) { it('should resize tiff', async () => { const query = { url: '/test.tiff', w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('image/webp') expect(res.headers.get('Cache-Control')).toBe( @@ -870,7 +871,7 @@ export function runTests(ctx: RunTestsCtx) { it('should resize avif', async () => { const query = { url: '/test.avif', w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('image/webp') expect(res.headers.get('Cache-Control')).toBe( @@ -889,7 +890,7 @@ export function runTests(ctx: RunTestsCtx) { const opts = { headers: { accept: 'image/webp,image/apng,image/*,*/*;q=0.8' }, } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('image/webp') expect(res.headers.get('Cache-Control')).toBe( @@ -911,7 +912,7 @@ export function runTests(ctx: RunTestsCtx) { accept: 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8', }, } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('image/avif') expect(res.headers.get('Cache-Control')).toBe( @@ -928,7 +929,7 @@ export function runTests(ctx: RunTestsCtx) { it('should resize avif and maintain format', async () => { const query = { url: '/test.avif', w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/avif' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('image/avif') expect(res.headers.get('Cache-Control')).toBe( @@ -943,24 +944,24 @@ export function runTests(ctx: RunTestsCtx) { }) it('should compress avif smaller than webp at q=100', async () => { - await expectAvifSmallerThanWebp(ctx.w, 100, ctx.appPort) + await expectAvifSmallerThanWebp(next, ctx.w, 100) }) it('should compress avif smaller than webp at q=75', async () => { - await expectAvifSmallerThanWebp(ctx.w, 75, ctx.appPort) + await expectAvifSmallerThanWebp(next, ctx.w, 75) }) it('should compress avif smaller than webp at q=50', async () => { - await expectAvifSmallerThanWebp(ctx.w, 50, ctx.appPort) + await expectAvifSmallerThanWebp(next, ctx.w, 50) }) } if (domains.length > 0 && dangerouslyAllowLocalIP) { it('should resize absolute url from localhost', async () => { - const url = `http://localhost:${ctx.appPort}/test.png` + const url = `http://localhost:${new URL(next.url).port}/test.png` const query = { url, w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('image/webp') expect(res.headers.get('Cache-Control')).toBe( @@ -976,14 +977,14 @@ export function runTests(ctx: RunTestsCtx) { it('should automatically detect image type when content-type is octet-stream', async () => { const url = '/png-as-octet-stream' - const resOrig = await fetchViaHTTP(ctx.appPort, url) + const resOrig = await next.fetch(url) expect(resOrig.status).toBe(200) expect(resOrig.headers.get('Content-Type')).toBe( 'application/octet-stream' ) const query = { url, w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('image/webp') expect(res.headers.get('Cache-Control')).toBe( @@ -1011,12 +1012,7 @@ export function runTests(ctx: RunTestsCtx) { const query = { url, w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/webp' } } - const one = await fetchWithDuration( - ctx.appPort, - '/_next/image', - query, - opts - ) + const one = await fetchWithDuration(next, '/_next/image', query, opts) expect(one.duration).toBeGreaterThan(delay) expect(one.res.status).toBe(200) expect(one.res.headers.get('X-Nextjs-Cache')).toBe('MISS') @@ -1029,7 +1025,7 @@ export function runTests(ctx: RunTestsCtx) { let json1 await check(async () => { - json1 = await fsToJson(ctx.imagesDir) + json1 = await fsToJson(imagesDir) return Object.keys(json1).some((dir) => { return Object.keys(json1[dir]).some((file) => file.includes(etagOne)) }) @@ -1037,19 +1033,14 @@ export function runTests(ctx: RunTestsCtx) { : 'fail' }, 'success') - const two = await fetchWithDuration( - ctx.appPort, - '/_next/image', - query, - opts - ) + const two = await fetchWithDuration(next, '/_next/image', query, opts) expect(two.res.status).toBe(200) expect(two.res.headers.get('X-Nextjs-Cache')).toBe('HIT') expect(two.res.headers.get('Content-Type')).toBe('image/webp') expect(two.res.headers.get('Content-Disposition')).toBe( `${contentDispositionType}; filename="slow.webp"` ) - const json2 = await fsToJson(ctx.imagesDir) + const json2 = await fsToJson(imagesDir) expect(json2).toStrictEqual(json1) if (ctx.nextConfigImages?.minimumCacheTTL) { @@ -1057,8 +1048,8 @@ export function runTests(ctx: RunTestsCtx) { await waitFor(ctx.nextConfigImages.minimumCacheTTL * 1000) const [three, four] = await Promise.all([ - fetchWithDuration(ctx.appPort, '/_next/image', query, opts), - fetchWithDuration(ctx.appPort, '/_next/image', query, opts), + fetchWithDuration(next, '/_next/image', query, opts), + fetchWithDuration(next, '/_next/image', query, opts), ]) expect(three.duration).toBeLessThan(one.duration) @@ -1078,7 +1069,7 @@ export function runTests(ctx: RunTestsCtx) { ) await check(async () => { - const json4 = await fsToJson(ctx.imagesDir) + const json4 = await fsToJson(imagesDir) try { assert.deepStrictEqual(json4, json1) return 'fail' @@ -1087,12 +1078,7 @@ export function runTests(ctx: RunTestsCtx) { } }, 'success') - const five = await fetchWithDuration( - ctx.appPort, - '/_next/image', - query, - opts - ) + const five = await fetchWithDuration(next, '/_next/image', query, opts) // expect(five.duration).toBeLessThan(one.duration) // TODO: investigate why this timing varies randomly expect(five.res.status).toBe(200) expect(five.res.headers.get('X-Nextjs-Cache')).toBe('HIT') @@ -1101,7 +1087,7 @@ export function runTests(ctx: RunTestsCtx) { `${contentDispositionType}; filename="slow.webp"` ) await check(async () => { - const json5 = await fsToJson(ctx.imagesDir) + const json5 = await fsToJson(imagesDir) try { assert.deepStrictEqual(json5, json1) return 'fail' @@ -1114,33 +1100,33 @@ export function runTests(ctx: RunTestsCtx) { } it('should fail when url has file protocol', async () => { - const url = `file://example.vercel.sh:${ctx.appPort}/test.png` + const url = `file://example.vercel.sh:${new URL(next.url).port}/test.png` const query = { url, w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(400) expect(await res.text()).toBe(`"url" parameter is invalid`) }) it('should fail when url has ftp protocol', async () => { - const url = `ftp://example.vercel.sh:${ctx.appPort}/test.png` + const url = `ftp://example.vercel.sh:${new URL(next.url).port}/test.png` const query = { url, w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(400) expect(await res.text()).toBe(`"url" parameter is invalid`) }) it('should fail when url is too long', async () => { const query = { url: `/${'a'.repeat(4000)}`, w: ctx.w, q: ctx.q } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(400) expect(await res.text()).toBe(`"url" parameter is too long`) }) it('should fail when url is protocol relative', async () => { const query = { url: `//example.vercel.sh`, w: ctx.w, q: ctx.q } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(400) expect(await res.text()).toBe( `"url" parameter cannot be a protocol-relative URL (//)` @@ -1154,7 +1140,7 @@ export function runTests(ctx: RunTestsCtx) { w: ctx.w, q: ctx.q, } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(400) expect(await res.text()).toBe(`"url" parameter cannot be recursive`) }) @@ -1165,7 +1151,7 @@ export function runTests(ctx: RunTestsCtx) { w: ctx.w, q: ctx.q, } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(400) expect(await res.text()).toBe(`"url" parameter is invalid`) }) @@ -1175,7 +1161,7 @@ export function runTests(ctx: RunTestsCtx) { const fullUrl = 'https://image-optimization-test.vercel.app/_next/image?url=%2Ffrog.jpg&w=1024&q=75' const query = { url: fullUrl, w: ctx.w, q: ctx.q } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(200) await expectWidth(res, ctx.w) }) @@ -1184,7 +1170,7 @@ export function runTests(ctx: RunTestsCtx) { const fullUrl = 'https://image-optimization-test.vercel.app/_next/image?url=%2Ffrog.jpg&w=1024&q=75' const query = { url: fullUrl, w: ctx.w, q: ctx.q } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(400) expect(await res.text()).toBe(`"url" parameter is not allowed`) }) @@ -1193,7 +1179,7 @@ export function runTests(ctx: RunTestsCtx) { it('should fail with relative image url with assetPrefix', async () => { const fullUrl = '/assets/_next/image?url=%2Ftest.png&w=128&q=75' const query = { url: fullUrl, w: ctx.w, q: ctx.q } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(400) expect(await res.text()).toBe(`"url" parameter cannot be recursive`) }) @@ -1203,7 +1189,7 @@ export function runTests(ctx: RunTestsCtx) { const url = `/api/no-header` const query = { url, w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(400) expect(await res.text()).toContain( "The requested resource isn't a valid image" @@ -1212,9 +1198,9 @@ export function runTests(ctx: RunTestsCtx) { if (domains.length > 0 && dangerouslyAllowLocalIP) { it('should fail when url fails to load an image', async () => { - const url = `http://localhost:${ctx.appPort}/not-an-image` + const url = `http://localhost:${new URL(next.url).port}/not-an-image` const query = { w: ctx.w, url, q: ctx.q } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`) expect(res.status).toBe(404) expect(await res.text()).toBe( `"url" parameter is valid but upstream response is invalid` @@ -1238,12 +1224,7 @@ export function runTests(ctx: RunTestsCtx) { } const opts = { headers: { accept: 'image/webp' } } - const one = await fetchWithDuration( - ctx.appPort, - '/_next/image', - query, - opts - ) + const one = await fetchWithDuration(next, '/_next/image', query, opts) expect(one.res.status).toBe(200) expect(one.res.headers.get('X-Nextjs-Cache')).toBe('MISS') expect(one.res.headers.get('Content-Type')).toBe('image/webp') @@ -1254,7 +1235,7 @@ export function runTests(ctx: RunTestsCtx) { let json1 await check(async () => { - json1 = await fsToJson(ctx.imagesDir) + json1 = await fsToJson(imagesDir) return Object.keys(json1).some((dir) => { return Object.keys(json1[dir]).some((file) => file.includes(etagOne)) }) @@ -1262,19 +1243,14 @@ export function runTests(ctx: RunTestsCtx) { : 'fail' }, 'success') - const two = await fetchWithDuration( - ctx.appPort, - '/_next/image', - query, - opts - ) + const two = await fetchWithDuration(next, '/_next/image', query, opts) expect(two.res.status).toBe(200) expect(two.res.headers.get('X-Nextjs-Cache')).toBe('HIT') expect(two.res.headers.get('Content-Type')).toBe('image/webp') expect(two.res.headers.get('Content-Disposition')).toBe( `${contentDispositionType}; filename="test.webp"` ) - const json2 = await fsToJson(ctx.imagesDir) + const json2 = await fsToJson(imagesDir) expect(json2).toStrictEqual(json1) if (ctx.nextConfigImages?.minimumCacheTTL) { @@ -1282,8 +1258,8 @@ export function runTests(ctx: RunTestsCtx) { await waitFor(ctx.nextConfigImages.minimumCacheTTL * 1000) const [three, four] = await Promise.all([ - fetchWithDuration(ctx.appPort, '/_next/image', query, opts), - fetchWithDuration(ctx.appPort, '/_next/image', query, opts), + fetchWithDuration(next, '/_next/image', query, opts), + fetchWithDuration(next, '/_next/image', query, opts), ]) expect(three.res.status).toBe(200) @@ -1300,7 +1276,7 @@ export function runTests(ctx: RunTestsCtx) { `${contentDispositionType}; filename="test.webp"` ) await check(async () => { - const json3 = await fsToJson(ctx.imagesDir) + const json3 = await fsToJson(imagesDir) try { assert.deepStrictEqual(json3, json1) return 'fail' @@ -1309,12 +1285,7 @@ export function runTests(ctx: RunTestsCtx) { } }, 'success') - const five = await fetchWithDuration( - ctx.appPort, - '/_next/image', - query, - opts - ) + const five = await fetchWithDuration(next, '/_next/image', query, opts) // expect(five.duration).toBeLessThan(one.duration) // TODO: investigate why this timing varies randomly expect(five.res.status).toBe(200) expect(five.res.headers.get('X-Nextjs-Cache')).toBe('HIT') @@ -1323,7 +1294,7 @@ export function runTests(ctx: RunTestsCtx) { `${contentDispositionType}; filename="test.webp"` ) await check(async () => { - const json5 = await fsToJson(ctx.imagesDir) + const json5 = await fsToJson(imagesDir) try { assert.deepStrictEqual(json5, json1) return 'fail' @@ -1341,7 +1312,10 @@ export function runTests(ctx: RunTestsCtx) { const query = { url: '/test.svg', w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/webp' } } - const res1 = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res1 = await next.fetch( + `/_next/image?${toQueryString(query)}`, + opts + ) expect(res1.status).toBe(200) expect(res1.headers.get('X-Nextjs-Cache')).toBe('MISS') expect(res1.headers.get('Content-Type')).toBe('image/svg+xml') @@ -1352,7 +1326,7 @@ export function runTests(ctx: RunTestsCtx) { let json1 await check(async () => { - json1 = await fsToJson(ctx.imagesDir) + json1 = await fsToJson(imagesDir) return Object.keys(json1).some((dir) => { return Object.keys(json1[dir]).some((file) => file.includes(etagOne)) }) @@ -1360,14 +1334,17 @@ export function runTests(ctx: RunTestsCtx) { : 'fail' }, 'success') - const res2 = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res2 = await next.fetch( + `/_next/image?${toQueryString(query)}`, + opts + ) expect(res2.status).toBe(200) expect(res2.headers.get('X-Nextjs-Cache')).toBe('HIT') expect(res2.headers.get('Content-Type')).toBe('image/svg+xml') expect(res2.headers.get('Content-Disposition')).toBe( `${contentDispositionType}; filename="test.svg"` ) - const json2 = await fsToJson(ctx.imagesDir) + const json2 = await fsToJson(imagesDir) expect(json2).toStrictEqual(json1) }) } @@ -1384,7 +1361,7 @@ export function runTests(ctx: RunTestsCtx) { const query = { url: '/animated.gif', w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/webp' } } - const res1 = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res1 = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res1.status).toBe(200) expect(res1.headers.get('X-Nextjs-Cache')).toBe('MISS') expect(res1.headers.get('Content-Type')).toBe('image/gif') @@ -1394,18 +1371,18 @@ export function runTests(ctx: RunTestsCtx) { let json1 await check(async () => { - json1 = await fsToJson(ctx.imagesDir) + json1 = await fsToJson(imagesDir) return Object.keys(json1).length === 1 ? 'success' : 'fail' }, 'success') - const res2 = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res2 = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res2.status).toBe(200) expect(res2.headers.get('X-Nextjs-Cache')).toBe('HIT') expect(res2.headers.get('Content-Type')).toBe('image/gif') expect(res2.headers.get('Content-Disposition')).toBe( `${contentDispositionType}; filename="animated.gif"` ) - const json2 = await fsToJson(ctx.imagesDir) + const json2 = await fsToJson(imagesDir) expect(json2).toStrictEqual(json1) }) @@ -1413,7 +1390,7 @@ export function runTests(ctx: RunTestsCtx) { const query = { url: '/test.jpg', w: ctx.w, q: ctx.q } const opts1 = { headers: { accept: 'image/webp' } } - const res1 = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts1) + const res1 = await next.fetch(`/_next/image?${toQueryString(query)}`, opts1) expect(res1.status).toBe(200) expect(res1.headers.get('Content-Type')).toBe('image/webp') expect(res1.headers.get('Cache-Control')).toBe( @@ -1428,7 +1405,7 @@ export function runTests(ctx: RunTestsCtx) { await expectWidth(res1, ctx.w) const opts2 = { headers: { accept: 'image/webp', 'if-none-match': etag } } - const res2 = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts2) + const res2 = await next.fetch(`/_next/image?${toQueryString(query)}`, opts2) expect(res2.status).toBe(304) expect(res2.headers.get('Content-Type')).toBeFalsy() expect(res2.headers.get('Etag')).toBe(etag) @@ -1437,15 +1414,13 @@ export function runTests(ctx: RunTestsCtx) { ) expect(res2.headers.get('Vary')).toBe('Accept') expect(res2.headers.get('Content-Disposition')).toBeFalsy() - expect((await res2.buffer()).length).toBe(0) + expect((await res2.arrayBuffer()).byteLength).toBe(0) if (ctx?.nextConfigImages?.qualities) { const q = ctx.nextConfigImages.qualities[0] const query3 = { url: '/test.jpg', w: ctx.w, q } - const res3 = await fetchViaHTTP( - ctx.appPort, - '/_next/image', - query3, + const res3 = await next.fetch( + `/_next/image?${toQueryString(query3)}`, opts2 ) expect(res3.status).toBe(200) @@ -1464,12 +1439,12 @@ export function runTests(ctx: RunTestsCtx) { }) it('should maintain bmp', async () => { - const json1 = await fsToJson(ctx.imagesDir) + const json1 = await fsToJson(imagesDir) expect(json1).toBeTruthy() const query = { url: '/test.bmp', w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/invalid' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('image/bmp') expect(res.headers.get('Cache-Control')).toBe( @@ -1488,11 +1463,11 @@ export function runTests(ctx: RunTestsCtx) { ctx.nextConfigImages?.maximumDiskCacheSize === 0 ) { expect(json1).toEqual({}) - expect(await fsToJson(ctx.imagesDir)).toEqual({}) + expect(await fsToJson(imagesDir)).toEqual({}) } else { await check(async () => { try { - assert.deepStrictEqual(await fsToJson(ctx.imagesDir), json1) + assert.deepStrictEqual(await fsToJson(imagesDir), json1) return 'expected change, but matched' } catch (_) { return 'success' @@ -1504,7 +1479,7 @@ export function runTests(ctx: RunTestsCtx) { it('should not resize if requested width is larger than original source image', async () => { const query = { url: '/test.jpg', w: largeSize, q: ctx.q } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('image/webp') expect(res.headers.get('Cache-Control')).toBe( @@ -1519,8 +1494,8 @@ export function runTests(ctx: RunTestsCtx) { }) it('should set cache-control to immutable for static images', async () => { - if (!ctx.isDev) { - const file = (await listClientChunks(join(ctx.appDir, '.next'))).find( + if (!isDev) { + const file = (await listClientChunks(join(next.testDir, '.next'))).find( (f) => /\/test\.[0-9a-z_-]+\.jpg$/.test(f) ) expect(file).toBeString() @@ -1529,13 +1504,16 @@ export function runTests(ctx: RunTestsCtx) { w: String(ctx.w), q: String(ctx.q), } - const assetToken = getDeploymentId(ctx.appDir, false).assetToken + const assetToken = getDeploymentId(next.testDir, false).assetToken if (assetToken) { query.dpl = assetToken } const opts = { headers: { accept: 'image/webp' } } - const res1 = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res1 = await next.fetch( + `/_next/image?${toQueryString(query)}`, + opts + ) expect(res1.status).toBe(200) expect(res1.headers.get('Cache-Control')).toBe( 'public, max-age=315360000, immutable' @@ -1547,7 +1525,10 @@ export function runTests(ctx: RunTestsCtx) { await expectWidth(res1, ctx.w) // Ensure subsequent request also has immutable header - const res2 = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res2 = await next.fetch( + `/_next/image?${toQueryString(query)}`, + opts + ) expect(res2.status).toBe(200) expect(res2.headers.get('Cache-Control')).toBe( 'public, max-age=315360000, immutable' @@ -1563,7 +1544,7 @@ export function runTests(ctx: RunTestsCtx) { it("should error if the resource isn't a valid image", async () => { const query = { url: '/test.txt', w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(400) expect(await res.text()).toBe("The requested resource isn't a valid image.") }) @@ -1571,14 +1552,14 @@ export function runTests(ctx: RunTestsCtx) { it('should error if the image file does not exist', async () => { const query = { url: '/does_not_exist.jpg', w: ctx.w, q: ctx.q } const opts = { headers: { accept: 'image/webp' } } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch(`/_next/image?${toQueryString(query)}`, opts) expect(res.status).toBe(400) expect(await res.text()).toBe("The requested resource isn't a valid image.") }) if (domains.length > 0 && dangerouslyAllowLocalIP) { it('should handle concurrent requests', async () => { - await cleanImagesDir(ctx.imagesDir) + await cleanImagesDir(imagesDir) const delay = 500 const query = { url: `http://localhost:${slowImageServer.port}/slow.png?delay=${delay}`, @@ -1587,9 +1568,9 @@ export function runTests(ctx: RunTestsCtx) { } const opts = { headers: { accept: 'image/webp,*/*' } } const [res1, res2, res3] = await Promise.all([ - fetchViaHTTP(ctx.appPort, '/_next/image', query, opts), - fetchViaHTTP(ctx.appPort, '/_next/image', query, opts), - fetchViaHTTP(ctx.appPort, '/_next/image', query, opts), + next.fetch(`/_next/image?${toQueryString(query)}`, opts), + next.fetch(`/_next/image?${toQueryString(query)}`, opts), + next.fetch(`/_next/image?${toQueryString(query)}`, opts), ]) if (res1.status !== 200) { @@ -1624,7 +1605,7 @@ export function runTests(ctx: RunTestsCtx) { : 1 await check(async () => { - const json1 = await fsToJson(ctx.imagesDir) + const json1 = await fsToJson(imagesDir) return Object.keys(json1).length === length ? 'success' : 'fail' }, 'success') @@ -1657,20 +1638,24 @@ export function runTests(ctx: RunTestsCtx) { { url: '/animated2.png', w: largeSize }, ] await cleanImagesDir(imagesDir) - const json1 = await fsToJson(ctx.imagesDir) + const json1 = await fsToJson(imagesDir) expect(Object.keys(json1).length).toEqual(0) for (const { url, w } of requests) { const query = { url, w, q: ctx.q } - const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + const res = await next.fetch( + `/_next/image?${toQueryString(query)}`, + opts + ) expect(res.status).toBe(200) - await res.buffer() // consume response body + await res.arrayBuffer() + // eslint-disable-next-line no-loop-func await retry(async () => { const size = await getDirSize(imagesDir) expect(size).toBeLessThanOrEqual(maximumDiskCacheSize) }) } - const json2 = await fsToJson(ctx.imagesDir) + const json2 = await fsToJson(imagesDir) const json2Length = Object.keys(json2).length if (maximumDiskCacheSize === 0) { expect(json2Length).toEqual(0) @@ -1678,16 +1663,14 @@ export function runTests(ctx: RunTestsCtx) { expect(json2Length).toBeGreaterThan(0) } - const res = await fetchViaHTTP( - ctx.appPort, - '/_next/image', - { url: '/mountains.jpg', w: ctx.w, q: ctx.q }, + const res = await next.fetch( + `/_next/image?${toQueryString({ url: '/mountains.jpg', w: ctx.w, q: ctx.q })}`, opts ) expect(res.status).toBe(200) await retry(async () => { - const json3 = await fsToJson(ctx.imagesDir) + const json3 = await fsToJson(imagesDir) const json3Length = Object.keys(json3).length if (maximumDiskCacheSize === 0) { expect(json3Length).toEqual(0) @@ -1703,205 +1686,76 @@ export function runTests(ctx: RunTestsCtx) { } export const setupTests = (ctx: SetupTestsCtx) => { - const nextConfig = new File(join(ctx.appDir, 'next.config.js')) - - const originalIsNextDev = (global as any).isNextDev - afterAll(() => { - ;(global as any).isNextDev = originalIsNextDev - }) - - describe('dev support w/o next.config.js', () => { - if (ctx.nextConfigImages) { - // skip this test because it requires next.config.js - return - } - - const isDev = true - // Set global.isNextDev for getDistDir() - ;(global as any).isNextDev = isDev - const imagesDir = join(ctx.appDir, getDistDir(), 'cache', 'images') - const size = 384 // defaults defined in server/config.ts - const curCtx: RunTestsCtx = { - ...ctx, - w: size, - q: 75, - isDev, - imagesDir, - } - - beforeAll(async () => { - const json = JSON.stringify({ - // See https://github.com/vercel/next.js/pull/60972 - outputFileTracingRoot: join(__dirname, '../../../..'), - experimental: curCtx.nextConfigExperimental, - } satisfies NextConfig) - nextConfig.replace('{ /* replaceme */ }', json) - curCtx.nextOutput = '' - curCtx.appPort = await findPort() - curCtx.app = await launchApp(curCtx.appDir, curCtx.appPort, { - onStderr(msg) { - curCtx.nextOutput += msg - }, - cwd: curCtx.appDir, + const maybeSkipTurbopackProd = + shouldUseTurbopack() && !isNextDev ? describe.skip : describe + + if (!ctx.nextConfigImages) { + maybeSkipTurbopackProd('w/o next.config.js', () => { + const size = 384 + const { next, isNextDeploy } = nextTestSetup({ + files: join(__dirname, 'app'), + nextConfig: ctx.nextConfigExperimental + ? { experimental: ctx.nextConfigExperimental } + : undefined, + // The image optimizer suite asserts on the local Next.js image + // pipeline (custom upstream HTTP server, on-disk cache, response + // headers from `/_next/image`). Vercel's deploy serves images + // through its own image CDN with different headers, paths, and + // cache semantics, so these assertions don't apply. + skipDeployment: true, }) - await cleanImagesDir(imagesDir) - }) - afterAll(async () => { - nextConfig.restore() - if (curCtx.app) await killApp(curCtx.app) - }) + if (isNextDeploy) return - runTests(curCtx) - }) - - describe('dev support with next.config.js', () => { - const isDev = true - // Set global.isNextDev for getDistDir() - ;(global as any).isNextDev = isDev - const imagesDir = join(ctx.appDir, getDistDir(), 'cache', 'images') - const size = 400 - const curCtx: RunTestsCtx = { - ...ctx, - w: size, - q: 100, - isDev, - nextConfigImages: { - dangerouslyAllowLocalIP: true, - domains: [ - 'localhost', - '127.0.0.1', - 'example.com', - 'assets.vercel.com', - 'image-optimization-test.vercel.app', - ], - formats: ['image/avif', 'image/webp'] as any, - deviceSizes: [largeSize], - imageSizes: [size], - qualities: [50, 75, 100], - ...ctx.nextConfigImages, - }, - imagesDir, - } - beforeAll(async () => { - const json = JSON.stringify({ - // See https://github.com/vercel/next.js/pull/60972 - outputFileTracingRoot: join(__dirname, '../../../..'), - images: curCtx.nextConfigImages, - experimental: curCtx.nextConfigExperimental, - } satisfies NextConfig) - curCtx.nextOutput = '' - nextConfig.replace('{ /* replaceme */ }', json) - await cleanImagesDir(imagesDir) - curCtx.appPort = await findPort() - curCtx.app = await launchApp(curCtx.appDir, curCtx.appPort, { - onStderr(msg) { - curCtx.nextOutput += msg - }, - cwd: curCtx.appDir, + runTests({ + next, + w: size, + q: 75, + nextConfigExperimental: ctx.nextConfigExperimental, }) }) - afterAll(async () => { - nextConfig.restore() - if (curCtx.app) await killApp(curCtx.app) - }) + } - runTests(curCtx) - }) - ;(process.env.TURBOPACK_DEV || process.env.TURBOPACK_BUILD - ? describe.skip - : describe)('Production Mode Server support w/o next.config.js', () => { - if (ctx.nextConfigImages) { - // skip this test because it requires next.config.js - return - } - const isDev = false - ;(global as any).isNextDev = isDev - const imagesDir = join(ctx.appDir, getDistDir(), 'cache', 'images') - const size = 384 // defaults defined in server/config.ts - const curCtx: RunTestsCtx = { - ...ctx, - w: size, - q: 75, - isDev, - imagesDir, + maybeSkipTurbopackProd('with next.config.js', () => { + const w = isNextDev ? 400 : 399 + const mergedImages = { + dangerouslyAllowLocalIP: true, + domains: [ + 'localhost', + '127.0.0.1', + 'example.com', + 'assets.vercel.com', + 'image-optimization-test.vercel.app', + ], + formats: ['image/avif', 'image/webp'] as any, + deviceSizes: isNextDev ? [largeSize] : [w, largeSize], + ...(isNextDev ? { imageSizes: [w] } : {}), + qualities: [50, 75, 100], + ...ctx.nextConfigImages, } - beforeAll(async () => { - const json = JSON.stringify({ - // See https://github.com/vercel/next.js/pull/60972 - outputFileTracingRoot: join(__dirname, '../../../..'), - experimental: curCtx.nextConfigExperimental, - } satisfies NextConfig) - nextConfig.replace('{ /* replaceme */ }', json) - curCtx.nextOutput = '' - await nextBuild(curCtx.appDir) - await cleanImagesDir(imagesDir) - curCtx.appPort = await findPort() - curCtx.app = await nextStart(curCtx.appDir, curCtx.appPort, { - onStderr(msg) { - curCtx.nextOutput += msg - }, - cwd: curCtx.appDir, - }) - }) - afterAll(async () => { - nextConfig.restore() - if (curCtx.app) await killApp(curCtx.app) - }) - runTests(curCtx) - }) - ;(process.env.TURBOPACK_DEV || process.env.TURBOPACK_BUILD - ? describe.skip - : describe)('Production Mode Server support with next.config.js', () => { - const isDev = false - ;(global as any).isNextDev = isDev - const imagesDir = join(ctx.appDir, getDistDir(), 'cache', 'images') - const size = 399 - const curCtx: RunTestsCtx = { - ...ctx, - w: size, - q: 100, - isDev, - nextConfigImages: { - dangerouslyAllowLocalIP: true, - domains: [ - 'localhost', - '127.0.0.1', - 'example.com', - 'assets.vercel.com', - 'image-optimization-test.vercel.app', - ], - formats: ['image/avif', 'image/webp'] as any, - deviceSizes: [size, largeSize], - qualities: [50, 75, 100], - ...ctx.nextConfigImages, + const { next, isNextDeploy } = nextTestSetup({ + files: join(__dirname, 'app'), + nextConfig: { + images: mergedImages, + ...(ctx.nextConfigExperimental + ? { experimental: ctx.nextConfigExperimental } + : {}), }, - imagesDir, - } - beforeAll(async () => { - const json = JSON.stringify({ - // See https://github.com/vercel/next.js/pull/60972 - outputFileTracingRoot: join(__dirname, '../../../..'), - images: curCtx.nextConfigImages, - experimental: curCtx.nextConfigExperimental, - } satisfies NextConfig) - curCtx.nextOutput = '' - nextConfig.replace('{ /* replaceme */ }', json) - await nextBuild(curCtx.appDir) - await cleanImagesDir(imagesDir) - curCtx.appPort = await findPort() - curCtx.app = await nextStart(curCtx.appDir, curCtx.appPort, { - onStderr(msg) { - curCtx.nextOutput += msg - }, - cwd: curCtx.appDir, - }) - }) - afterAll(async () => { - nextConfig.restore() - if (curCtx.app) await killApp(curCtx.app) + // The image optimizer suite asserts on the local Next.js image + // pipeline (custom upstream HTTP server, on-disk cache, response + // headers from `/_next/image`). Vercel's deploy serves images + // through its own image CDN with different headers, paths, and + // cache semantics, so these assertions don't apply. + skipDeployment: true, }) + if (isNextDeploy) return - runTests(curCtx) + runTests({ + next, + w, + q: 100, + nextConfigImages: mergedImages, + nextConfigExperimental: ctx.nextConfigExperimental, + }) }) } diff --git a/test/integration/import-assertion/data b/test/e2e/import-assertion/data similarity index 100% rename from test/integration/import-assertion/data rename to test/e2e/import-assertion/data diff --git a/test/integration/import-assertion/data.d.ts b/test/e2e/import-assertion/data.d.ts similarity index 100% rename from test/integration/import-assertion/data.d.ts rename to test/e2e/import-assertion/data.d.ts diff --git a/test/e2e/import-assertion/import-assertion.test.ts b/test/e2e/import-assertion/import-assertion.test.ts new file mode 100644 index 000000000000..3f8ab4cc3b87 --- /dev/null +++ b/test/e2e/import-assertion/import-assertion.test.ts @@ -0,0 +1,16 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('import-assertion', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should handle json assertions', async () => { + const esHtml = await next.render('/es') + const tsHtml = await next.render('/ts') + // checking json value `foo` is not suffecient, since parse error + // will include code stack include those values as source + expect(esHtml).toContain(`<div id="__next">foo</div>`) + expect(tsHtml).toContain(`<div id="__next">foo</div>`) + }) +}) diff --git a/test/integration/import-attributes/pages/es.js b/test/e2e/import-assertion/pages/es.js similarity index 100% rename from test/integration/import-attributes/pages/es.js rename to test/e2e/import-assertion/pages/es.js diff --git a/test/integration/import-attributes/pages/ts.ts b/test/e2e/import-assertion/pages/ts.ts similarity index 100% rename from test/integration/import-attributes/pages/ts.ts rename to test/e2e/import-assertion/pages/ts.ts diff --git a/test/integration/import-attributes/data b/test/e2e/import-attributes/data similarity index 100% rename from test/integration/import-attributes/data rename to test/e2e/import-attributes/data diff --git a/test/integration/import-attributes/data.d.ts b/test/e2e/import-attributes/data.d.ts similarity index 100% rename from test/integration/import-attributes/data.d.ts rename to test/e2e/import-attributes/data.d.ts diff --git a/test/e2e/import-attributes/import-attributes.test.ts b/test/e2e/import-attributes/import-attributes.test.ts new file mode 100644 index 000000000000..23c67188a945 --- /dev/null +++ b/test/e2e/import-attributes/import-attributes.test.ts @@ -0,0 +1,16 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('import-attributes', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should handle json attributes', async () => { + const esHtml = await next.render('/es') + const tsHtml = await next.render('/ts') + // checking json value `foo` is not suffecient, since parse error + // will include code stack include those values as source + expect(esHtml).toContain(`<div id="__next">foo</div>`) + expect(tsHtml).toContain(`<div id="__next">foo</div>`) + }) +}) diff --git a/test/integration/import-assertion/pages/es.js b/test/e2e/import-attributes/pages/es.js similarity index 50% rename from test/integration/import-assertion/pages/es.js rename to test/e2e/import-attributes/pages/es.js index 88e860399cbe..d88a3a6eac4d 100644 --- a/test/integration/import-assertion/pages/es.js +++ b/test/e2e/import-attributes/pages/es.js @@ -1,4 +1,4 @@ -import data from '../data' assert { type: 'json' } +import data from '../data' with { type: 'json' } export default function Es() { return data.foo diff --git a/test/integration/import-assertion/pages/ts.ts b/test/e2e/import-attributes/pages/ts.ts similarity index 50% rename from test/integration/import-assertion/pages/ts.ts rename to test/e2e/import-attributes/pages/ts.ts index fe4eb6dc2acc..fc238dfb17cb 100644 --- a/test/integration/import-assertion/pages/ts.ts +++ b/test/e2e/import-attributes/pages/ts.ts @@ -1,4 +1,4 @@ -import data from '../data' assert { type: 'json' } +import data from '../data' with { type: 'json' } export default function Ts() { return data.foo diff --git a/test/e2e/index-index/index-index.test.ts b/test/e2e/index-index/index-index.test.ts new file mode 100644 index 000000000000..5c77d17bc7cb --- /dev/null +++ b/test/e2e/index-index/index-index.test.ts @@ -0,0 +1,144 @@ +/* eslint-disable jest/no-standalone-expect */ +import { nextTestSetup, isNextDev } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('nested index.js', () => { + const { next, isTurbopack, skipped } = nextTestSetup({ + files: __dirname, + // Vercel's deploy infrastructure normalizes nested `/index/index/index` + // paths differently from Next.js's local server, so the routing + // assertions here are local-only. + skipDeployment: true, + }) + if (skipped) return + + it('should ssr page /', async () => { + const $ = await next.render$('/') + expect($('#page').text()).toBe('index') + }) + + it('should client render page /', async () => { + const browser = await next.browser('/') + const text = await browser.elementByCss('#page').text() + expect(text).toBe('index') + }) + + it('should follow link to /', async () => { + const browser = await next.browser('/links') + await browser.elementByCss('#link1').click() + await retry(async () => { + const text = await browser.elementByCss('#page').text() + expect(text).toBe('index') + }) + }) + + it('should ssr page /index', async () => { + const $ = await next.render$('/index') + expect($('#page').text()).toBe('index > index') + }) + + // pages named "index" are never hydrated in Webpack during development + ;(isNextDev && !isTurbopack ? it.failing : it)( + 'should client render page /index', + async () => { + const browser = await next.browser('/index') + const text = await browser.elementByCss('#page').text() + expect(text).toBe('index > index') + } + ) + + it('should follow link to /index', async () => { + const browser = await next.browser('/links') + await browser.elementByCss('#link2').click() + await retry(async () => { + const text = await browser.elementByCss('#page').text() + expect(text).toBe('index > index') + }) + }) + + it('should ssr page /index/user', async () => { + const $ = await next.render$('/index/user') + expect($('#page').text()).toBe('index > user') + }) + + // pages named "index" are never hydrated in Webpack during development + ;(isNextDev && !isTurbopack ? it.failing : it)( + 'should client render page /index/user', + async () => { + const browser = await next.browser('/index/user') + const text = await browser.elementByCss('#page').text() + expect(text).toBe('index > user') + } + ) + + it('should follow link to /index/user', async () => { + const browser = await next.browser('/links') + await browser.elementByCss('#link5').click() + await retry(async () => { + const text = await browser.elementByCss('#page').text() + expect(text).toBe('index > user') + }) + }) + + it('should ssr page /index/project', async () => { + const $ = await next.render$('/index/project') + expect($('#page').text()).toBe('index > project') + }) + + // pages named "index" are never hydrated in Webpack during development + ;(isNextDev && !isTurbopack ? it.failing : it)( + 'should client render page /index/project', + async () => { + const browser = await next.browser('/index/project') + const text = await browser.elementByCss('#page').text() + expect(text).toBe('index > project') + } + ) + + it('should follow link to /index/project', async () => { + const browser = await next.browser('/links') + await browser.elementByCss('#link6').click() + await retry(async () => { + const text = await browser.elementByCss('#page').text() + expect(text).toBe('index > project') + }) + }) + + it('should ssr page /index/index', async () => { + const $ = await next.render$('/index/index') + expect($('#page').text()).toBe('index > index > index') + }) + + // pages named "index" are never hydrated in Webpack during development + ;(isNextDev && !isTurbopack ? it.failing : it)( + 'should client render page /index/index', + async () => { + const browser = await next.browser('/index/index') + const text = await browser.elementByCss('#page').text() + expect(text).toBe('index > index > index') + } + ) + + it('should follow link to /index/index', async () => { + const browser = await next.browser('/links') + await browser.elementByCss('#link3').click() + await retry(async () => { + const text = await browser.elementByCss('#page').text() + expect(text).toBe('index > index > index') + }) + }) + + it('should 404 on /index/index/index', async () => { + const response = await next.fetch('/index/index/index') + expect(response.status).toBe(404) + }) + + it('should not find a link to /index/index/index', async () => { + const browser = await next.browser('/links') + await browser.elementByCss('#link4').click() + await retry(async () => { + const text = await browser.elementByCss('h1').text() + expect(text).toMatch(/404/) + }) + }) +}) diff --git a/test/integration/future/next.config.js b/test/e2e/index-index/next.config.js similarity index 100% rename from test/integration/future/next.config.js rename to test/e2e/index-index/next.config.js diff --git a/test/integration/index-index/pages/index.js b/test/e2e/index-index/pages/index.js similarity index 100% rename from test/integration/index-index/pages/index.js rename to test/e2e/index-index/pages/index.js diff --git a/test/integration/index-index/pages/index/index.js b/test/e2e/index-index/pages/index/index.js similarity index 100% rename from test/integration/index-index/pages/index/index.js rename to test/e2e/index-index/pages/index/index.js diff --git a/test/integration/index-index/pages/index/index/index.js b/test/e2e/index-index/pages/index/index/index.js similarity index 100% rename from test/integration/index-index/pages/index/index/index.js rename to test/e2e/index-index/pages/index/index/index.js diff --git a/test/integration/index-index/pages/index/project/index.js b/test/e2e/index-index/pages/index/project/index.js similarity index 100% rename from test/integration/index-index/pages/index/project/index.js rename to test/e2e/index-index/pages/index/project/index.js diff --git a/test/integration/index-index/pages/index/user.js b/test/e2e/index-index/pages/index/user.js similarity index 100% rename from test/integration/index-index/pages/index/user.js rename to test/e2e/index-index/pages/index/user.js diff --git a/test/integration/index-index/pages/links.js b/test/e2e/index-index/pages/links.js similarity index 100% rename from test/integration/index-index/pages/links.js rename to test/e2e/index-index/pages/links.js diff --git a/test/e2e/initial-ref/initial-ref.test.ts b/test/e2e/initial-ref/initial-ref.test.ts new file mode 100644 index 000000000000..de68abef5b8d --- /dev/null +++ b/test/e2e/initial-ref/initial-ref.test.ts @@ -0,0 +1,12 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('Initial Refs', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('Has correct initial ref values', async () => { + const browser = await next.browser('/') + expect(await browser.elementByCss('#ref-val').text()).toContain('76px') + }) +}) diff --git a/test/integration/initial-ref/pages/index.js b/test/e2e/initial-ref/pages/index.js similarity index 100% rename from test/integration/initial-ref/pages/index.js rename to test/e2e/initial-ref/pages/index.js diff --git a/test/e2e/invalid-custom-routes/invalid-custom-routes.test.ts b/test/e2e/invalid-custom-routes/invalid-custom-routes.test.ts new file mode 100644 index 000000000000..55c786cbad8c --- /dev/null +++ b/test/e2e/invalid-custom-routes/invalid-custom-routes.test.ts @@ -0,0 +1,567 @@ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('Errors on invalid custom routes', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + if (skipped) return + + const writeConfig = async (routes: any, type = 'redirects') => { + await next.patchFile( + 'next.config.js', + ` + module.exports = { + async ${type}() { + return ${JSON.stringify(routes)} + } + } + ` + ) + } + + /** + * Dev: start server, hit `/`, assert on streaming `cliOutput` (like integration `launchApp` + stderr). + * Start: run `next build` and assert on build output (like integration `nextBuild` + stderr). + */ + async function captureStderr( + mode: 'dev' | 'start', + assertions: (getOutput: () => string) => void | Promise<void> + ) { + if (mode === 'dev') { + await next.start() + try { + await next.fetch('/').catch(() => {}) + await retry(async () => { + await assertions(() => next.cliOutput) + }) + } finally { + await next.stop() + } + } else { + const { cliOutput } = await next.build() + const fixed = cliOutput + await retry(async () => { + await assertions(() => fixed) + }) + } + } + + afterAll(async () => { + await next.patchFile('next.config.js', `module.exports = {}\n`) + }) + + function runTests(mode: 'dev' | 'start') { + it('should error when empty headers array is present on header item', async () => { + await writeConfig( + [ + { + source: `/:path*`, + headers: [], + }, + ], + 'headers' + ) + await captureStderr(mode, (getOutput) => { + const stderr = getOutput() + expect(stderr).toContain( + '`headers` field cannot be empty for route {"source":"/:path*"' + ) + }) + }) + + it('should error when source and destination length is exceeded', async () => { + await writeConfig( + [ + { + source: `/${Array(4096).join('a')}`, + destination: `/another`, + permanent: false, + }, + { + source: `/`, + destination: `/${Array(4096).join('a')}`, + permanent: false, + }, + ], + 'redirects' + ) + await captureStderr(mode, (getOutput) => { + const stderr = getOutput() + expect(stderr).toContain( + '`source` exceeds max built length of 4096 for route {"source":"/aaaaaaaaaaaaaaaaaa' + ) + expect(stderr).toContain( + '`destination` exceeds max built length of 4096 for route {"source":"/","destination":"/aaaa' + ) + }) + }) + + it('should error during next build for invalid redirects', async () => { + await writeConfig( + [ + { + source: '/hello', + permanent: false, + }, + { + source: 123, + destination: '/another', + permanent: false, + }, + { + source: '/hello', + destination: '/another', + statusCode: '301', + }, + { + source: '/hello', + destination: '/another', + statusCode: 404, + }, + { + source: '/hello', + destination: '/another', + permanent: 'yes', + }, + { + source: '/hello/world/(.*)', + destination: '/:0', + permanent: true, + }, + null, + 'string', + { + source: '/hello', + destination: '/another', + has: [ + { + type: 'cookiee', + key: 'loggedIn', + }, + ], + permanent: false, + }, + { + source: '/hello', + destination: '/another', + permanent: false, + has: [ + { + type: 'headerr', + }, + { + type: 'queryr', + key: 'hello', + }, + ], + }, + ], + 'redirects' + ) + await captureStderr(mode, (getOutput) => { + const stderr = getOutput() + expect(stderr).toContain( + `\`destination\` is missing for route {"source":"/hello","permanent":false}` + ) + expect(stderr).toContain( + `\`source\` is not a string for route {"source":123,"destination":"/another","permanent":false}` + ) + expect(stderr).toContain( + `\`statusCode\` is not undefined or valid statusCode for route {"source":"/hello","destination":"/another","statusCode":"301"}` + ) + expect(stderr).toContain( + `\`statusCode\` is not undefined or valid statusCode for route {"source":"/hello","destination":"/another","statusCode":404}` + ) + expect(stderr).toContain( + `\`permanent\` is not set to \`true\` or \`false\` for route {"source":"/hello","destination":"/another","permanent":"yes"}` + ) + expect(stderr).toContain( + `\`destination\` has unnamed params :0 for route {"source":"/hello/world/(.*)","destination":"/:0","permanent":true}` + ) + expect(stderr).toContain( + `The route null is not a valid object with \`source\` and \`destination\`` + ) + expect(stderr).toContain( + `The route "string" is not a valid object with \`source\` and \`destination\`` + ) + expect(stderr).toContain('Invalid `has` item:') + expect(stderr).toContain( + `invalid type "cookiee" for {"type":"cookiee","key":"loggedIn"}` + ) + expect(stderr).toContain( + `invalid \`has\` item found for route {"source":"/hello","destination":"/another","has":[{"type":"cookiee","key":"loggedIn"}],"permanent":false}` + ) + expect(stderr).toContain('Invalid `has` items:') + expect(stderr).toContain( + `invalid type "headerr", invalid key "undefined" for {"type":"headerr"}` + ) + expect(stderr).toContain( + `invalid type "queryr" for {"type":"queryr","key":"hello"}` + ) + expect(stderr).toContain( + `invalid \`has\` items found for route {"source":"/hello","destination":"/another","permanent":false,"has":[{"type":"headerr"},{"type":"queryr","key":"hello"}]}` + ) + expect(stderr).toContain(`Valid \`has\` object shape is {`) + expect(stderr).toContain('Invalid redirects found') + }) + }) + + it('should error during next build for invalid rewrites', async () => { + await writeConfig( + [ + { + source: '/hello', + }, + { + source: 123, + destination: '/another', + }, + { + source: '/hello', + destination: '/another', + headers: 'not-allowed', + }, + { + source: 'hello', + destination: '/another', + }, + { + source: '/hello', + destination: 'another', + }, + { + source: '/feedback/(?!general)', + destination: '/feedback/general', + }, + { + source: '/hello/world/(.*)', + destination: '/:0', + }, + { + source: '/hello', + destination: '/world', + basePath: false, + }, + null, + 'string', + { + source: '/hello', + destination: '/another', + has: [ + { + type: 'cookiee', + key: 'loggedIn', + }, + ], + }, + { + source: '/hello', + destination: '/another', + has: [ + { + type: 'headerr', + }, + { + type: 'queryr', + key: 'hello', + }, + ], + }, + ], + 'rewrites' + ) + await captureStderr(mode, (getOutput) => { + const stderr = getOutput() + expect(stderr).toContain( + `\`destination\` is missing for route {"source":"/hello"}` + ) + expect(stderr).toContain( + `\`source\` is not a string for route {"source":123,"destination":"/another"}` + ) + expect(stderr).toContain( + `invalid field: headers for route {"source":"/hello","destination":"/another","headers":"not-allowed"}` + ) + expect(stderr).toContain( + `\`source\` does not start with / for route {"source":"hello","destination":"/another"}` + ) + expect(stderr).toContain( + `\`destination\` does not start with \`/\`, \`http://\`, or \`https://\` for route {"source":"/hello","destination":"another"}` + ) + expect(stderr).toContain( + `Error parsing \`/feedback/(?!general)\` https://nextjs.org/docs/messages/invalid-route-source` + ) + expect(stderr).toContain( + `\`destination\` has unnamed params :0 for route {"source":"/hello/world/(.*)","destination":"/:0"}` + ) + expect(stderr).toContain( + `The route null is not a valid object with \`source\` and \`destination\`` + ) + expect(stderr).toContain( + `The route "string" is not a valid object with \`source\` and \`destination\`` + ) + expect(stderr).toContain(`Reason: Pattern cannot start with "?" at 11`) + expect(stderr).toContain(`/feedback/(?!general)`) + expect(stderr).not.toContain( + 'Valid redirect statusCode values are 301, 302, 303, 307, 308' + ) + expect(stderr).toContain( + `The route /hello rewrites urls outside of the basePath. Please use a destination that starts with \`http://\` or \`https://\` https://nextjs.org/docs/messages/invalid-external-rewrite` + ) + expect(stderr).toContain('Invalid `has` item:') + expect(stderr).toContain( + `invalid type "cookiee" for {"type":"cookiee","key":"loggedIn"}` + ) + expect(stderr).toContain( + `invalid \`has\` item found for route {"source":"/hello","destination":"/another","has":[{"type":"cookiee","key":"loggedIn"}]}` + ) + expect(stderr).toContain('Invalid `has` items:') + expect(stderr).toContain( + `invalid type "headerr", invalid key "undefined" for {"type":"headerr"}` + ) + expect(stderr).toContain( + `invalid type "queryr" for {"type":"queryr","key":"hello"}` + ) + expect(stderr).toContain( + `invalid \`has\` items found for route {"source":"/hello","destination":"/another","has":[{"type":"headerr"},{"type":"queryr","key":"hello"}]}` + ) + expect(stderr).toContain(`Valid \`has\` object shape is {`) + expect(stderr).toContain('Invalid rewrites found') + }) + }) + + it('should error during next build for invalid headers', async () => { + await writeConfig( + [ + { + headers: [ + { + 'x-first': 'first', + }, + ], + }, + { + source: '/hello', + headers: { + 'x-first': 'first', + }, + }, + { + source: '/again', + headers: [ + { + value: 'idk', + }, + ], + }, + { + source: '/again', + headers: [ + { + key: 'idk', + }, + ], + }, + { + source: '/again', + destination: '/another', + headers: [ + { + key: 'x-first', + value: 'idk', + }, + ], + }, + { + source: '/valid-header', + headers: [ + { + key: 'x-first', + value: 'first', + }, + { + key: 'x-another', + value: 'again', + }, + ], + }, + null, + 'string', + { + source: '/hello', + has: [ + { + type: 'cookiee', + key: 'loggedIn', + }, + ], + headers: [ + { + key: 'x-hello', + value: 'world', + }, + ], + }, + { + source: '/hello', + has: [ + { + type: 'headerr', + }, + { + type: 'queryr', + key: 'hello', + }, + ], + headers: [ + { + key: 'x-hello', + value: 'world', + }, + ], + }, + ], + 'headers' + ) + await captureStderr(mode, (getOutput) => { + const stderr = getOutput() + expect(stderr).toContain( + '`source` is missing, `key` in header item must be string for route {"headers":[{"x-first":"first"}]}' + ) + expect(stderr).toContain( + '`headers` field must be an array for route {"source":"/hello","headers":{"x-first":"first"}}' + ) + expect(stderr).toContain( + '`key` in header item must be string for route {"source":"/again","headers":[{"value":"idk"}]}' + ) + expect(stderr).toContain( + '`value` in header item must be string for route {"source":"/again","headers":[{"key":"idk"}]}' + ) + expect(stderr).toContain( + 'invalid field: destination for route {"source":"/again","destination":"/another","headers":[{"key":"x-first","value":"idk"}]}' + ) + expect(stderr).toContain( + `The route null is not a valid object with \`source\` and \`headers\`` + ) + expect(stderr).toContain( + `The route "string" is not a valid object with \`source\` and \`headers\`` + ) + expect(stderr).toContain('Invalid `has` item:') + expect(stderr).toContain( + `invalid type "cookiee" for {"type":"cookiee","key":"loggedIn"}` + ) + expect(stderr).toContain( + `invalid \`has\` item found for route {"source":"/hello","has":[{"type":"cookiee","key":"loggedIn"}],"headers":[{"key":"x-hello","value":"world"}]}` + ) + expect(stderr).toContain('Invalid `has` items:') + expect(stderr).toContain( + `invalid type "headerr", invalid key "undefined" for {"type":"headerr"}` + ) + expect(stderr).toContain( + `invalid type "queryr" for {"type":"queryr","key":"hello"}` + ) + expect(stderr).toContain( + `invalid \`has\` items found for route {"source":"/hello","has":[{"type":"headerr"},{"type":"queryr","key":"hello"}],"headers":[{"key":"x-hello","value":"world"}]}` + ) + expect(stderr).toContain(`Valid \`has\` object shape is {`) + expect(stderr).not.toContain('/valid-header') + }) + }) + + it('should show formatted error for redirect source parse fail', async () => { + await writeConfig( + [ + { + source: '/feedback/(?!general)', + destination: '/feedback/general', + permanent: false, + }, + { + source: '/learning/?', + destination: '/learning', + permanent: true, + }, + ], + 'redirects' + ) + await captureStderr(mode, (getOutput) => { + const stderr = getOutput() + expect(stderr).toContain( + `Error parsing \`/feedback/(?!general)\` https://nextjs.org/docs/messages/invalid-route-source` + ) + expect(stderr).toContain(`Reason: Pattern cannot start with "?" at 11`) + expect(stderr).toContain(`/feedback/(?!general)`) + expect(stderr).toContain( + `Error parsing \`/learning/?\` https://nextjs.org/docs/messages/invalid-route-source` + ) + expect(stderr).toContain( + `Reason: Unexpected MODIFIER at 10, expected END` + ) + expect(stderr).toContain(`/learning/?`) + }) + }) + + it('should show valid error when non-array is returned from rewrites', async () => { + await writeConfig( + { + source: '/feedback/(?!general)', + destination: '/feedback/general', + }, + 'rewrites' + ) + await captureStderr(mode, (getOutput) => { + const stderr = getOutput() + expect(stderr).toContain( + `rewrites must return an array, received object` + ) + }) + }) + + it('should show valid error when non-array is returned from redirects', async () => { + await writeConfig(false, 'redirects') + await captureStderr(mode, (getOutput) => { + const stderr = getOutput() + expect(stderr).toContain( + `redirects must return an array, received boolean` + ) + }) + }) + + it('should show valid error when non-array is returned from headers', async () => { + await writeConfig(undefined, 'headers') + await captureStderr(mode, (getOutput) => { + const stderr = getOutput() + expect(stderr).toContain( + `headers must return an array, received undefined` + ) + }) + }) + + it('should show valid error when segments not in source are used in destination', async () => { + await writeConfig( + [ + { + source: '/feedback/:type', + destination: '/feedback/:id', + }, + ], + 'rewrites' + ) + await captureStderr(mode, (getOutput) => { + const stderr = getOutput() + expect(stderr).toContain( + `\`destination\` has segments not in \`source\` or \`has\` (id) for route {"source":"/feedback/:type","destination":"/feedback/:id"}` + ) + }) + }) + } + + ;(isNextDev ? describe : describe.skip)('development mode', () => { + runTests('dev') + }) + ;(isNextStart ? describe : describe.skip)('production mode', () => { + runTests('start') + }) +}) diff --git a/test/integration/gsp-build-errors/next.config.js b/test/e2e/invalid-custom-routes/next.config.js similarity index 100% rename from test/integration/gsp-build-errors/next.config.js rename to test/e2e/invalid-custom-routes/next.config.js diff --git a/test/integration/file-serving/pages/index.js b/test/e2e/invalid-custom-routes/pages/index.js similarity index 100% rename from test/integration/file-serving/pages/index.js rename to test/e2e/invalid-custom-routes/pages/index.js diff --git a/test/e2e/invalid-href/invalid-href.test.ts b/test/e2e/invalid-href/invalid-href.test.ts new file mode 100644 index 000000000000..d1603c374194 --- /dev/null +++ b/test/e2e/invalid-href/invalid-href.test.ts @@ -0,0 +1,183 @@ +/* eslint-disable jest/no-identical-title */ +import { nextTestSetup, isNextDev } from 'e2e-utils' +import { waitForRedbox, getRedboxHeader, retry } from 'next-test-utils' + +jest.retryTimes(0) + +describe('Invalid hrefs', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + const showsError = async ( + pathname: string, + regex: RegExp, + click = false, + isWarn = false + ) => { + const browser = await next.browser(pathname) + try { + await browser.waitForElementByCss('#click-me') + if (isWarn) { + await browser.eval(`(function() { + window.warnLogs = [] + var origWarn = window.console.warn + window.console.warn = (...args) => { + window.warnLogs.push(args.join(' ')) + origWarn.apply(window.console, args) + } + })()`) + } + if (click) { + await browser.elementByCss('#click-me').click() + await new Promise((resolve) => setTimeout(resolve, 500)) + } + if (isWarn) { + await retry(async () => { + const warnLogs = await browser.eval('window.warnLogs') + expect(warnLogs.join('\n')).toMatch(regex) + }) + } else { + await waitForRedbox(browser) + const errorContent = await getRedboxHeader(browser) + expect(errorContent).toMatch(regex) + } + } finally { + await browser.close() + } + } + + const noError = async (pathname: string, click = false) => { + const browser = await next.browser('/') + try { + await browser.eval(`(function() { + window.caughtErrors = [] + window.addEventListener('error', function (error) { + window.caughtErrors.push(error.message || 1) + }) + window.addEventListener('unhandledrejection', function (error) { + window.caughtErrors.push(error.message || 1) + }) + window.next.router.replace('${pathname}') + })()`) + await browser.waitForElementByCss('#click-me') + if (click) { + await browser.elementByCss('#click-me').click() + await new Promise((resolve) => setTimeout(resolve, 500)) + } + const caughtErrors = await browser.eval(`window.caughtErrors`) + expect(caughtErrors).toHaveLength(0) + } finally { + await browser.close() + } + } + + if (!isNextDev) { + it('does not show error in production when mailto: is used as href on Link', async () => { + await noError('/first') + }) + + it('does not show error in production when https:// is used in href on Link', async () => { + await noError('/second') + }) + + it('does not show error in production when exotic protocols are used in href in Link', async () => { + const browser = await next.browser('/exotic-href') + expect((await browser.log()).filter((x) => x.source === 'error')).toEqual( + [] + ) + }) + + it('does not show error when internal href is used with external as', async () => { + await noError('/invalid-relative', true) + }) + + it('shows error when dynamic route mismatch is used on Link', async () => { + const browser = await next.browser('/dynamic-route-mismatch') + try { + await browser.eval(`(function() { + window.caughtErrors = [] + window.addEventListener('unhandledrejection', (error) => { + window.caughtErrors.push(error.reason.message) + }) + })()`) + await browser.elementByCss('a').click() + await new Promise((resolve) => setTimeout(resolve, 500)) + const errors = await browser.eval('window.caughtErrors') + expect( + errors.find((err: string) => + err.includes( + 'The provided `as` value (/blog/post-1) is incompatible with the `href` value (/[post]). Read more: https://nextjs.org/docs/messages/incompatible-href-as' + ) + ) + ).toBeTruthy() + } finally { + await browser.close() + } + }) + + it("doesn't fail on invalid url", async () => { + await noError('/third') + }) + + it('renders a link with invalid href', async () => { + const $ = await next.render$('/third') + expect($('#click-me').attr('href')).toBe('https://') + }) + + it('renders a link with mailto: href', async () => { + const $ = await next.render$('/first') + expect($('#click-me').attr('href')).toBe('mailto:idk@idk.com') + }) + } + + if (isNextDev) { + it('does not show error when mailto: is used as href on Link', async () => { + await noError('/first') + }) + + it('does not show error when https:// is used as href in Link', async () => { + await noError('/second') + }) + + it('does not show error when exotic protocols are used in href in Link', async () => { + const browser = await next.browser('/exotic-href') + expect((await browser.log()).filter((x) => x.source === 'error')).toEqual( + [] + ) + }) + + it('shows error when dynamic route mismatch is used on Link', async () => { + await showsError( + '/dynamic-route-mismatch', + /The provided `as` value \(\/blog\/post-1\) is incompatible with the `href` value \(\/\[post\]\)/, + true + ) + }) + + it('shows error when internal href is used with external as', async () => { + await showsError( + '/invalid-relative', + /Invalid href: "\/second" and as: "mailto:hello@example\.com", received relative href and external as/, + true + ) + }) + + it('does not throw error when dynamic route mismatch is used on Link and params are manually provided', async () => { + await noError('/dynamic-route-mismatch-manual', true) + }) + + it("doesn't fail on invalid url", async () => { + await noError('/third') + }) + + it('shows warning when dynamic route mismatch is used on Link', async () => { + await showsError( + '/dynamic-route-mismatch', + /Mismatching `as` and `href` failed to manually provide the params: post in the `href`'s `query`/, + true, + true + ) + }) + } +}) diff --git a/test/integration/invalid-href/pages/[post].js b/test/e2e/invalid-href/pages/[post].js similarity index 100% rename from test/integration/invalid-href/pages/[post].js rename to test/e2e/invalid-href/pages/[post].js diff --git a/test/integration/invalid-href/pages/dynamic-route-mismatch-manual.js b/test/e2e/invalid-href/pages/dynamic-route-mismatch-manual.js similarity index 100% rename from test/integration/invalid-href/pages/dynamic-route-mismatch-manual.js rename to test/e2e/invalid-href/pages/dynamic-route-mismatch-manual.js diff --git a/test/integration/invalid-href/pages/dynamic-route-mismatch.js b/test/e2e/invalid-href/pages/dynamic-route-mismatch.js similarity index 100% rename from test/integration/invalid-href/pages/dynamic-route-mismatch.js rename to test/e2e/invalid-href/pages/dynamic-route-mismatch.js diff --git a/test/integration/invalid-href/pages/exotic-href.js b/test/e2e/invalid-href/pages/exotic-href.js similarity index 100% rename from test/integration/invalid-href/pages/exotic-href.js rename to test/e2e/invalid-href/pages/exotic-href.js diff --git a/test/integration/invalid-href/pages/first.js b/test/e2e/invalid-href/pages/first.js similarity index 100% rename from test/integration/invalid-href/pages/first.js rename to test/e2e/invalid-href/pages/first.js diff --git a/test/integration/invalid-href/pages/index.js b/test/e2e/invalid-href/pages/index.js similarity index 100% rename from test/integration/invalid-href/pages/index.js rename to test/e2e/invalid-href/pages/index.js diff --git a/test/integration/invalid-href/pages/invalid-relative.js b/test/e2e/invalid-href/pages/invalid-relative.js similarity index 100% rename from test/integration/invalid-href/pages/invalid-relative.js rename to test/e2e/invalid-href/pages/invalid-relative.js diff --git a/test/integration/invalid-href/pages/second.js b/test/e2e/invalid-href/pages/second.js similarity index 100% rename from test/integration/invalid-href/pages/second.js rename to test/e2e/invalid-href/pages/second.js diff --git a/test/integration/invalid-href/pages/third.js b/test/e2e/invalid-href/pages/third.js similarity index 100% rename from test/integration/invalid-href/pages/third.js rename to test/e2e/invalid-href/pages/third.js diff --git a/test/e2e/invalid-middleware-matchers/invalid-middleware-matchers.test.ts b/test/e2e/invalid-middleware-matchers/invalid-middleware-matchers.test.ts new file mode 100644 index 000000000000..ba9cbf0f4111 --- /dev/null +++ b/test/e2e/invalid-middleware-matchers/invalid-middleware-matchers.test.ts @@ -0,0 +1,209 @@ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('Errors on invalid custom middleware matchers', () => { + const { next, isTurbopack, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + if (skipped) return + + afterEach(async () => { + await next.deleteFile('middleware.js').catch(() => {}) + await next.stop().catch(() => {}) + }) + + function writeMiddleware(matchers: any) { + return next.patchFile( + 'middleware.js', + ` + import { NextResponse } from 'next/server' + + export default function middleware() { + return NextResponse.next() + } + + export const config = { + matcher: ${JSON.stringify(matchers)}, + } + ` + ) + } + + /** Same branching as integration `runTests(getStderr, isDev)`. */ + function assertInvalidMatchersStderr(stderr: string, isDev: boolean) { + if (isTurbopack && !isDev) { + expect(stderr).toContain('Turbopack build failed with 10 errors') + + let matches = 0 + matches += stderr.includes('Missing `source` in `matcher[0]` object') + ? 1 + : 0 + matches += stderr.includes('Missing `source` in `matcher[1]` object') + ? 1 + : 0 + matches += stderr.includes('Unexpected property in `matcher[3]` object') + ? 1 + : 0 + matches += stderr.includes( + 'Entry `matcher[4]` need to be static strings or static objects.' + ) + ? 1 + : 0 + matches += stderr.includes( + "`matcher[5].has[0].type` must be one of the strings: 'header', 'cookie', 'query', 'host'" + ) + ? 1 + : 0 + matches += stderr.includes( + "`matcher[6].has[0].type` must be one of the strings: 'header', 'cookie', 'query', 'host'" + ) + ? 1 + : 0 + matches += stderr.includes('Unexpected property in `matcher[7]` object') + ? 1 + : 0 + matches += stderr.includes( + '`locale` in `matcher[8]` object must be false or undefined' + ) + ? 1 + : 0 + + if (matches < 4) { + throw new Error('Missing error messages for stderr:\n' + stderr) + } + } else { + expect(stderr).toContain( + 'Expected string, received object at "matcher[0]", or source is required at "matcher[0].source"' + ) + expect(stderr).toContain( + 'Expected string, received number at "matcher[1].source"' + ) + expect(stderr).toContain( + 'Unrecognized key(s) in object: \'destination\' at "matcher[3]"' + ) + expect(stderr).toContain('Expected string, received null at "matcher[4]"') + expect(stderr).toContain( + "Expected 'header' | 'query' | 'cookie' | 'host' at \"matcher[6].has[1].type\"" + ) + expect(stderr).toContain( + "Expected 'header' | 'query' | 'cookie' | 'host' at \"matcher[5].has[0].type\"" + ) + expect(stderr).toContain( + "Expected 'header' | 'query' | 'cookie' | 'host' at \"matcher[6].has[0].type\"" + ) + expect(stderr).toContain( + "Expected 'header' | 'query' | 'cookie' | 'host' at \"matcher[6].has[1].type\"" + ) + expect(stderr).toContain( + 'Unrecognized key(s) in object: \'basePath\' at "matcher[7]"' + ) + expect(stderr).toContain( + 'Expected string, received object at "matcher[8]", or Invalid literal value, expected false at "matcher[8].locale", or Expected undefined, received boolean at "matcher[8].locale"' + ) + + // TODO currently not covered by Turbopack + expect(stderr).toContain('source must start with / at "matcher[2]"') + } + } + + function runTests(mode: 'dev' | 'start') { + const isDevMode = mode === 'dev' + + it('should error when source length is exceeded', async () => { + await writeMiddleware([{ source: `/${Array(4096).join('a')}` }]) + if (isDevMode) { + await next.start() + try { + await next.fetch('/').catch(() => {}) + await retry(() => { + expect(next.cliOutput).toContain( + 'exceeds max built length of 4096 for route' + ) + }) + } finally { + await next.stop() + } + } else { + await next.build() + expect(next.cliOutput).toContain( + 'exceeds max built length of 4096 for route' + ) + } + }) + + it('should error during next build for invalid matchers', async () => { + await writeMiddleware([ + { + // missing source + }, + { + // invalid source + source: 123, + }, + // missing forward slash in source + 'hello', + { + // extra field + source: '/hello', + destination: '/not-allowed', + }, + // invalid objects + null, + // invalid has items + { + source: '/hello', + has: [ + { + type: 'cookiee', + key: 'loggedIn', + }, + ], + }, + { + source: '/hello', + has: [ + { + type: 'headerr', + }, + { + type: 'queryr', + key: 'hello', + }, + ], + }, + { + source: '/hello', + basePath: false, + }, + { + source: '/hello', + locale: true, + }, + ]) + + if (isDevMode) { + await next.start() + try { + await next.fetch('/').catch(() => {}) + await retry(async () => { + assertInvalidMatchersStderr(next.cliOutput, isDevMode) + }) + } finally { + await next.stop() + } + } else { + await next.build() + assertInvalidMatchersStderr(next.cliOutput, isDevMode) + } + }) + } + + ;(isNextDev ? describe : describe.skip)('development mode', () => { + runTests('dev') + }) + ;(isNextStart ? describe : describe.skip)('production mode', () => { + runTests('start') + }) +}) diff --git a/test/integration/gip-identifier/pages/index.js b/test/e2e/invalid-middleware-matchers/pages/index.js similarity index 100% rename from test/integration/gip-identifier/pages/index.js rename to test/e2e/invalid-middleware-matchers/pages/index.js diff --git a/test/e2e/invalid-multi-match/invalid-multi-match.test.ts b/test/e2e/invalid-multi-match/invalid-multi-match.test.ts new file mode 100644 index 000000000000..2133fb1afed0 --- /dev/null +++ b/test/e2e/invalid-multi-match/invalid-multi-match.test.ts @@ -0,0 +1,26 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('Custom routes invalid multi-match', () => { + const { next, isNextDeploy, skipped } = nextTestSetup({ + files: __dirname, + disableAutoSkewProtection: true, + // The test asserts on `next.cliOutput`, expecting the local + // `next build` failure message ("To use a multi-match in the + // destination..."). In deploy mode `next.cliOutput` contains the + // Vercel deploy log, which doesn't surface the same Next.js build + // error string at the top level, so the assertions don't apply. + skipDeployment: true, + }) + if (skipped) return + if (isNextDeploy) return + + it('should show error for invalid multi-match', async () => { + await next.render('/random') + expect(next.cliOutput).toContain( + 'To use a multi-match in the destination you must add' + ) + expect(next.cliOutput).toContain( + 'https://nextjs.org/docs/messages/invalid-multi-match' + ) + }) +}) diff --git a/test/integration/invalid-multi-match/next.config.js b/test/e2e/invalid-multi-match/next.config.js similarity index 100% rename from test/integration/invalid-multi-match/next.config.js rename to test/e2e/invalid-multi-match/next.config.js diff --git a/test/integration/invalid-multi-match/pages/hello.js b/test/e2e/invalid-multi-match/pages/hello.js similarity index 100% rename from test/integration/invalid-multi-match/pages/hello.js rename to test/e2e/invalid-multi-match/pages/hello.js diff --git a/test/integration/invalid-server-options/test/index.test.ts b/test/e2e/invalid-server-options/invalid-server-options.test.ts similarity index 97% rename from test/integration/invalid-server-options/test/index.test.ts rename to test/e2e/invalid-server-options/invalid-server-options.test.ts index 757b50582e10..ca25713735d8 100644 --- a/test/integration/invalid-server-options/test/index.test.ts +++ b/test/e2e/invalid-server-options/invalid-server-options.test.ts @@ -1,6 +1,6 @@ import next from 'next' -import { join } from 'path' -const dir = join(__dirname, '../') + +const dir = __dirname const warningMessage = "Warning: 'dev' is not a boolean which could introduce unexpected behavior. https://nextjs.org/docs/messages/invalid-server-options" diff --git a/test/integration/invalid-server-options/pages/index.js b/test/e2e/invalid-server-options/pages/index.js similarity index 100% rename from test/integration/invalid-server-options/pages/index.js rename to test/e2e/invalid-server-options/pages/index.js diff --git a/test/integration/jsconfig-baseurl/components/world.js b/test/e2e/jsconfig-baseurl/components/world.js similarity index 100% rename from test/integration/jsconfig-baseurl/components/world.js rename to test/e2e/jsconfig-baseurl/components/world.js diff --git a/test/e2e/jsconfig-baseurl/jsconfig-baseurl.test.ts b/test/e2e/jsconfig-baseurl/jsconfig-baseurl.test.ts new file mode 100644 index 000000000000..25a3b5138e03 --- /dev/null +++ b/test/e2e/jsconfig-baseurl/jsconfig-baseurl.test.ts @@ -0,0 +1,59 @@ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +import stripAnsi from 'next/dist/compiled/strip-ansi' +import { retry } from 'next-test-utils' + +describe('jsconfig.json baseurl', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + }) + if (skipped) return + + describe('default behavior', () => { + it('should render the page', async () => { + const $ = await next.render$('/hello') + expect($('body').text()).toMatch(/World/) + }) + + // Integration ran this under `launchApp` only. e2e splits dev vs `next start` jobs, so + // `it.skip` when !isNextDev is correct: the module-not-found overlay is dev-only; production + // jobs still cover `should trace correctly` under `should build` below. + ;(isNextDev ? it : it.skip)( + 'should have correct module not found error', + async () => { + const contents = await next.readFile('pages/hello.js') + try { + await next.patchFile( + 'pages/hello.js', + contents.replace('components/world', 'components/worldd') + ) + + await retry(async () => { + await next.render('/hello').catch(() => {}) + const strippedOutput = stripAnsi(next.cliOutput) + expect(strippedOutput).toMatch( + /Module not found: Can't resolve 'components\/worldd'/ + ) + }) + } finally { + await next.patchFile('pages/hello.js', contents) + } + } + ) + }) + ;(isNextStart ? describe : describe.skip)('should build', () => { + it('should trace correctly', async () => { + const helloTrace = JSON.parse( + await next.readFile('.next/server/pages/hello.js.nft.json') + ) + expect( + helloTrace.files.some((file: string) => + file.includes('components/world.js') + ) + ).toBe(false) + expect( + helloTrace.files.some((file: string) => file.includes('react/index.js')) + ).toBe(true) + }) + }) +}) diff --git a/test/integration/jsconfig-baseurl/jsconfig.json b/test/e2e/jsconfig-baseurl/jsconfig.json similarity index 100% rename from test/integration/jsconfig-baseurl/jsconfig.json rename to test/e2e/jsconfig-baseurl/jsconfig.json diff --git a/test/integration/jsconfig-paths-wildcard/next.config.js b/test/e2e/jsconfig-baseurl/next.config.js similarity index 100% rename from test/integration/jsconfig-paths-wildcard/next.config.js rename to test/e2e/jsconfig-baseurl/next.config.js diff --git a/test/integration/jsconfig-baseurl/pages/hello.js b/test/e2e/jsconfig-baseurl/pages/hello.js similarity index 100% rename from test/integration/jsconfig-baseurl/pages/hello.js rename to test/e2e/jsconfig-baseurl/pages/hello.js diff --git a/test/integration/jsconfig-paths/.gitignore b/test/e2e/jsconfig-paths/.gitignore similarity index 100% rename from test/integration/jsconfig-paths/.gitignore rename to test/e2e/jsconfig-paths/.gitignore diff --git a/test/integration/jsconfig-paths/components/hello.js b/test/e2e/jsconfig-paths/components/hello.js similarity index 100% rename from test/integration/jsconfig-paths/components/hello.js rename to test/e2e/jsconfig-paths/components/hello.js diff --git a/test/integration/jsconfig-paths/components/world.js b/test/e2e/jsconfig-paths/components/world.js similarity index 100% rename from test/integration/jsconfig-paths/components/world.js rename to test/e2e/jsconfig-paths/components/world.js diff --git a/test/e2e/jsconfig-paths/jsconfig-paths.test.ts b/test/e2e/jsconfig-paths/jsconfig-paths.test.ts new file mode 100644 index 000000000000..d9ca1e896634 --- /dev/null +++ b/test/e2e/jsconfig-paths/jsconfig-paths.test.ts @@ -0,0 +1,215 @@ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +import { retry } from 'next-test-utils' +import stripAnsi from 'next/dist/compiled/strip-ansi' + +describe('jsconfig paths', () => { + const { next, isNextDeploy, skipped } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + }) + if (skipped) return + if (isNextDeploy) return + + it('should alias components', async () => { + const $ = await next.render$('/basic-alias') + expect($('body').text()).toMatch(/World/) + }) + + it('should resolve the first item in the array first', async () => { + const $ = await next.render$('/resolve-order') + expect($('body').text()).toMatch(/Hello from a/) + }) + + it('should resolve the second item as fallback', async () => { + const $ = await next.render$('/resolve-fallback') + expect($('body').text()).toMatch(/Hello from only b/) + }) + + it('should resolve a single matching alias', async () => { + const $ = await next.render$('/single-alias') + expect($('body').text()).toMatch(/Hello/) + }) + + if (isNextDev) { + it('should have correct module not found error', async () => { + const originalContent = await next.readFile('pages/basic-alias.js') + + try { + await next.patchFile( + 'pages/basic-alias.js', + originalContent.replace('@c/world', '@c/worldd') + ) + + await retry(async () => { + await next.render('/basic-alias') + expect(stripAnsi(next.cliOutput)).toMatch( + /Module not found: Can't resolve '@c\/worldd'/ + ) + }) + } finally { + await next.patchFile('pages/basic-alias.js', originalContent) + } + }) + } + + if (isNextStart) { + it('should trace correctly', async () => { + const singleAliasTrace = JSON.parse( + await next.readFile('.next/server/pages/single-alias.js.nft.json') + ) + const resolveOrderTrace = JSON.parse( + await next.readFile('.next/server/pages/resolve-order.js.nft.json') + ) + const resolveFallbackTrace = JSON.parse( + await next.readFile('.next/server/pages/resolve-fallback.js.nft.json') + ) + const basicAliasTrace = JSON.parse( + await next.readFile('.next/server/pages/basic-alias.js.nft.json') + ) + + expect( + singleAliasTrace.files.some((file: string) => + file.includes('components/hello.js') + ) + ).toBe(false) + expect( + resolveOrderTrace.files.some((file: string) => + file.includes('lib/a/api.js') + ) + ).toBe(false) + expect( + resolveOrderTrace.files.some((file: string) => + file.includes('mypackage/data.js') + ) + ).toBe(true) + expect( + resolveFallbackTrace.files.some((file: string) => + file.includes('lib/b/b-only.js') + ) + ).toBe(false) + expect( + basicAliasTrace.files.some((file: string) => + file.includes('components/world.js') + ) + ).toBe(false) + }) + } +}) + +describe('jsconfig paths without baseurl', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + if (skipped) return + + let originalJsconfigContent: string + + beforeAll(async () => { + // Store original jsconfig content for restoration + originalJsconfigContent = await next.readFile('jsconfig.json') + + const jsconfig = JSON.parse(originalJsconfigContent) + delete jsconfig.compilerOptions.baseUrl + jsconfig.compilerOptions.paths = { + '@c/*': ['./components/*'], + '@lib/*': ['./lib/a/*', './lib/b/*'], + '@mycomponent': ['./components/hello.js'], + } + await next.patchFile('jsconfig.json', JSON.stringify(jsconfig, null, 2)) + await next.start() + }) + + afterAll(async () => { + // Restore original jsconfig content + if (originalJsconfigContent) { + await next.patchFile('jsconfig.json', originalJsconfigContent) + } + }) + + it('should alias components', async () => { + const $ = await next.render$('/basic-alias') + expect($('body').text()).toMatch(/World/) + }) + + it('should resolve the first item in the array first', async () => { + const $ = await next.render$('/resolve-order') + expect($('body').text()).toMatch(/Hello from a/) + }) + + it('should resolve the second item as fallback', async () => { + const $ = await next.render$('/resolve-fallback') + expect($('body').text()).toMatch(/Hello from only b/) + }) + + it('should resolve a single matching alias', async () => { + const $ = await next.render$('/single-alias') + expect($('body').text()).toMatch(/Hello/) + }) + + if (isNextDev) { + it('should have correct module not found error', async () => { + const originalContent = await next.readFile('pages/basic-alias.js') + + try { + await next.patchFile( + 'pages/basic-alias.js', + originalContent.replace('@c/world', '@c/worldd') + ) + + await retry(async () => { + await next.render('/basic-alias') + expect(stripAnsi(next.cliOutput)).toMatch( + /Module not found: Can't resolve '@c\/worldd'/ + ) + }) + } finally { + await next.patchFile('pages/basic-alias.js', originalContent) + } + }) + } + + if (isNextStart) { + it('should trace correctly', async () => { + const singleAliasTrace = JSON.parse( + await next.readFile('.next/server/pages/single-alias.js.nft.json') + ) + const resolveOrderTrace = JSON.parse( + await next.readFile('.next/server/pages/resolve-order.js.nft.json') + ) + const resolveFallbackTrace = JSON.parse( + await next.readFile('.next/server/pages/resolve-fallback.js.nft.json') + ) + const basicAliasTrace = JSON.parse( + await next.readFile('.next/server/pages/basic-alias.js.nft.json') + ) + + expect( + singleAliasTrace.files.some((file: string) => + file.includes('components/hello.js') + ) + ).toBe(false) + expect( + resolveOrderTrace.files.some((file: string) => + file.includes('lib/a/api.js') + ) + ).toBe(false) + expect( + resolveOrderTrace.files.some((file: string) => + file.includes('mypackage/data.js') + ) + ).toBe(true) + expect( + resolveFallbackTrace.files.some((file: string) => + file.includes('lib/b/b-only.js') + ) + ).toBe(false) + expect( + basicAliasTrace.files.some((file: string) => + file.includes('components/world.js') + ) + ).toBe(false) + }) + } +}) diff --git a/test/integration/jsconfig-paths/jsconfig.json b/test/e2e/jsconfig-paths/jsconfig.json similarity index 100% rename from test/integration/jsconfig-paths/jsconfig.json rename to test/e2e/jsconfig-paths/jsconfig.json diff --git a/test/integration/jsconfig-paths/lib/a/api.js b/test/e2e/jsconfig-paths/lib/a/api.js similarity index 100% rename from test/integration/jsconfig-paths/lib/a/api.js rename to test/e2e/jsconfig-paths/lib/a/api.js diff --git a/test/integration/jsconfig-paths/lib/b/api.js b/test/e2e/jsconfig-paths/lib/b/api.js similarity index 100% rename from test/integration/jsconfig-paths/lib/b/api.js rename to test/e2e/jsconfig-paths/lib/b/api.js diff --git a/test/integration/jsconfig-paths/lib/b/b-only.js b/test/e2e/jsconfig-paths/lib/b/b-only.js similarity index 100% rename from test/integration/jsconfig-paths/lib/b/b-only.js rename to test/e2e/jsconfig-paths/lib/b/b-only.js diff --git a/test/integration/jsconfig-paths/next.config.js b/test/e2e/jsconfig-paths/next.config.js similarity index 100% rename from test/integration/jsconfig-paths/next.config.js rename to test/e2e/jsconfig-paths/next.config.js diff --git a/test/integration/jsconfig-paths/node_modules/mypackage/data.js b/test/e2e/jsconfig-paths/node_modules/mypackage/data.js similarity index 100% rename from test/integration/jsconfig-paths/node_modules/mypackage/data.js rename to test/e2e/jsconfig-paths/node_modules/mypackage/data.js diff --git a/test/integration/jsconfig-paths/node_modules/mypackage/myfile.js b/test/e2e/jsconfig-paths/node_modules/mypackage/myfile.js similarity index 100% rename from test/integration/jsconfig-paths/node_modules/mypackage/myfile.js rename to test/e2e/jsconfig-paths/node_modules/mypackage/myfile.js diff --git a/test/integration/jsconfig-paths/pages/basic-alias.js b/test/e2e/jsconfig-paths/pages/basic-alias.js similarity index 100% rename from test/integration/jsconfig-paths/pages/basic-alias.js rename to test/e2e/jsconfig-paths/pages/basic-alias.js diff --git a/test/integration/jsconfig-paths/pages/resolve-fallback.js b/test/e2e/jsconfig-paths/pages/resolve-fallback.js similarity index 100% rename from test/integration/jsconfig-paths/pages/resolve-fallback.js rename to test/e2e/jsconfig-paths/pages/resolve-fallback.js diff --git a/test/integration/jsconfig-paths/pages/resolve-order.js b/test/e2e/jsconfig-paths/pages/resolve-order.js similarity index 100% rename from test/integration/jsconfig-paths/pages/resolve-order.js rename to test/e2e/jsconfig-paths/pages/resolve-order.js diff --git a/test/integration/jsconfig-paths/pages/single-alias.js b/test/e2e/jsconfig-paths/pages/single-alias.js similarity index 100% rename from test/integration/jsconfig-paths/pages/single-alias.js rename to test/e2e/jsconfig-paths/pages/single-alias.js diff --git a/test/e2e/legacy-link-behavior/validations.console.test.ts b/test/e2e/legacy-link-behavior/validations.console.test.ts index e3377bb93da8..aba4d5d43310 100644 --- a/test/e2e/legacy-link-behavior/validations.console.test.ts +++ b/test/e2e/legacy-link-behavior/validations.console.test.ts @@ -2,13 +2,11 @@ import { isNextDev, nextTestSetup } from 'e2e-utils' import { waitForNoRedbox } from 'next-test-utils' describe('Validations for <Link legacyBehavior>', () => { - const { next, isNextDeploy } = nextTestSetup({ + const { next, skipped } = nextTestSetup({ files: __dirname, skipDeployment: true, }) - if (isNextDeploy) { - return it('should skip deploy', () => {}) - } + if (skipped) return let previousOutputIndex beforeEach(() => { diff --git a/test/integration/link-ref-app/app/child-ref-func-cleanup/page.js b/test/e2e/link-ref-app/app/child-ref-func-cleanup/page.js similarity index 100% rename from test/integration/link-ref-app/app/child-ref-func-cleanup/page.js rename to test/e2e/link-ref-app/app/child-ref-func-cleanup/page.js diff --git a/test/integration/link-ref-app/app/child-ref-func/page.js b/test/e2e/link-ref-app/app/child-ref-func/page.js similarity index 100% rename from test/integration/link-ref-app/app/child-ref-func/page.js rename to test/e2e/link-ref-app/app/child-ref-func/page.js diff --git a/test/integration/link-ref-app/app/child-ref/page.js b/test/e2e/link-ref-app/app/child-ref/page.js similarity index 100% rename from test/integration/link-ref-app/app/child-ref/page.js rename to test/e2e/link-ref-app/app/child-ref/page.js diff --git a/test/integration/link-ref-app/app/class/page.js b/test/e2e/link-ref-app/app/class/page.js similarity index 100% rename from test/integration/link-ref-app/app/class/page.js rename to test/e2e/link-ref-app/app/class/page.js diff --git a/test/integration/link-ref-app/app/click-away-race-condition/page.js b/test/e2e/link-ref-app/app/click-away-race-condition/page.js similarity index 100% rename from test/integration/link-ref-app/app/click-away-race-condition/page.js rename to test/e2e/link-ref-app/app/click-away-race-condition/page.js diff --git a/test/integration/link-ref-app/app/function/page.js b/test/e2e/link-ref-app/app/function/page.js similarity index 100% rename from test/integration/link-ref-app/app/function/page.js rename to test/e2e/link-ref-app/app/function/page.js diff --git a/test/integration/link-ref-app/app/layout.js b/test/e2e/link-ref-app/app/layout.js similarity index 100% rename from test/integration/link-ref-app/app/layout.js rename to test/e2e/link-ref-app/app/layout.js diff --git a/test/integration/link-ref-app/app/page.js b/test/e2e/link-ref-app/app/page.js similarity index 100% rename from test/integration/link-ref-app/app/page.js rename to test/e2e/link-ref-app/app/page.js diff --git a/test/e2e/link-ref-app/link-ref-app.test.ts b/test/e2e/link-ref-app/link-ref-app.test.ts new file mode 100644 index 000000000000..2d15bda5a572 --- /dev/null +++ b/test/e2e/link-ref-app/link-ref-app.test.ts @@ -0,0 +1,97 @@ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('Link ref app', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + const noError = async (pathname: string) => { + const browser = await next.browser('/') + await browser.eval(`(function() { + window.caughtErrors = [] + const origError = window.console.error + window.console.error = function (format) { + window.caughtErrors.push(format) + origError(arguments) + } + window.next.router.replace('${pathname}') + })()`) + await retry(async () => { + const errors = await browser.eval(`window.caughtErrors`) + expect(errors).toEqual([]) + }) + await browser.close() + } + + const didPrefetch = async (pathname: string) => { + const requests: string[] = [] + const browser = await next.browser(pathname, { + beforePageLoad(page: any) { + page.on('request', async (req: any) => { + const url = new URL(req.url()) + const headers = await req.allHeaders() + if (headers['next-router-prefetch']) { + requests.push(url.pathname) + } + }) + }, + }) + + await browser.waitForIdleNetwork() + + await retry(async () => { + expect(requests).toEqual( + expect.arrayContaining([expect.stringContaining('/')]) + ) + }) + + await browser.close() + } + + it('should not have a race condition with a click handler', async () => { + const browser = await next.browser('/click-away-race-condition') + await browser.elementByCss('#click-me').click() + await browser.waitForElementByCss('#the-menu') + }) + + if (isNextDev) { + it('should not show error for function component with forwardRef', async () => { + await noError('/function') + }) + + it('should not show error for class component as child of next/link', async () => { + await noError('/class') + }) + + it('should handle child ref with React.createRef', async () => { + await noError('/child-ref') + }) + + it('should handle child ref that is a function', async () => { + await noError('/child-ref-func') + }) + + it('should handle child ref that is a function that returns a cleanup function', async () => { + await noError('/child-ref-func-cleanup') + }) + } + + if (isNextStart) { + it('should preload with forwardRef', async () => { + await didPrefetch('/function') + }) + + it('should preload with child ref with React.createRef', async () => { + await didPrefetch('/child-ref') + }) + + it('should preload with child ref with function', async () => { + await didPrefetch('/child-ref-func') + }) + + it('should preload with child ref with function that returns a cleanup function', async () => { + await didPrefetch('/child-ref-func-cleanup') + }) + } +}) diff --git a/test/e2e/link-ref-pages/link-ref-pages.test.ts b/test/e2e/link-ref-pages/link-ref-pages.test.ts new file mode 100644 index 000000000000..418cb31365c8 --- /dev/null +++ b/test/e2e/link-ref-pages/link-ref-pages.test.ts @@ -0,0 +1,98 @@ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +import { + getClientBuildManifestLoaderChunkUrlPath, + retry, +} from 'next-test-utils' + +describe('Link ref forwarding', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + async function noError(pathname: string) { + const browser = await next.browser('/') + await browser.eval(`(function() { + window.caughtErrors = [] + const origError = window.console.error + window.console.error = function (format) { + window.caughtErrors.push(format) + origError(arguments) + } + window.next.router.replace('${pathname}') + })()`) + await retry(async () => { + const errors = await browser.eval(`window.caughtErrors`) + expect(errors).toEqual([]) + }) + await browser.close() + } + + async function didPrefetch(pathname: string) { + const browser = await next.browser(pathname) + const chunk = getClientBuildManifestLoaderChunkUrlPath(next.testDir, '/') + + await retry(async () => { + const links = await browser.elementsByCss('link[rel=prefetch]') + const hrefs = await Promise.all( + links.map((link) => link.getAttribute('href')) + ) + // Same as integration: prefetch hrefs must include the client build manifest loader chunk for `/`. + expect(hrefs).toEqual( + expect.arrayContaining([expect.stringContaining(chunk)]) + ) + }) + + await browser.close() + } + + function runCommonTests() { + it('should not have a race condition with a click handler', async () => { + const browser = await next.browser('/click-away-race-condition') + await browser.elementByCss('#click-me').click() + await browser.waitForElementByCss('#the-menu') + }) + } + + ;(isNextDev ? describe : describe.skip)('development mode', () => { + runCommonTests() + + it('should not show error for function component with forwardRef', async () => { + await noError('/function') + }) + + it('should not show error for class component as child of next/link', async () => { + await noError('/class') + }) + + it('should handle child ref with React.createRef', async () => { + await noError('/child-ref') + }) + + it('should handle child ref that is a function', async () => { + await noError('/child-ref-func') + }) + + it('should handle child ref that is a function that returns a cleanup function', async () => { + await noError('/child-ref-func-cleanup') + }) + }) + ;(isNextStart ? describe : describe.skip)('production mode', () => { + runCommonTests() + + it('should preload with forwardRef', async () => { + await didPrefetch('/function') + }) + + it('should preload with child ref with React.createRef', async () => { + await didPrefetch('/child-ref') + }) + + it('should preload with child ref with function', async () => { + await didPrefetch('/child-ref-func') + }) + + it('should preload with child ref with function that returns a cleanup function', async () => { + await didPrefetch('/child-ref-func-cleanup') + }) + }) +}) diff --git a/test/integration/link-ref-pages/pages/child-ref-func-cleanup.js b/test/e2e/link-ref-pages/pages/child-ref-func-cleanup.js similarity index 100% rename from test/integration/link-ref-pages/pages/child-ref-func-cleanup.js rename to test/e2e/link-ref-pages/pages/child-ref-func-cleanup.js diff --git a/test/integration/link-ref-pages/pages/child-ref-func.js b/test/e2e/link-ref-pages/pages/child-ref-func.js similarity index 100% rename from test/integration/link-ref-pages/pages/child-ref-func.js rename to test/e2e/link-ref-pages/pages/child-ref-func.js diff --git a/test/integration/link-ref-pages/pages/child-ref.js b/test/e2e/link-ref-pages/pages/child-ref.js similarity index 100% rename from test/integration/link-ref-pages/pages/child-ref.js rename to test/e2e/link-ref-pages/pages/child-ref.js diff --git a/test/integration/link-ref-pages/pages/class.js b/test/e2e/link-ref-pages/pages/class.js similarity index 100% rename from test/integration/link-ref-pages/pages/class.js rename to test/e2e/link-ref-pages/pages/class.js diff --git a/test/integration/link-ref-pages/pages/click-away-race-condition.js b/test/e2e/link-ref-pages/pages/click-away-race-condition.js similarity index 100% rename from test/integration/link-ref-pages/pages/click-away-race-condition.js rename to test/e2e/link-ref-pages/pages/click-away-race-condition.js diff --git a/test/integration/link-ref-pages/pages/function.js b/test/e2e/link-ref-pages/pages/function.js similarity index 100% rename from test/integration/link-ref-pages/pages/function.js rename to test/e2e/link-ref-pages/pages/function.js diff --git a/test/integration/invalid-config-values/pages/index.js b/test/e2e/link-ref-pages/pages/index.js similarity index 100% rename from test/integration/invalid-config-values/pages/index.js rename to test/e2e/link-ref-pages/pages/index.js diff --git a/test/e2e/middleware-basic/middleware-basic.test.ts b/test/e2e/middleware-basic/middleware-basic.test.ts new file mode 100644 index 000000000000..7c2c558f9af8 --- /dev/null +++ b/test/e2e/middleware-basic/middleware-basic.test.ts @@ -0,0 +1,14 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('middleware-basic', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + const header = 'X-From-Middleware' + + it('loads a middleware', async () => { + const response = await next.fetch('/post-1') + expect(response.headers.has(header)).toBe(true) + }) +}) diff --git a/test/integration/middleware-basic/middleware.ts b/test/e2e/middleware-basic/middleware.ts similarity index 100% rename from test/integration/middleware-basic/middleware.ts rename to test/e2e/middleware-basic/middleware.ts diff --git a/test/integration/middleware-basic/next.config.js b/test/e2e/middleware-basic/next.config.js similarity index 100% rename from test/integration/middleware-basic/next.config.js rename to test/e2e/middleware-basic/next.config.js diff --git a/test/integration/middleware-basic/pages/index.js b/test/e2e/middleware-basic/pages/index.js similarity index 100% rename from test/integration/middleware-basic/pages/index.js rename to test/e2e/middleware-basic/pages/index.js diff --git a/test/e2e/middleware-src-node/middleware-src-node.test.ts b/test/e2e/middleware-src-node/middleware-src-node.test.ts new file mode 100644 index 000000000000..acd8da03b04a --- /dev/null +++ b/test/e2e/middleware-src-node/middleware-src-node.test.ts @@ -0,0 +1,113 @@ +import { nextTestSetup, isNextDev } from 'e2e-utils' +import { retry } from 'next-test-utils' + +const srcHeader = 'X-From-Src-Middleware' +const rootHeader = 'X-From-Root-Middleware' + +describe('middleware-src-node', () => { + const { next, isTurbopack, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + if (skipped) return + + if (isNextDev) { + beforeAll(async () => { + await next.start() + }) + } + + describe('Middleware in src/ folder', () => { + if (isNextDev) { + it('loads and runs src middleware', async () => { + const response = await next.fetch('/post-1') + expect(response.headers.has(srcHeader)).toBe(false) + expect(response.headers.has(`${srcHeader}-TS`)).toBe(true) + }) + } + + if (!isNextDev) { + it('should warn about middleware on export', async () => { + await next.patchFile( + 'next.config.js', + "module.exports = { output: 'export' }" + ) + await next.build() + expect(next.cliOutput).toContain( + 'Statically exporting a Next.js application via `next export` disables API routes and middleware.' + ) + }) + } + }) + + describe('Middleware in src/ and / folders', () => { + beforeAll(async () => { + const pagesContent = await next.readFile('src/pages/index.js') + await next.patchFile('pages/index.js', pagesContent) + await next.patchFile( + 'middleware.js', + ` +import { NextResponse } from 'next/server' + +export default function () { + const response = NextResponse.next() + response.headers.set('${rootHeader}', 'true') + return response +}` + ) + await next.patchFile( + 'middleware.ts', + ` +import { NextResponse } from 'next/server' + +export default function () { + const response = NextResponse.next() + response.headers.set('${rootHeader}-TS', 'true') + return response +}` + ) + // Webpack dev does not reliably switch from the already-compiled + // src/middleware.* to the newly created root middleware.* files when + // they are added at runtime. Restarting the dev server forces a fresh + // middleware resolution. Turbopack picks up the new root middleware + // without a restart. In production mode no server is running here + // (skipStart: true), so the restart must not run. + if (isNextDev && !isTurbopack) { + await next.stop() + await next.start() + } + }) + + afterAll(async () => { + await next.deleteFile('pages/index.js').catch(() => {}) + await next.deleteFile('middleware.js').catch(() => {}) + await next.deleteFile('middleware.ts').catch(() => {}) + }) + + if (isNextDev) { + it('loads and runs only root middleware', async () => { + await retry(async () => { + const response = await next.fetch('/post-1') + expect(response.headers.has(srcHeader)).toBe(false) + expect(response.headers.has(`${srcHeader}-TS`)).toBe(false) + expect(response.headers.has(rootHeader)).toBe(false) + expect(response.headers.has(`${rootHeader}-TS`)).toBe(true) + }) + }) + } + + if (!isNextDev) { + it('should warn about middleware on export', async () => { + await next.patchFile( + 'next.config.js', + "module.exports = { output: 'export' }" + ) + await next.build() + expect(next.cliOutput).toContain( + 'Statically exporting a Next.js application via `next export` disables API routes and middleware.' + ) + }) + } + }) +}) diff --git a/test/integration/index-index/next.config.js b/test/e2e/middleware-src-node/next.config.js similarity index 100% rename from test/integration/index-index/next.config.js rename to test/e2e/middleware-src-node/next.config.js diff --git a/test/integration/middleware-src-node/src/middleware.js b/test/e2e/middleware-src-node/src/middleware.js similarity index 100% rename from test/integration/middleware-src-node/src/middleware.js rename to test/e2e/middleware-src-node/src/middleware.js diff --git a/test/integration/middleware-src-node/src/middleware.ts b/test/e2e/middleware-src-node/src/middleware.ts similarity index 100% rename from test/integration/middleware-src-node/src/middleware.ts rename to test/e2e/middleware-src-node/src/middleware.ts diff --git a/test/integration/middleware-src-node/src/pages/index.js b/test/e2e/middleware-src-node/src/pages/index.js similarity index 100% rename from test/integration/middleware-src-node/src/pages/index.js rename to test/e2e/middleware-src-node/src/pages/index.js diff --git a/test/e2e/middleware-src/middleware-src.test.ts b/test/e2e/middleware-src/middleware-src.test.ts new file mode 100644 index 000000000000..c2d5af147746 --- /dev/null +++ b/test/e2e/middleware-src/middleware-src.test.ts @@ -0,0 +1,114 @@ +import { nextTestSetup, isNextDev } from 'e2e-utils' +import { retry } from 'next-test-utils' + +const srcHeader = 'X-From-Src-Middleware' +const rootHeader = 'X-From-Root-Middleware' + +describe('middleware-src', () => { + const { next, isTurbopack, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + if (skipped) return + + if (isNextDev) { + beforeAll(async () => { + await next.start() + }) + } + + describe('Middleware in src/ folder', () => { + if (isNextDev) { + it('loads and runs src middleware', async () => { + const response = await next.fetch('/post-1') + expect(response.headers.has(srcHeader)).toBe(false) + expect(response.headers.has(`${srcHeader}-TS`)).toBe(true) + }) + } + + if (!isNextDev) { + it('should warn about middleware on export', async () => { + await next.patchFile( + 'next.config.js', + "module.exports = { output: 'export' }" + ) + await next.build() + expect(next.cliOutput).toContain( + 'Statically exporting a Next.js application via `next export` disables API routes and middleware.' + ) + }) + } + }) + + describe('Middleware in src/ and / folders', () => { + beforeAll(async () => { + const pagesContent = await next.readFile('src/pages/index.js') + await next.patchFile('pages/index.js', pagesContent) + await next.patchFile( + 'middleware.js', + ` +import { NextResponse } from 'next/server' + +export default function () { + const response = NextResponse.next() + response.headers.set('${rootHeader}', 'true') + return response +}` + ) + await next.patchFile( + 'middleware.ts', + ` +import { NextResponse } from 'next/server' + +export default function () { + const response = NextResponse.next() + response.headers.set('${rootHeader}-TS', 'true') + return response +}` + ) + // Webpack dev does not reliably switch from the already-compiled + // src/middleware.* to the newly created root middleware.* files when + // they are added at runtime. Restarting the dev server forces a fresh + // middleware resolution. Turbopack picks up the new root middleware + // without a restart. In production mode the server is never started + // (skipStart: true + only `next.build()` is invoked), so the restart + // dance must be limited to dev mode. + if (isNextDev && !isTurbopack) { + await next.stop() + await next.start() + } + }) + + afterAll(async () => { + await next.deleteFile('pages/index.js').catch(() => {}) + await next.deleteFile('middleware.js').catch(() => {}) + await next.deleteFile('middleware.ts').catch(() => {}) + }) + + if (isNextDev) { + it('loads and runs only root middleware', async () => { + await retry(async () => { + const response = await next.fetch('/post-1') + expect(response.headers.has(srcHeader)).toBe(false) + expect(response.headers.has(`${srcHeader}-TS`)).toBe(false) + expect(response.headers.has(rootHeader)).toBe(false) + expect(response.headers.has(`${rootHeader}-TS`)).toBe(true) + }) + }) + } + + if (!isNextDev) { + it('should warn about middleware on export', async () => { + await next.patchFile( + 'next.config.js', + "module.exports = { output: 'export' }" + ) + await next.build() + expect(next.cliOutput).toContain( + 'Statically exporting a Next.js application via `next export` disables API routes and middleware.' + ) + }) + } + }) +}) diff --git a/test/integration/middleware-src/src/middleware.js b/test/e2e/middleware-src/src/middleware.js similarity index 100% rename from test/integration/middleware-src/src/middleware.js rename to test/e2e/middleware-src/src/middleware.js diff --git a/test/integration/middleware-src/src/middleware.ts b/test/e2e/middleware-src/src/middleware.ts similarity index 100% rename from test/integration/middleware-src/src/middleware.ts rename to test/e2e/middleware-src/src/middleware.ts diff --git a/test/integration/middleware-src/src/pages/index.js b/test/e2e/middleware-src/src/pages/index.js similarity index 100% rename from test/integration/middleware-src/src/pages/index.js rename to test/e2e/middleware-src/src/pages/index.js diff --git a/test/integration/module-ids/components/CustomComponent.tsx b/test/e2e/module-ids/components/CustomComponent.tsx similarity index 100% rename from test/integration/module-ids/components/CustomComponent.tsx rename to test/e2e/module-ids/components/CustomComponent.tsx diff --git a/test/e2e/module-ids/module-ids.test.ts b/test/e2e/module-ids/module-ids.test.ts new file mode 100644 index 000000000000..d79699d1e35b --- /dev/null +++ b/test/e2e/module-ids/module-ids.test.ts @@ -0,0 +1,121 @@ +import { nextTestSetup } from 'e2e-utils' +import { listClientChunks } from 'next-test-utils' +import fs from 'fs-extra' +import path from 'path' + +describe('minified module ids', () => { + ;(!process.env.IS_TURBOPACK_TEST ? describe.skip : describe)( + 'production mode', + () => { + const { next, isNextStart } = nextTestSetup({ + files: __dirname, + }) + if (!isNextStart) { + it('skipped for non-start mode', () => {}) + return + } + + let ssrBundles = '' + let staticBundles = '' + + beforeAll(async () => { + const distDir = path.join(next.testDir, next.distDir) + const ssrPath = path.join(distDir, 'server/chunks/ssr/') + const ssrBundleBasenames = (await fs.readdir(ssrPath)).filter((p) => + p.match(/\.js$/) + ) + for (const basename of ssrBundleBasenames) { + const output = await fs.readFile(path.join(ssrPath, basename), 'utf8') + ssrBundles += output + } + + const staticBundleBasenames = (await listClientChunks(distDir)).filter( + (p) => p.endsWith('.js') + ) + for (const basename of staticBundleBasenames) { + const output = await fs.readFile(path.join(distDir, basename), 'utf8') + staticBundles += output + } + }) + + it('should have no long module ids for basic modules', async () => { + expect(ssrBundles).not.toContain('module-with-long-name') + expect(ssrBundles).toContain('the content of a module with a long name') + }) + + it('should have no long module ids for external modules', async () => { + expect(ssrBundles).not.toContain('external-module-with-long-name') + expect(ssrBundles).toContain( + 'the content of an external module with a long name' + ) + }) + + it('should have no long module ids for async loader modules', async () => { + expect(ssrBundles).not.toContain('CustomComponent.tsx') + expect(ssrBundles).toContain('the content of a dynamic component') + }) + + it('should have no long module id for the next client runtime module', async () => { + expect(staticBundles).not.toContain('next/dist/client/next-turbopack') + }) + } + ) + ;(!process.env.IS_TURBOPACK_TEST ? describe.skip : describe)( + 'development mode', + () => { + const { next, isNextDev } = nextTestSetup({ + files: __dirname, + }) + if (!isNextDev) { + it('skipped for non-dev mode', () => {}) + return + } + + let ssrBundles = '' + let staticBundles = '' + + beforeAll(async () => { + await next.render('/') + + const distDir = path.join(next.testDir, next.distDir) + const ssrPath = path.join(distDir, 'server/chunks/ssr/') + const ssrBundleBasenames = (await fs.readdir(ssrPath)).filter((p) => + p.match(/\.js$/) + ) + for (const basename of ssrBundleBasenames) { + const output = await fs.readFile(path.join(ssrPath, basename), 'utf8') + ssrBundles += output + } + + const staticBundleBasenames = (await listClientChunks(distDir)).filter( + (p) => p.endsWith('.js') + ) + for (const basename of staticBundleBasenames) { + const output = await fs.readFile(path.join(distDir, basename), 'utf8') + staticBundles += output + } + }) + + it('should have long module ids for basic modules', async () => { + expect(ssrBundles).toContain('module-with-long-name') + expect(ssrBundles).toContain('the content of a module with a long name') + }) + + it('should have long module ids for external modules', async () => { + expect(ssrBundles).toContain('external-module-with-long-name') + expect(ssrBundles).toContain( + 'the content of an external module with a long name' + ) + }) + + it('should have long module ids for async loader modules', async () => { + expect(ssrBundles).toContain('CustomComponent.tsx') + expect(ssrBundles).toContain('the content of a dynamic component') + }) + + it('should have long module id for the next client runtime module', async () => { + expect(staticBundles).toContain('next/dist/client/next-dev-turbopack') + }) + } + ) +}) diff --git a/test/integration/module-ids/module-with-long-name.js b/test/e2e/module-ids/module-with-long-name.js similarity index 100% rename from test/integration/module-ids/module-with-long-name.js rename to test/e2e/module-ids/module-with-long-name.js diff --git a/test/integration/module-ids/next.config.js b/test/e2e/module-ids/next.config.js similarity index 100% rename from test/integration/module-ids/next.config.js rename to test/e2e/module-ids/next.config.js diff --git a/test/integration/module-ids/node_modules/external-module-with-long-name.js b/test/e2e/module-ids/node_modules/external-module-with-long-name.js similarity index 100% rename from test/integration/module-ids/node_modules/external-module-with-long-name.js rename to test/e2e/module-ids/node_modules/external-module-with-long-name.js diff --git a/test/integration/module-ids/pages/index.js b/test/e2e/module-ids/pages/index.js similarity index 100% rename from test/integration/module-ids/pages/index.js rename to test/e2e/module-ids/pages/index.js diff --git a/test/e2e/next-analyze/next-analyze.test.ts b/test/e2e/next-analyze/next-analyze.test.ts index 3fe1f603a651..b44db7f84a12 100644 --- a/test/e2e/next-analyze/next-analyze.test.ts +++ b/test/e2e/next-analyze/next-analyze.test.ts @@ -1,7 +1,7 @@ import { nextTestSetup } from 'e2e-utils' -import { runNextCommand, shouldUseTurbopack } from 'next-test-utils' +import { shouldUseTurbopack } from 'next-test-utils' import path from 'node:path' -import { ChildProcess, spawn } from 'node:child_process' +import type { ChildProcess } from 'node:child_process' import { existsSync, readFileSync } from 'node:fs' describe('next experimental-analyze', () => { @@ -24,27 +24,46 @@ describe('next experimental-analyze', () => { } it('runs successfully without errors', async () => { - const nextDir = path.dirname(require.resolve('next/package')) - const nextBin = path.join(nextDir, 'dist/bin/next') + let serveProcess: ChildProcess | undefined + let stdoutBuffer = '' + let resolveUrl!: (url: string) => void + let rejectUrl!: (err: Error) => void + const urlPromise = new Promise<string>((resolve, reject) => { + resolveUrl = resolve + rejectUrl = reject + }) - const serveProcess = spawn( - 'node', - [nextBin, 'experimental-analyze', '--port', '0'], - { - cwd: next.testDir, - stdio: ['ignore', 'pipe', 'pipe'], - } - ) + const timeout = setTimeout(() => { + rejectUrl(new Error('Server did not start within timeout')) + }, 30000) + + const exit = next + .runCommand(['experimental-analyze', '--port', '0'], { + onStdout(msg) { + stdoutBuffer += msg + const urlMatch = stdoutBuffer.match(/http:\/\/[^\s]+/) + if (urlMatch) { + resolveUrl(urlMatch[0]) + } + }, + instance(p) { + serveProcess = p + }, + }) + .finally(() => { + clearTimeout(timeout) + }) try { - const url = await waitForServer(serveProcess) + const url = await urlPromise const response = await fetch(url) expect(response.status).toBe(200) expect(await response.text()).toContain( '<title>Next.js Bundle Analyzer' ) } finally { - serveProcess.kill() + serveProcess?.kill() + await exit.catch(() => {}) } }) ;['-o', '--output'].forEach((flag) => { @@ -55,16 +74,12 @@ describe('next experimental-analyze', () => { '.next/diagnostics/analyze' ) - const { code, stderr, stdout } = await runNextCommand( - ['experimental-analyze', flag], - { - cwd: next.testDir, - stderr: true, - stdout: true, - } - ) + const { exitCode, stderr, stdout } = await next.runCommand([ + 'experimental-analyze', + flag, + ]) - expect(code).toBe(0) + expect(exitCode).toBe(0) expect(stderr).not.toContain('Error') expect(stdout).toContain('.next/diagnostics/analyze') @@ -88,51 +103,3 @@ describe('next experimental-analyze', () => { }) }) }) - -function waitForServer(process: ChildProcess, timeoutMs: number = 30000) { - const serverUrlPromise = new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - process.stdout.off('data', onStdout) - process.off('error', onError) - process.off('exit', onExit) - reject(new Error('Server did not start within timeout')) - }, timeoutMs) - - function onStdout(data: Buffer) { - const urlMatch = data.toString().match(/http:\/\/[^\s]+/) - if (urlMatch) { - clearTimeout(timeout) - process.stdout.off('data', onStdout) - process.off('error', onError) - process.off('exit', onExit) - resolve(urlMatch[0]) - } - } - - function onError(error: Error) { - clearTimeout(timeout) - process.stdout.off('data', onStdout) - process.off('error', onError) - process.off('exit', onExit) - reject(error) - } - - function onExit(code: number) { - clearTimeout(timeout) - process.stdout.off('data', onStdout) - process.off('error', onError) - process.off('exit', onExit) - reject( - new Error( - `Server process exited with code ${code} before URL was emitted` - ) - ) - } - - process.stdout.on('data', onStdout) - process.on('error', onError) - process.on('exit', onExit) - }) - - return serverUrlPromise -} diff --git a/test/e2e/next-dynamic-css-asset-prefix/next-dynamic-css-asset-prefix.test.ts b/test/e2e/next-dynamic-css-asset-prefix/next-dynamic-css-asset-prefix.test.ts new file mode 100644 index 000000000000..923dd471d0cf --- /dev/null +++ b/test/e2e/next-dynamic-css-asset-prefix/next-dynamic-css-asset-prefix.test.ts @@ -0,0 +1,105 @@ +import { createServer, request } from 'http' +import type { Server } from 'http' +import { findPort } from 'next-test-utils' +import { nextTestSetup, isNextDev } from 'e2e-utils' + +describe('next/dynamic with assetPrefix', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + dependencies: { + sass: '1.54.0', + }, + skipDeployment: true, + }) + if (skipped) return + + let cdnPort: number + let cdn: Server + + function createCdnProxy(targetPort: number) { + return createServer((clientReq, clientRes) => { + const proxyPath = clientReq.url!.slice('/path-prefix'.length) + const proxyReq = request( + { + hostname: 'localhost', + port: targetPort, + path: proxyPath, + method: clientReq.method, + headers: clientReq.headers, + }, + (proxyRes) => { + proxyRes.headers['Access-Control-Allow-Origin'] = + `http://localhost:${targetPort}` + clientRes.writeHead(proxyRes.statusCode!, proxyRes.headers) + proxyRes.on('error', (e) => { + require('console').error(e) + }) + clientRes.on('error', (e) => { + require('console').error(e) + }) + proxyRes.pipe(clientRes, { end: true }) + } + ) + + proxyReq.on('error', (e) => { + require('console').error(e) + }) + clientReq.on('error', (e) => { + require('console').error(e) + }) + clientReq.pipe(proxyReq, { end: true }) + }) + } + + beforeAll(async () => { + cdnPort = await findPort() + + await next.patchFile('next.config.js', (content) => + content.replace('__CDN_PORT__', String(cdnPort)) + ) + + if (!isNextDev) { + await next.build() + } + await next.start() + + const nextPort = Number(new URL(next.url).port) + cdn = createCdnProxy(nextPort) + await new Promise((resolve) => cdn.listen(cdnPort, resolve)) + }) + + afterAll(() => { + if (cdn) { + cdn.close() + } + }) + + it('should load a Pages Router page correctly', async () => { + const browser = await next.browser('/') + + expect( + await browser + .elementByCss('#__next div:nth-child(2)') + .getComputedCss('background-color') + ).toContain('221, 221, 221') + + expect(await browser.eval('document.documentElement.innerHTML')).toContain( + 'Where does it come from?' + ) + }) + + it('should load a App Router page correctly', async () => { + const browser = await next.browser('/test-app') + + expect( + await browser + .elementByCss('body div:nth-child(3)') + .getComputedCss('background-color') + ).toContain('221, 221, 221') + + expect(await browser.eval('document.documentElement.innerHTML')).toContain( + 'Where does it come from?' + ) + }) +}) diff --git a/test/integration/absolute-assetprefix/next.config.js b/test/e2e/next-dynamic-css-asset-prefix/next.config.js similarity index 100% rename from test/integration/absolute-assetprefix/next.config.js rename to test/e2e/next-dynamic-css-asset-prefix/next.config.js diff --git a/test/integration/next-dynamic-css-asset-prefix/src/Component2.jsx b/test/e2e/next-dynamic-css-asset-prefix/src/Component2.jsx similarity index 100% rename from test/integration/next-dynamic-css-asset-prefix/src/Component2.jsx rename to test/e2e/next-dynamic-css-asset-prefix/src/Component2.jsx diff --git a/test/integration/next-dynamic-css-asset-prefix/src/Component2.module.scss b/test/e2e/next-dynamic-css-asset-prefix/src/Component2.module.scss similarity index 100% rename from test/integration/next-dynamic-css-asset-prefix/src/Component2.module.scss rename to test/e2e/next-dynamic-css-asset-prefix/src/Component2.module.scss diff --git a/test/integration/next-dynamic-css-asset-prefix/src/Content.jsx b/test/e2e/next-dynamic-css-asset-prefix/src/Content.jsx similarity index 100% rename from test/integration/next-dynamic-css-asset-prefix/src/Content.jsx rename to test/e2e/next-dynamic-css-asset-prefix/src/Content.jsx diff --git a/test/integration/next-dynamic-css-asset-prefix/src/Content.module.css b/test/e2e/next-dynamic-css-asset-prefix/src/Content.module.css similarity index 100% rename from test/integration/next-dynamic-css-asset-prefix/src/Content.module.css rename to test/e2e/next-dynamic-css-asset-prefix/src/Content.module.css diff --git a/test/integration/next-dynamic-css-asset-prefix/src/Content4.module.css b/test/e2e/next-dynamic-css-asset-prefix/src/Content4.module.css similarity index 100% rename from test/integration/next-dynamic-css-asset-prefix/src/Content4.module.css rename to test/e2e/next-dynamic-css-asset-prefix/src/Content4.module.css diff --git a/test/integration/next-dynamic-css-asset-prefix/src/app/layout.tsx b/test/e2e/next-dynamic-css-asset-prefix/src/app/layout.tsx similarity index 100% rename from test/integration/next-dynamic-css-asset-prefix/src/app/layout.tsx rename to test/e2e/next-dynamic-css-asset-prefix/src/app/layout.tsx diff --git a/test/integration/next-dynamic-css-asset-prefix/src/app/test-app/page.tsx b/test/e2e/next-dynamic-css-asset-prefix/src/app/test-app/page.tsx similarity index 100% rename from test/integration/next-dynamic-css-asset-prefix/src/app/test-app/page.tsx rename to test/e2e/next-dynamic-css-asset-prefix/src/app/test-app/page.tsx diff --git a/test/integration/next-dynamic-css-asset-prefix/src/inner/k.jsx b/test/e2e/next-dynamic-css-asset-prefix/src/inner/k.jsx similarity index 100% rename from test/integration/next-dynamic-css-asset-prefix/src/inner/k.jsx rename to test/e2e/next-dynamic-css-asset-prefix/src/inner/k.jsx diff --git a/test/integration/next-dynamic-css-asset-prefix/src/pages/index.jsx b/test/e2e/next-dynamic-css-asset-prefix/src/pages/index.jsx similarity index 100% rename from test/integration/next-dynamic-css-asset-prefix/src/pages/index.jsx rename to test/e2e/next-dynamic-css-asset-prefix/src/pages/index.jsx diff --git a/test/e2e/next-dynamic-css/next-dynamic-css.test.ts b/test/e2e/next-dynamic-css/next-dynamic-css.test.ts new file mode 100644 index 000000000000..27633a93814e --- /dev/null +++ b/test/e2e/next-dynamic-css/next-dynamic-css.test.ts @@ -0,0 +1,38 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('next/dynamic css', () => { + const { next } = nextTestSetup({ + files: __dirname, + dependencies: { + sass: 'latest', + }, + }) + + it('should load a Pages Router page correctly', async () => { + const browser = await next.browser('/') + + expect( + await browser + .elementByCss('#__next div:nth-child(2)') + .getComputedCss('background-color') + ).toContain('221, 221, 221') + + expect(await browser.eval('document.documentElement.innerHTML')).toContain( + 'Where does it come from?' + ) + }) + + it('should load a App Router page correctly', async () => { + const browser = await next.browser('/test-app') + + expect( + await browser + .elementByCss('body div:nth-child(3)') + .getComputedCss('background-color') + ).toContain('221, 221, 221') + + expect(await browser.eval('document.documentElement.innerHTML')).toContain( + 'Where does it come from?' + ) + }) +}) diff --git a/test/integration/middleware-src-node/next.config.js b/test/e2e/next-dynamic-css/next.config.js similarity index 100% rename from test/integration/middleware-src-node/next.config.js rename to test/e2e/next-dynamic-css/next.config.js diff --git a/test/integration/next-dynamic-css/src/Component2.jsx b/test/e2e/next-dynamic-css/src/Component2.jsx similarity index 100% rename from test/integration/next-dynamic-css/src/Component2.jsx rename to test/e2e/next-dynamic-css/src/Component2.jsx diff --git a/test/integration/next-dynamic-css/src/Component2.module.scss b/test/e2e/next-dynamic-css/src/Component2.module.scss similarity index 100% rename from test/integration/next-dynamic-css/src/Component2.module.scss rename to test/e2e/next-dynamic-css/src/Component2.module.scss diff --git a/test/integration/next-dynamic-css/src/Content.jsx b/test/e2e/next-dynamic-css/src/Content.jsx similarity index 100% rename from test/integration/next-dynamic-css/src/Content.jsx rename to test/e2e/next-dynamic-css/src/Content.jsx diff --git a/test/integration/next-dynamic-css/src/Content.module.css b/test/e2e/next-dynamic-css/src/Content.module.css similarity index 100% rename from test/integration/next-dynamic-css/src/Content.module.css rename to test/e2e/next-dynamic-css/src/Content.module.css diff --git a/test/integration/next-dynamic-css/src/Content4.module.css b/test/e2e/next-dynamic-css/src/Content4.module.css similarity index 100% rename from test/integration/next-dynamic-css/src/Content4.module.css rename to test/e2e/next-dynamic-css/src/Content4.module.css diff --git a/test/integration/next-dynamic-css/src/app/layout.tsx b/test/e2e/next-dynamic-css/src/app/layout.tsx similarity index 100% rename from test/integration/next-dynamic-css/src/app/layout.tsx rename to test/e2e/next-dynamic-css/src/app/layout.tsx diff --git a/test/integration/next-dynamic-css/src/app/test-app/page.tsx b/test/e2e/next-dynamic-css/src/app/test-app/page.tsx similarity index 100% rename from test/integration/next-dynamic-css/src/app/test-app/page.tsx rename to test/e2e/next-dynamic-css/src/app/test-app/page.tsx diff --git a/test/integration/next-dynamic-css/src/inner/k.jsx b/test/e2e/next-dynamic-css/src/inner/k.jsx similarity index 100% rename from test/integration/next-dynamic-css/src/inner/k.jsx rename to test/e2e/next-dynamic-css/src/inner/k.jsx diff --git a/test/integration/next-dynamic-css/src/pages/index.jsx b/test/e2e/next-dynamic-css/src/pages/index.jsx similarity index 100% rename from test/integration/next-dynamic-css/src/pages/index.jsx rename to test/e2e/next-dynamic-css/src/pages/index.jsx diff --git a/test/integration/next-dynamic-lazy-compilation/.babelrc b/test/e2e/next-dynamic-lazy-compilation/.babelrc similarity index 100% rename from test/integration/next-dynamic-lazy-compilation/.babelrc rename to test/e2e/next-dynamic-lazy-compilation/.babelrc diff --git a/test/integration/next-dynamic-lazy-compilation/apples/index.js b/test/e2e/next-dynamic-lazy-compilation/apples/index.js similarity index 100% rename from test/integration/next-dynamic-lazy-compilation/apples/index.js rename to test/e2e/next-dynamic-lazy-compilation/apples/index.js diff --git a/test/integration/next-dynamic-lazy-compilation/components/four.js b/test/e2e/next-dynamic-lazy-compilation/components/four.js similarity index 100% rename from test/integration/next-dynamic-lazy-compilation/components/four.js rename to test/e2e/next-dynamic-lazy-compilation/components/four.js diff --git a/test/integration/next-dynamic-lazy-compilation/components/one.js b/test/e2e/next-dynamic-lazy-compilation/components/one.js similarity index 100% rename from test/integration/next-dynamic-lazy-compilation/components/one.js rename to test/e2e/next-dynamic-lazy-compilation/components/one.js diff --git a/test/integration/next-dynamic-lazy-compilation/components/three.js b/test/e2e/next-dynamic-lazy-compilation/components/three.js similarity index 100% rename from test/integration/next-dynamic-lazy-compilation/components/three.js rename to test/e2e/next-dynamic-lazy-compilation/components/three.js diff --git a/test/integration/next-dynamic-lazy-compilation/components/two.js b/test/e2e/next-dynamic-lazy-compilation/components/two.js similarity index 100% rename from test/integration/next-dynamic-lazy-compilation/components/two.js rename to test/e2e/next-dynamic-lazy-compilation/components/two.js diff --git a/test/e2e/next-dynamic-lazy-compilation/next-dynamic-lazy-compilation.test.ts b/test/e2e/next-dynamic-lazy-compilation/next-dynamic-lazy-compilation.test.ts new file mode 100644 index 000000000000..d5b0c4839408 --- /dev/null +++ b/test/e2e/next-dynamic-lazy-compilation/next-dynamic-lazy-compilation.test.ts @@ -0,0 +1,40 @@ +import { nextTestSetup } from 'e2e-utils' +import { shouldUseTurbopack } from 'next-test-utils' + +// This test relies on an experimental webpack feature (lazyCompilation). +describe('next/dynamic lazy compilation', () => { + if (shouldUseTurbopack()) { + it('skips in Turbopack tests', () => {}) + return + } + + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should render server value', async () => { + const html = await next.render('/') + expect(html).toMatch(/the-server-value/i) + expect(html).toMatch(/the-second-server-value/i) + }) + + it('should render dynamic server rendered values before hydration', async () => { + const browser = await next.browser('/') + const text = await browser.elementByCss('#before-hydration').text() + + expect(text).toMatch( + /^Index1()+2()+3()+4()+4$/ + ) + expect(await browser.eval('window.caughtErrors')).toBe('') + }) + + it('should render dynamic server rendered values on client mount', async () => { + const browser = await next.browser('/') + const text = await browser.elementByCss('#first-render').text() + + expect(text).toMatch( + /^Index1()+2()+3()+4()+4$/ + ) + expect(await browser.eval('window.caughtErrors')).toBe('') + }) +}) diff --git a/test/integration/next-dynamic-lazy-compilation/next.config.js b/test/e2e/next-dynamic-lazy-compilation/next.config.js similarity index 100% rename from test/integration/next-dynamic-lazy-compilation/next.config.js rename to test/e2e/next-dynamic-lazy-compilation/next.config.js diff --git a/test/integration/next-dynamic-lazy-compilation/pages/index.js b/test/e2e/next-dynamic-lazy-compilation/pages/index.js similarity index 100% rename from test/integration/next-dynamic-lazy-compilation/pages/index.js rename to test/e2e/next-dynamic-lazy-compilation/pages/index.js diff --git a/test/integration/next-dynamic/apples/index.js b/test/e2e/next-dynamic/apples/index.js similarity index 100% rename from test/integration/next-dynamic/apples/index.js rename to test/e2e/next-dynamic/apples/index.js diff --git a/test/integration/next-dynamic/components/four.js b/test/e2e/next-dynamic/components/four.js similarity index 100% rename from test/integration/next-dynamic/components/four.js rename to test/e2e/next-dynamic/components/four.js diff --git a/test/integration/next-dynamic/components/one.js b/test/e2e/next-dynamic/components/one.js similarity index 100% rename from test/integration/next-dynamic/components/one.js rename to test/e2e/next-dynamic/components/one.js diff --git a/test/integration/next-dynamic/components/three.js b/test/e2e/next-dynamic/components/three.js similarity index 100% rename from test/integration/next-dynamic/components/three.js rename to test/e2e/next-dynamic/components/three.js diff --git a/test/integration/next-dynamic/components/two.js b/test/e2e/next-dynamic/components/two.js similarity index 100% rename from test/integration/next-dynamic/components/two.js rename to test/e2e/next-dynamic/components/two.js diff --git a/test/e2e/next-dynamic/next-dynamic.test.ts b/test/e2e/next-dynamic/next-dynamic.test.ts new file mode 100644 index 000000000000..91a0fad8e8fa --- /dev/null +++ b/test/e2e/next-dynamic/next-dynamic.test.ts @@ -0,0 +1,27 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('next/dynamic', () => { + const { next } = nextTestSetup({ files: __dirname }) + + it('should render server value', async () => { + const html = await next.render('/') + expect(html).toMatch(/the-server-value/i) + }) + + it('should render dynamic server rendered values on client mount', async () => { + const browser = await next.browser('/') + const text = await browser.elementByCss('#first-render').text() + + // Failure case is 'Index3' + expect(text).toMatch( + /^Index1()+2()+3()+4()+4$/ + ) + expect(await browser.eval('window.caughtErrors')).toBe('') + + // should not print "invalid-dynamic-suspense" warning in browser's console + const logs = (await browser.log()).map((log) => log.message).join('\n') + expect(logs).not.toContain( + 'https://nextjs.org/docs/messages/invalid-dynamic-suspense' + ) + }) +}) diff --git a/test/integration/next-dynamic/pages/index.js b/test/e2e/next-dynamic/pages/index.js similarity index 100% rename from test/integration/next-dynamic/pages/index.js rename to test/e2e/next-dynamic/pages/index.js diff --git a/test/e2e/next-image-legacy/asset-prefix/asset-prefix.test.ts b/test/e2e/next-image-legacy/asset-prefix/asset-prefix.test.ts new file mode 100644 index 000000000000..823c742691b8 --- /dev/null +++ b/test/e2e/next-image-legacy/asset-prefix/asset-prefix.test.ts @@ -0,0 +1,33 @@ +import { nextTestSetup, isNextDev } from 'e2e-utils' + +describe('Legacy Image Component assetPrefix Tests', () => { + const { next, isTurbopack } = nextTestSetup({ + files: __dirname, + }) + + it('should include assetPrefix when placeholder=blur during dev', async () => { + if (!isNextDev) return + const browser = await next.browser('/') + const id = 'test1' + const bgImage = await browser.eval( + `document.getElementById('${id}').style['background-image']` + ) + if (isTurbopack) { + expect(bgImage).toContain('data:image/jpeg;') + } else { + expect(bgImage).toMatch( + /\/_next\/image\?url=https%3A%2F%2Fexample.com%2Fpre%2F_next%2Fstatic%2Fmedia%2Ftest(.+).jpg&w=8&q=70/ + ) + } + }) + + it('should use base64 data url with placeholder=blur during production', async () => { + if (isNextDev) return + const browser = await next.browser('/') + const id = 'test1' + const bgImage = await browser.eval( + `document.getElementById('${id}').style['background-image']` + ) + expect(bgImage).toMatch('data:image/jpeg;base64') + }) +}) diff --git a/test/integration/next-image-legacy/asset-prefix/next.config.js b/test/e2e/next-image-legacy/asset-prefix/next.config.js similarity index 100% rename from test/integration/next-image-legacy/asset-prefix/next.config.js rename to test/e2e/next-image-legacy/asset-prefix/next.config.js diff --git a/test/integration/next-image-legacy/asset-prefix/pages/index.js b/test/e2e/next-image-legacy/asset-prefix/pages/index.js similarity index 100% rename from test/integration/next-image-legacy/asset-prefix/pages/index.js rename to test/e2e/next-image-legacy/asset-prefix/pages/index.js diff --git a/test/integration/build-trace-extra-entries/app/public/test.jpg b/test/e2e/next-image-legacy/asset-prefix/public/test.jpg similarity index 100% rename from test/integration/build-trace-extra-entries/app/public/test.jpg rename to test/e2e/next-image-legacy/asset-prefix/public/test.jpg diff --git a/test/integration/next-image-legacy/base-path/test/static.test.ts b/test/e2e/next-image-legacy/base-path/base-path-static.test.ts similarity index 71% rename from test/integration/next-image-legacy/base-path/test/static.test.ts rename to test/e2e/next-image-legacy/base-path/base-path-static.test.ts index 2311fa3abcfd..1f45bff55e0f 100644 --- a/test/integration/next-image-legacy/base-path/test/static.test.ts +++ b/test/e2e/next-image-legacy/base-path/base-path-static.test.ts @@ -1,25 +1,51 @@ -import { - findPort, - killApp, - nextBuild, - nextStart, - renderViaHTTP, - File, - waitFor, - launchApp, -} from 'next-test-utils' -import webdriver from 'next-webdriver' -import { join } from 'path' +import { nextTestSetup, isNextDev } from 'e2e-utils' -const appDir = join(__dirname, '../') -let appPort -let app -let browser -let html +describe('Build Error Tests for basePath', () => { + const { next } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + + if (isNextDev) { + it('no-op in dev', () => {}) + return + } + + it('should throw build error when import statement is used with missing file', async () => { + await next.patchFile( + 'pages/static-img.js', + (content) => + content.replace( + '../public/foo/test-rect.jpg', + '../public/foo/test-rect-broken.jpg' + ), + async () => { + const { cliOutput } = await next.build() + expect(cliOutput).toContain( + "Module not found: Can't resolve '../public/foo/test-rect-broken.jpg" + ) + expect(cliOutput).toContain('pages/static-img.js') + } + ) + }) +}) -const indexPage = new File(join(appDir, 'pages/static-img.js')) +describe('Static Image Component Tests for basePath', () => { + const { next, isTurbopack, skipped } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + }) + if (skipped) return + + let browser: Awaited> + let html: string + + beforeAll(async () => { + html = await next.render('/docs/static-img') + browser = await next.browser('/docs/static-img') + }) -const runTests = (isDev = false) => { it('Should allow an image with a static src to omit height and width', async () => { expect(await browser.elementById('basic-static')).toBeTruthy() expect(await browser.elementById('blur-png')).toBeTruthy() @@ -32,11 +58,12 @@ const runTests = (isDev = false) => { expect(await browser.elementById('static-ico')).toBeTruthy() expect(await browser.elementById('static-unoptimized')).toBeTruthy() }) + it('Should use immutable cache-control header for static import', async () => { await browser.eval( `document.getElementById("basic-static").scrollIntoView()` ) - await waitFor(1000) + await new Promise((resolve) => setTimeout(resolve, 1000)) const url = await browser.eval( `document.getElementById("basic-static").src` ) @@ -46,12 +73,12 @@ const runTests = (isDev = false) => { ) }) - if (!isDev) { + if (!isNextDev) { it('Should use immutable cache-control header even when unoptimized', async () => { await browser.eval( `document.getElementById("static-unoptimized").scrollIntoView()` ) - await waitFor(1000) + await new Promise((resolve) => setTimeout(resolve, 1000)) const url = await browser.eval( `document.getElementById("static-unoptimized").src` ) @@ -61,97 +88,45 @@ const runTests = (isDev = false) => { ) }) } + it('Should automatically provide an image height and width', async () => { expect(html).toContain('width:400px;height:300px') }) + it('Should allow provided width and height to override intrinsic', async () => { expect(html).toContain('width:200px;height:200px') expect(html).not.toContain('width:400px;height:400px') }) + it('Should add a blur placeholder to statically imported jpg', async () => { - if (process.env.IS_TURBOPACK_TEST) { + if (isTurbopack) { expect(html).toContain( `style="position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("data:image/jpeg;base64` ) } else { expect(html).toContain( `style="position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url(${ - isDev + isNextDev ? '"/docs/_next/image?url=%2Fdocs%2F_next%2Fstatic%2Fmedia%2Ftest.fab2915d.jpg&w=8&q=70"' : '"data:image/jpeg;base64,/9j/2wBDAAoKCgoKCgsMDAsPEA4QDxYUExMUFiIYGhgaGCIzICUgICUgMy03LCksNy1RQDg4QFFeT0pPXnFlZXGPiI+7u/v/2wBDAQoKCgoKCgsMDAsPEA4QDxYUExMUFiIYGhgaGCIzICUgICUgMy03LCksNy1RQDg4QFFeT0pPXnFlZXGPiI+7u/v/wgARCAAGAAgDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAACUg//EABwQAAICAgMAAAAAAAAAAAAAABITERQAAwUVIv/aAAgBAQABPwB3H9YmrsuvN5+VxADn/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgEBPwB//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAwEBPwB//9k="' })` ) } }) + it('Should add a blur placeholder to statically imported png', async () => { - if (process.env.IS_TURBOPACK_TEST) { + if (isTurbopack) { expect(html).toContain( `style="position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("data:image/png;base64` ) } else { expect(html).toContain( `style="position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url(${ - isDev + isNextDev ? '"/docs/_next/image?url=%2Fdocs%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=8&q=70"' : '"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAMAAADz0U65AAAAElBMVEUAAAA6OjolJSWwsLAfHx/9/f2oxsg2AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAH0lEQVR4nGNgwAaYmKAMZmYIzcjKyghmsDAysmDTAgAEXAAhXbseDQAAAABJRU5ErkJggg=="' })` ) } }) -} - -describe('Build Error Tests for basePath', () => { - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( - 'production mode', - () => { - it('should throw build error when import statement is used with missing file', async () => { - await indexPage.replace( - '../public/foo/test-rect.jpg', - '../public/foo/test-rect-broken.jpg' - ) - - const { stderr } = await nextBuild(appDir, undefined, { stderr: true }) - await indexPage.restore() - - expect(stderr).toContain( - "Module not found: Can't resolve '../public/foo/test-rect-broken.jpg" - ) - // should contain the importing module - expect(stderr).toContain('pages/static-img.js') - }) - } - ) -}) -describe('Static Image Component Tests for basePath', () => { - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( - 'production mode', - () => { - beforeAll(async () => { - await nextBuild(appDir) - appPort = await findPort() - app = await nextStart(appDir, appPort) - html = await renderViaHTTP(appPort, '/docs/static-img') - browser = await webdriver(appPort, '/docs/static-img') - }) - afterAll(async () => { - await killApp(app) - }) - runTests() - } - ) - ;(process.env.TURBOPACK_BUILD ? describe.skip : describe)( - 'development mode', - () => { - beforeAll(async () => { - appPort = await findPort() - app = await launchApp(appDir, appPort) - html = await renderViaHTTP(appPort, '/docs/static-img') - browser = await webdriver(appPort, '/docs/static-img') - }) - afterAll(async () => { - await killApp(app) - }) - runTests(true) - } - ) }) diff --git a/test/e2e/next-image-legacy/base-path/base-path.test.ts b/test/e2e/next-image-legacy/base-path/base-path.test.ts new file mode 100644 index 000000000000..7fa6e5a1db48 --- /dev/null +++ b/test/e2e/next-image-legacy/base-path/base-path.test.ts @@ -0,0 +1,399 @@ +import { nextTestSetup, isNextDev } from 'e2e-utils' +import { + waitForRedbox, + waitForNoRedbox, + getRedboxHeader, + getDeploymentId, + retry, +} from 'next-test-utils' + +describe('Legacy Image Component basePath Tests', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + disableAutoSkewProtection: true, + // Image URL assertions construct expected URLs via + // `getDeploymentId(next.testDir, ...)`, which reads the local + // `.next/required-server-files.json`. In deploy mode that file lives on + // Vercel's infrastructure (not on disk locally), so the constructed + // expected URL omits the `&dpl=...` query that Vercel injects at + // runtime. The assertions are about local-build URL shape, not deploy + // CDN URLs, so skip in deploy. + skipDeployment: true, + }) + if (skipped) return + + let dpl: string + beforeAll(() => { + dpl = getDeploymentId(next.testDir, isNextDev).getDeploymentIdQuery(true) + }) + + async function getImageUrls(browser) { + return await Promise.all( + (await browser.elementsByCss('img')).map(async (link) => + new URL(await link.getAttribute('src'), next.url).toString() + ) + ) + } + + async function getComputed(browser, id, prop) { + const val = await browser.eval(`document.getElementById('${id}').${prop}`) + if (typeof val === 'number') { + return val + } + if (typeof val === 'string') { + const v = parseInt(val, 10) + if (isNaN(v)) { + return val + } + return v + } + return null + } + + async function getSrc(browser, id: string) { + const src = await browser.elementById(id).getAttribute('src') + if (src) { + const url = new URL(src, next.url) + return url.href.slice(url.origin.length) + } + } + + function getRatio(width, height) { + return height / width + } + + it('should load the images', async () => { + const browser = await next.browser('/docs') + + await retry(async () => { + const result = await browser.eval( + `document.getElementById('basic-image').naturalWidth` + ) + expect(result).not.toBe(0) + }) + + expect(await getImageUrls(browser)).toContain( + `${next.url}/docs/_next/image?url=%2Fdocs%2Ftest.jpg&w=828&q=75${dpl}` + ) + }) + + it('should update the image on src change', async () => { + const browser = await next.browser('/docs/update') + + await retry(async () => { + const src = await browser.eval( + `document.getElementById("update-image").src` + ) + expect(src).toMatch(/test\.jpg/) + }) + + await browser.eval(`document.getElementById("toggle").click()`) + + await retry(async () => { + const src = await browser.eval( + `document.getElementById("update-image").src` + ) + expect(src).toMatch(/test\.png/) + }) + }) + + it('should work when using flexbox', async () => { + const browser = await next.browser('/docs/flex') + await retry(async () => { + const result = await browser.eval( + `document.getElementById('basic-image').width` + ) + expect(result).not.toBe(0) + }) + }) + + it('should work with layout-fixed so resizing window does not resize image', async () => { + const browser = await next.browser('/docs/layout-fixed') + const width = 1200 + const height = 700 + const delta = 250 + const id = 'fixed1' + expect(await getSrc(browser, id)).toBe( + `/docs/_next/image?url=%2Fdocs%2Fwide.png&w=3840&q=75${dpl}` + ) + expect(await browser.elementById(id).getAttribute('srcset')).toBe( + `/docs/_next/image?url=%2Fdocs%2Fwide.png&w=1200&q=75${dpl} 1x, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=3840&q=75${dpl} 2x` + ) + expect(await browser.elementById(id).getAttribute('sizes')).toBeFalsy() + await browser.setDimensions({ + width: width + delta, + height: height + delta, + }) + expect(await getComputed(browser, id, 'width')).toBe(width) + expect(await getComputed(browser, id, 'height')).toBe(height) + await browser.setDimensions({ + width: width - delta, + height: height - delta, + }) + expect(await getComputed(browser, id, 'width')).toBe(width) + expect(await getComputed(browser, id, 'height')).toBe(height) + }) + + it('should work with layout-intrinsic so resizing window maintains image aspect ratio', async () => { + const browser = await next.browser('/docs/layout-intrinsic') + const width = 1200 + const height = 700 + const delta = 250 + const id = 'intrinsic1' + + await retry(async () => { + expect(await getSrc(browser, id)).toBe( + `/docs/_next/image?url=%2Fdocs%2Fwide.png&w=3840&q=75${dpl}` + ) + }) + expect(await browser.elementById(id).getAttribute('srcset')).toBe( + `/docs/_next/image?url=%2Fdocs%2Fwide.png&w=1200&q=75${dpl} 1x, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=3840&q=75${dpl} 2x` + ) + expect(await browser.elementById(id).getAttribute('sizes')).toBeFalsy() + await browser.setDimensions({ + width: width + delta, + height: height + delta, + }) + expect(await getComputed(browser, id, 'width')).toBe(width) + expect(await getComputed(browser, id, 'height')).toBe(height) + await browser.setDimensions({ + width: width - delta, + height: height - delta, + }) + const newWidth = await getComputed(browser, id, 'width') + const newHeight = await getComputed(browser, id, 'height') + expect(newWidth).toBeLessThan(width) + expect(newHeight).toBeLessThan(height) + expect(getRatio(newWidth, newHeight)).toBeCloseTo( + getRatio(width, height), + 1 + ) + }) + + it('should work with layout-responsive so resizing window maintains image aspect ratio', async () => { + const browser = await next.browser('/docs/layout-responsive') + const width = 1200 + const height = 700 + const delta = 250 + const id = 'responsive1' + + await retry(async () => { + expect(await getSrc(browser, id)).toBe( + `/docs/_next/image?url=%2Fdocs%2Fwide.png&w=3840&q=75${dpl}` + ) + }) + expect(await browser.elementById(id).getAttribute('srcset')).toBe( + `/docs/_next/image?url=%2Fdocs%2Fwide.png&w=640&q=75${dpl} 640w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=750&q=75${dpl} 750w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=828&q=75${dpl} 828w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=1080&q=75${dpl} 1080w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=1200&q=75${dpl} 1200w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=1920&q=75${dpl} 1920w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=2048&q=75${dpl} 2048w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=3840&q=75${dpl} 3840w` + ) + expect(await browser.elementById(id).getAttribute('sizes')).toBe('100vw') + await browser.setDimensions({ + width: width + delta, + height: height + delta, + }) + expect(await getComputed(browser, id, 'width')).toBeGreaterThan(width) + expect(await getComputed(browser, id, 'height')).toBeGreaterThan(height) + await browser.setDimensions({ + width: width - delta, + height: height - delta, + }) + const newWidth = await getComputed(browser, id, 'width') + const newHeight = await getComputed(browser, id, 'height') + expect(newWidth).toBeLessThan(width) + expect(newHeight).toBeLessThan(height) + expect(getRatio(newWidth, newHeight)).toBeCloseTo( + getRatio(width, height), + 1 + ) + }) + + it('should work with layout-fill to fill the parent but NOT stretch with viewport', async () => { + const browser = await next.browser('/docs/layout-fill') + const width = 600 + const height = 350 + const delta = 150 + const id = 'fill1' + + await retry(async () => { + expect(await getSrc(browser, id)).toBe( + `/docs/_next/image?url=%2Fdocs%2Fwide.png&w=3840&q=75${dpl}` + ) + }) + expect(await browser.elementById(id).getAttribute('srcset')).toBe( + `/docs/_next/image?url=%2Fdocs%2Fwide.png&w=640&q=75${dpl} 640w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=750&q=75${dpl} 750w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=828&q=75${dpl} 828w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=1080&q=75${dpl} 1080w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=1200&q=75${dpl} 1200w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=1920&q=75${dpl} 1920w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=2048&q=75${dpl} 2048w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=3840&q=75${dpl} 3840w` + ) + expect(await browser.elementById(id).getAttribute('sizes')).toBe('100vw') + await browser.setDimensions({ + width: width + delta, + height: height + delta, + }) + expect(await getComputed(browser, id, 'width')).toBe(width) + expect(await getComputed(browser, id, 'height')).toBe(height) + await browser.setDimensions({ + width: width - delta, + height: height - delta, + }) + const newWidth = await getComputed(browser, id, 'width') + const newHeight = await getComputed(browser, id, 'height') + expect(newWidth).toBe(width) + expect(newHeight).toBe(height) + expect(getRatio(newWidth, newHeight)).toBeCloseTo( + getRatio(width, height), + 1 + ) + }) + + it('should work with layout-fill to fill the parent and stretch with viewport', async () => { + const browser = await next.browser('/docs/layout-fill') + const id = 'fill2' + const width = await getComputed(browser, id, 'width') + const height = await getComputed(browser, id, 'height') + await browser.eval(`document.getElementById("${id}").scrollIntoView()`) + + await retry(async () => { + expect(await getSrc(browser, id)).toBe( + `/docs/_next/image?url=%2Fdocs%2Fwide.png&w=3840&q=75${dpl}` + ) + }) + expect(await browser.elementById(id).getAttribute('srcset')).toBe( + `/docs/_next/image?url=%2Fdocs%2Fwide.png&w=640&q=75${dpl} 640w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=750&q=75${dpl} 750w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=828&q=75${dpl} 828w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=1080&q=75${dpl} 1080w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=1200&q=75${dpl} 1200w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=1920&q=75${dpl} 1920w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=2048&q=75${dpl} 2048w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=3840&q=75${dpl} 3840w` + ) + expect(await browser.elementById(id).getAttribute('sizes')).toBe('100vw') + expect(await getComputed(browser, id, 'width')).toBe(width) + expect(await getComputed(browser, id, 'height')).toBe(height) + const delta = 150 + const largeWidth = Number(width) + delta + const largeHeight = Number(height) + delta + await browser.setDimensions({ + width: largeWidth, + height: largeHeight, + }) + expect(await getComputed(browser, id, 'width')).toBe(largeWidth) + expect(await getComputed(browser, id, 'height')).toBe(largeHeight) + const smallWidth = Number(width) - delta + const smallHeight = Number(height) - delta + await browser.setDimensions({ + width: smallWidth, + height: smallHeight, + }) + expect(await getComputed(browser, id, 'width')).toBe(smallWidth) + expect(await getComputed(browser, id, 'height')).toBe(smallHeight) + + const objectFit = await browser.eval( + `document.getElementById("${id}").style.objectFit` + ) + const objectPosition = await browser.eval( + `document.getElementById("${id}").style.objectPosition` + ) + expect(objectFit).toBe('cover') + expect(objectPosition).toBe('left center') + }) + + it('should work with sizes and automatically use layout-responsive', async () => { + const browser = await next.browser('/docs/sizes') + const width = 1200 + const height = 700 + const delta = 250 + const id = 'sizes1' + + await retry(async () => { + expect(await getSrc(browser, id)).toBe( + `/docs/_next/image?url=%2Fdocs%2Fwide.png&w=3840&q=75${dpl}` + ) + }) + expect(await browser.elementById(id).getAttribute('srcset')).toBe( + `/docs/_next/image?url=%2Fdocs%2Fwide.png&w=32&q=75${dpl} 32w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=48&q=75${dpl} 48w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=64&q=75${dpl} 64w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=96&q=75${dpl} 96w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=128&q=75${dpl} 128w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=256&q=75${dpl} 256w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=384&q=75${dpl} 384w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=640&q=75${dpl} 640w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=750&q=75${dpl} 750w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=828&q=75${dpl} 828w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=1080&q=75${dpl} 1080w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=1200&q=75${dpl} 1200w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=1920&q=75${dpl} 1920w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=2048&q=75${dpl} 2048w, /docs/_next/image?url=%2Fdocs%2Fwide.png&w=3840&q=75${dpl} 3840w` + ) + expect(await browser.elementById(id).getAttribute('sizes')).toBe( + '(max-width: 2048px) 1200px, 3840px' + ) + await browser.setDimensions({ + width: width + delta, + height: height + delta, + }) + expect(await getComputed(browser, id, 'width')).toBeGreaterThan(width) + expect(await getComputed(browser, id, 'height')).toBeGreaterThan(height) + await browser.setDimensions({ + width: width - delta, + height: height - delta, + }) + const newWidth = await getComputed(browser, id, 'width') + const newHeight = await getComputed(browser, id, 'height') + expect(newWidth).toBeLessThan(width) + expect(newHeight).toBeLessThan(height) + expect(getRatio(newWidth, newHeight)).toBeCloseTo( + getRatio(width, height), + 1 + ) + }) + + if (isNextDev) { + it('should show missing src error', async () => { + const browser = await next.browser('/docs/missing-src') + + await waitForNoRedbox(browser) + + await retry(async () => { + const logs = (await browser.log()).map((log) => log.message).join('\n') + expect(logs).toMatch(/Image is missing required "src" property/gm) + }) + }) + + it('should show invalid src error', async () => { + const browser = await next.browser('/docs/invalid-src') + + await waitForRedbox(browser) + expect(await getRedboxHeader(browser)).toContain( + 'Invalid src prop (https://google.com/test.png) on `next/image`, hostname "google.com" is not configured under images in your `next.config.js`' + ) + }) + + it('should show invalid src error when protocol-relative', async () => { + const browser = await next.browser('/docs/invalid-src-proto-relative') + + await waitForRedbox(browser) + expect(await getRedboxHeader(browser)).toContain( + 'Failed to parse src "//assets.example.com/img.jpg" on `next/image`, protocol-relative URL (//) must be changed to an absolute URL (http:// or https://)' + ) + }) + } + + it('should correctly ignore prose styles', async () => { + const browser = await next.browser('/docs/prose') + + const id = 'prose-image' + + await retry(async () => { + const result = await browser.eval( + `document.getElementById(${JSON.stringify(id)}).naturalWidth` + ) + expect(result).toBeGreaterThan(0) + }) + + await new Promise((resolve) => setTimeout(resolve, 1000)) + + const computedWidth = await getComputed(browser, id, 'width') + const computedHeight = await getComputed(browser, id, 'height') + expect(getRatio(computedWidth, computedHeight)).toBeCloseTo(1, 1) + }) + + if (!isNextDev) { + it('should correctly rotate image', async () => { + const browser = await next.browser('/docs/rotated') + + const id = 'exif-rotation-image' + + await retry(async () => { + const result = await browser.eval( + `document.getElementById(${JSON.stringify(id)}).naturalWidth` + ) + expect(result).toBeGreaterThan(0) + }) + + await new Promise((resolve) => setTimeout(resolve, 1000)) + + const computedWidth = await getComputed(browser, id, 'width') + const computedHeight = await getComputed(browser, id, 'height') + expect(getRatio(computedWidth, computedHeight)).toBeCloseTo(0.5625, 1) + }) + } +}) diff --git a/test/integration/next-image-legacy/base-path/components/TallImage.js b/test/e2e/next-image-legacy/base-path/components/TallImage.js similarity index 100% rename from test/integration/next-image-legacy/base-path/components/TallImage.js rename to test/e2e/next-image-legacy/base-path/components/TallImage.js diff --git a/test/integration/next-image-legacy/base-path/components/tall.png b/test/e2e/next-image-legacy/base-path/components/tall.png similarity index 100% rename from test/integration/next-image-legacy/base-path/components/tall.png rename to test/e2e/next-image-legacy/base-path/components/tall.png diff --git a/test/integration/next-image-legacy/base-path/next.config.js b/test/e2e/next-image-legacy/base-path/next.config.js similarity index 100% rename from test/integration/next-image-legacy/base-path/next.config.js rename to test/e2e/next-image-legacy/base-path/next.config.js diff --git a/test/integration/next-image-legacy/base-path/pages/flex.js b/test/e2e/next-image-legacy/base-path/pages/flex.js similarity index 100% rename from test/integration/next-image-legacy/base-path/pages/flex.js rename to test/e2e/next-image-legacy/base-path/pages/flex.js diff --git a/test/integration/next-image-legacy/base-path/pages/hidden-parent.js b/test/e2e/next-image-legacy/base-path/pages/hidden-parent.js similarity index 100% rename from test/integration/next-image-legacy/base-path/pages/hidden-parent.js rename to test/e2e/next-image-legacy/base-path/pages/hidden-parent.js diff --git a/test/integration/next-image-legacy/base-path/pages/index.js b/test/e2e/next-image-legacy/base-path/pages/index.js similarity index 100% rename from test/integration/next-image-legacy/base-path/pages/index.js rename to test/e2e/next-image-legacy/base-path/pages/index.js diff --git a/test/integration/next-image-legacy/base-path/pages/invalid-src-proto-relative.js b/test/e2e/next-image-legacy/base-path/pages/invalid-src-proto-relative.js similarity index 100% rename from test/integration/next-image-legacy/base-path/pages/invalid-src-proto-relative.js rename to test/e2e/next-image-legacy/base-path/pages/invalid-src-proto-relative.js diff --git a/test/integration/next-image-legacy/base-path/pages/invalid-src.js b/test/e2e/next-image-legacy/base-path/pages/invalid-src.js similarity index 100% rename from test/integration/next-image-legacy/base-path/pages/invalid-src.js rename to test/e2e/next-image-legacy/base-path/pages/invalid-src.js diff --git a/test/integration/next-image-legacy/base-path/pages/layout-fill.js b/test/e2e/next-image-legacy/base-path/pages/layout-fill.js similarity index 100% rename from test/integration/next-image-legacy/base-path/pages/layout-fill.js rename to test/e2e/next-image-legacy/base-path/pages/layout-fill.js diff --git a/test/integration/next-image-legacy/base-path/pages/layout-fixed.js b/test/e2e/next-image-legacy/base-path/pages/layout-fixed.js similarity index 100% rename from test/integration/next-image-legacy/base-path/pages/layout-fixed.js rename to test/e2e/next-image-legacy/base-path/pages/layout-fixed.js diff --git a/test/integration/next-image-legacy/base-path/pages/layout-intrinsic.js b/test/e2e/next-image-legacy/base-path/pages/layout-intrinsic.js similarity index 100% rename from test/integration/next-image-legacy/base-path/pages/layout-intrinsic.js rename to test/e2e/next-image-legacy/base-path/pages/layout-intrinsic.js diff --git a/test/integration/next-image-legacy/base-path/pages/layout-responsive.js b/test/e2e/next-image-legacy/base-path/pages/layout-responsive.js similarity index 100% rename from test/integration/next-image-legacy/base-path/pages/layout-responsive.js rename to test/e2e/next-image-legacy/base-path/pages/layout-responsive.js diff --git a/test/integration/next-image-legacy/base-path/pages/missing-src.js b/test/e2e/next-image-legacy/base-path/pages/missing-src.js similarity index 100% rename from test/integration/next-image-legacy/base-path/pages/missing-src.js rename to test/e2e/next-image-legacy/base-path/pages/missing-src.js diff --git a/test/integration/next-image-legacy/base-path/pages/prose.js b/test/e2e/next-image-legacy/base-path/pages/prose.js similarity index 100% rename from test/integration/next-image-legacy/base-path/pages/prose.js rename to test/e2e/next-image-legacy/base-path/pages/prose.js diff --git a/test/integration/next-image-legacy/base-path/pages/prose.module.css b/test/e2e/next-image-legacy/base-path/pages/prose.module.css similarity index 100% rename from test/integration/next-image-legacy/base-path/pages/prose.module.css rename to test/e2e/next-image-legacy/base-path/pages/prose.module.css diff --git a/test/integration/next-image-legacy/base-path/pages/rotated.js b/test/e2e/next-image-legacy/base-path/pages/rotated.js similarity index 100% rename from test/integration/next-image-legacy/base-path/pages/rotated.js rename to test/e2e/next-image-legacy/base-path/pages/rotated.js diff --git a/test/integration/next-image-legacy/base-path/pages/sizes.js b/test/e2e/next-image-legacy/base-path/pages/sizes.js similarity index 100% rename from test/integration/next-image-legacy/base-path/pages/sizes.js rename to test/e2e/next-image-legacy/base-path/pages/sizes.js diff --git a/test/integration/next-image-legacy/base-path/pages/static-img.js b/test/e2e/next-image-legacy/base-path/pages/static-img.js similarity index 100% rename from test/integration/next-image-legacy/base-path/pages/static-img.js rename to test/e2e/next-image-legacy/base-path/pages/static-img.js diff --git a/test/integration/next-image-legacy/base-path/pages/update.js b/test/e2e/next-image-legacy/base-path/pages/update.js similarity index 100% rename from test/integration/next-image-legacy/base-path/pages/update.js rename to test/e2e/next-image-legacy/base-path/pages/update.js diff --git a/test/integration/next-image-legacy/base-path/public/exif-rotation.jpg b/test/e2e/next-image-legacy/base-path/public/exif-rotation.jpg similarity index 100% rename from test/integration/next-image-legacy/base-path/public/exif-rotation.jpg rename to test/e2e/next-image-legacy/base-path/public/exif-rotation.jpg diff --git a/test/integration/next-image-legacy/base-path/public/foo/test-rect.jpg b/test/e2e/next-image-legacy/base-path/public/foo/test-rect.jpg similarity index 100% rename from test/integration/next-image-legacy/base-path/public/foo/test-rect.jpg rename to test/e2e/next-image-legacy/base-path/public/foo/test-rect.jpg diff --git a/test/integration/next-image-legacy/base-path/public/test.avif b/test/e2e/next-image-legacy/base-path/public/test.avif similarity index 100% rename from test/integration/next-image-legacy/base-path/public/test.avif rename to test/e2e/next-image-legacy/base-path/public/test.avif diff --git a/test/integration/next-image-legacy/base-path/public/test.bmp b/test/e2e/next-image-legacy/base-path/public/test.bmp similarity index 100% rename from test/integration/next-image-legacy/base-path/public/test.bmp rename to test/e2e/next-image-legacy/base-path/public/test.bmp diff --git a/test/integration/next-image-legacy/base-path/public/test.gif b/test/e2e/next-image-legacy/base-path/public/test.gif similarity index 100% rename from test/integration/next-image-legacy/base-path/public/test.gif rename to test/e2e/next-image-legacy/base-path/public/test.gif diff --git a/test/integration/next-image-legacy/base-path/public/test.ico b/test/e2e/next-image-legacy/base-path/public/test.ico similarity index 100% rename from test/integration/next-image-legacy/base-path/public/test.ico rename to test/e2e/next-image-legacy/base-path/public/test.ico diff --git a/test/integration/image-optimizer/app/public/test.jpg b/test/e2e/next-image-legacy/base-path/public/test.jpg similarity index 100% rename from test/integration/image-optimizer/app/public/test.jpg rename to test/e2e/next-image-legacy/base-path/public/test.jpg diff --git a/test/integration/next-image-legacy/base-path/public/test.png b/test/e2e/next-image-legacy/base-path/public/test.png similarity index 100% rename from test/integration/next-image-legacy/base-path/public/test.png rename to test/e2e/next-image-legacy/base-path/public/test.png diff --git a/test/integration/next-image-legacy/base-path/public/test.svg b/test/e2e/next-image-legacy/base-path/public/test.svg similarity index 100% rename from test/integration/next-image-legacy/base-path/public/test.svg rename to test/e2e/next-image-legacy/base-path/public/test.svg diff --git a/test/integration/next-image-legacy/base-path/public/test.tiff b/test/e2e/next-image-legacy/base-path/public/test.tiff similarity index 100% rename from test/integration/next-image-legacy/base-path/public/test.tiff rename to test/e2e/next-image-legacy/base-path/public/test.tiff diff --git a/test/integration/next-image-legacy/default/public/test.webp b/test/e2e/next-image-legacy/base-path/public/test.webp similarity index 100% rename from test/integration/next-image-legacy/default/public/test.webp rename to test/e2e/next-image-legacy/base-path/public/test.webp diff --git a/test/integration/next-image-legacy/base-path/public/wide.png b/test/e2e/next-image-legacy/base-path/public/wide.png similarity index 100% rename from test/integration/next-image-legacy/base-path/public/wide.png rename to test/e2e/next-image-legacy/base-path/public/wide.png diff --git a/test/integration/next-image-legacy/default/components/TallImage.js b/test/e2e/next-image-legacy/default/components/TallImage.js similarity index 100% rename from test/integration/next-image-legacy/default/components/TallImage.js rename to test/e2e/next-image-legacy/default/components/TallImage.js diff --git a/test/integration/next-image-legacy/default/components/static-img.js b/test/e2e/next-image-legacy/default/components/static-img.js similarity index 100% rename from test/integration/next-image-legacy/default/components/static-img.js rename to test/e2e/next-image-legacy/default/components/static-img.js diff --git a/test/integration/next-image-legacy/default/components/tall.png b/test/e2e/next-image-legacy/default/components/tall.png similarity index 100% rename from test/integration/next-image-legacy/default/components/tall.png rename to test/e2e/next-image-legacy/default/components/tall.png diff --git a/test/e2e/next-image-legacy/default/default-static.test.ts b/test/e2e/next-image-legacy/default/default-static.test.ts new file mode 100644 index 000000000000..e1b0b9fceb44 --- /dev/null +++ b/test/e2e/next-image-legacy/default/default-static.test.ts @@ -0,0 +1,190 @@ +/* eslint-disable jest/no-standalone-expect */ +import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils' +import cheerio from 'cheerio' + +describe('Build Error Tests', () => { + const { next, isTurbopack, isNextDeploy } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + if (isNextDeploy) return + ;(isNextStart ? it : it.skip)( + 'should throw build error when import statement is used with missing file', + async () => { + await next.patchFile( + 'pages/static-img.js', + (content) => + content.replace( + '../public/foo/test-rect.jpg', + '../public/foo/test-rect-broken.jpg' + ), + async () => { + const { cliOutput } = await next.build() + expect(cliOutput).toContain( + "Module not found: Can't resolve '../public/foo/test-rect-broken.jpg" + ) + if (isTurbopack) { + expect(cliOutput).toContain('pages/static-img.js') + } else { + expect(cliOutput).toContain('./pages/static-img.js') + } + expect(cliOutput).not.toContain('Import trace for requested module') + } + ) + } + ) +}) + +describe('Static Image Component Tests', () => { + const { next, isTurbopack, skipped } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + }) + if (skipped) return + + let browser: Awaited> + let html: string + + beforeAll(async () => { + html = await next.render('/static-img') + browser = await next.browser('/static-img') + }) + + it('Should allow an image with a static src to omit height and width', async () => { + expect(await browser.elementById('basic-static')).toBeTruthy() + expect(await browser.elementById('blur-png')).toBeTruthy() + expect(await browser.elementById('blur-webp')).toBeTruthy() + expect(await browser.elementById('blur-avif')).toBeTruthy() + expect(await browser.elementById('blur-jpg')).toBeTruthy() + expect(await browser.elementById('static-svg')).toBeTruthy() + expect(await browser.elementById('static-gif')).toBeTruthy() + expect(await browser.elementById('static-bmp')).toBeTruthy() + expect(await browser.elementById('static-ico')).toBeTruthy() + expect(await browser.elementById('static-unoptimized')).toBeTruthy() + }) + ;(isNextStart ? it : it.skip)( + 'Should use immutable cache-control header for static import', + async () => { + await browser.eval( + `document.getElementById("basic-static").scrollIntoView()` + ) + await new Promise((resolve) => setTimeout(resolve, 1000)) + const url = await browser.eval( + `document.getElementById("basic-static").src` + ) + const res = await fetch(url) + expect(res.headers.get('cache-control')).toBe( + 'public, max-age=315360000, immutable' + ) + } + ) + ;(isNextStart ? it : it.skip)( + 'Should use immutable cache-control header even when unoptimized', + async () => { + await browser.eval( + `document.getElementById("static-unoptimized").scrollIntoView()` + ) + await new Promise((resolve) => setTimeout(resolve, 1000)) + const url = await browser.eval( + `document.getElementById("static-unoptimized").src` + ) + const res = await fetch(url) + expect(res.headers.get('cache-control')).toBe( + 'public, max-age=31536000, immutable' + ) + } + ) + + it('Should automatically provide an image height and width', async () => { + expect(html).toContain('width:400px;height:300px') + }) + + it('Should allow provided width and height to override intrinsic', async () => { + expect(html).toContain('width:200px;height:200px') + expect(html).not.toContain('width:400px;height:400px') + }) + + it('Should add a blur placeholder to statically imported jpg', async () => { + const $ = cheerio.load(html) + const style = $('#basic-static').attr('style') + if (isTurbopack) { + expect(replaceDataUrl(style)).toMatchInlineSnapshot( + `"position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("data:")"` + ) + } else if (isNextDev) { + // In webpack dev, `next/legacy/image` emits a dynamic blur URL via the + // image optimizer route instead of an inlined base64 data URL, to avoid + // slowing down the dev server (see + // `packages/next/src/build/webpack/loaders/next-image-loader/blur.ts`). + expect(replaceBlurUrl(style)).toMatchInlineSnapshot( + `"position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("")"` + ) + } else { + expect(replaceDataUrl(style)).toMatchInlineSnapshot( + `"position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("data:")"` + ) + } + }) + + it('Should add a blur placeholder to statically imported png', async () => { + const $ = cheerio.load(html) + const style = $('#basic-static')[2].attribs.style + if (isTurbopack) { + expect(style).toMatchInlineSnapshot( + `"position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAICAYAAAA870V8AAAARUlEQVR42l3MoQ0AQQhE0XG7xWwIJSBIKBRJOZRBEXOWnPjimQ8AXC3ce+nuPOcQEcHuppkRVcWZYWYSIkJV5XvvN9j4AFZHJTnjDHb/AAAAAElFTkSuQmCC")"` + ) + } else if (isNextDev) { + // In webpack dev, `next/legacy/image` emits a dynamic blur URL via the + // image optimizer route instead of an inlined base64 data URL, to avoid + // slowing down the dev server (see + // `packages/next/src/build/webpack/loaders/next-image-loader/blur.ts`). + expect(replaceBlurUrl(style)).toMatchInlineSnapshot( + `"position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("")"` + ) + } else { + // In webpack start, the exact base64 output of the blur placeholder + // depends on the environment's sharp/libvips version, so normalize the + // data URL contents to only assert the data URL prefix. + expect(replaceDataUrl(style)).toMatchInlineSnapshot( + `"position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0% 0%;filter:blur(20px);background-image:url("data:")"` + ) + } + }) + + it('should load direct imported image', async () => { + const src = await browser.elementById('basic-static').getAttribute('src') + expect(src).toMatch( + /_next\/image\?url=%2F_next%2Fstatic%2F(immutable%2F)?media%2Ftest-rect(.+)\.jpg&w=828&q=75/ + ) + const fullSrc = new URL(src, next.url) + const res = await fetch(fullSrc) + expect(res.status).toBe(200) + }) + + it('should load staticprops imported image', async () => { + const src = await browser + .elementById('basic-staticprop') + .getAttribute('src') + expect(src).toMatch( + /_next\/image\?url=%2F_next%2Fstatic%2F(immutable%2F)?media%2Fexif-rotation(.+)\.jpg&w=256&q=75/ + ) + const fullSrc = new URL(src, next.url) + const res = await fetch(fullSrc) + expect(res.status).toBe(200) + }) +}) + +function replaceDataUrl(styles) { + return styles.replace(/url\("data:[^"]+"\)/g, 'url("data:")') +} + +// Webpack dev emits a dynamic blur URL that points at the image optimizer +// route, e.g. `/_next/image?url=...&w=8&q=70`. Normalize it so the snapshot +// only asserts the style shape, not the encoded src. +function replaceBlurUrl(styles) { + return styles.replace( + /url\("\/_next\/image\?[^"]+"\)/g, + 'url("")' + ) +} diff --git a/test/e2e/next-image-legacy/default/default.test.ts b/test/e2e/next-image-legacy/default/default.test.ts new file mode 100644 index 000000000000..6d3a2cc7dff5 --- /dev/null +++ b/test/e2e/next-image-legacy/default/default.test.ts @@ -0,0 +1,1302 @@ +import cheerio from 'cheerio' +import validateHTML from 'html-validator' +import { nextTestSetup, isNextDev } from 'e2e-utils' +import { + waitForRedbox, + waitForNoRedbox, + getRedboxHeader, + retry, +} from 'next-test-utils' + +describe('Image Component Tests', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + let dpl: string + beforeAll(() => { + dpl = next.getDeploymentIdQuery(true) + }) + + async function getImageUrls(browser) { + return await Promise.all( + (await browser.elementsByCss('img')).map(async (link) => + new URL(await link.getAttribute('src'), next.url).toString() + ) + ) + } + + async function getComputed(browser, id, prop) { + const val = await browser.eval(`document.getElementById('${id}').${prop}`) + if (typeof val === 'number') { + return val + } + if (typeof val === 'string') { + const v = parseInt(val, 10) + if (isNaN(v)) { + return val + } + return v + } + return null + } + + async function getComputedStyle(browser, id, prop) { + return browser.eval( + `window.getComputedStyle(document.getElementById('${id}')).getPropertyValue('${prop}')` + ) + } + + async function getSrc(browser, id) { + const src = await browser.elementById(id).getAttribute('src') + if (src) { + const url = new URL(src, next.url) + return url.href.slice(url.origin.length) + } + } + + function getRatio(width, height) { + return height / width + } + + it('should load the images', async () => { + const browser = await next.browser('/') + + await retry(async () => { + const result = await browser.eval( + `document.getElementById('basic-image').naturalWidth` + ) + expect(result).toBeGreaterThan(0) + }) + + expect(await getImageUrls(browser)).toContain( + `${next.url}/_next/image?url=%2Ftest.jpg&w=828&q=75${dpl}` + ) + }) + + it('should preload priority images', async () => { + const browser = await next.browser('/priority') + + await retry(async () => { + const result = await browser.eval( + `document.getElementById('basic-image').naturalWidth` + ) + expect(result).toBeGreaterThan(0) + }) + + const links = await browser.elementsByCss('link[rel=preload][as=image]') + const entries = [] + for (const link of links) { + const imagesrcset = await link.getAttribute('imagesrcset') + const imagesizes = await link.getAttribute('imagesizes') + entries.push({ imagesrcset, imagesizes }) + } + + expect(entries).toEqual([ + { + imagesizes: '', + imagesrcset: `/_next/image?url=%2Ftest.jpg&w=640&q=75${dpl} 1x, /_next/image?url=%2Ftest.jpg&w=828&q=75${dpl} 2x`, + }, + { + imagesizes: '', + imagesrcset: `/_next/image?url=%2Ftest.gif&w=640&q=75${dpl} 1x, /_next/image?url=%2Ftest.gif&w=828&q=75${dpl} 2x`, + }, + { + imagesizes: '', + imagesrcset: `/_next/image?url=%2Ftest.png&w=640&q=75${dpl} 1x, /_next/image?url=%2Ftest.png&w=828&q=75${dpl} 2x`, + }, + { + imagesizes: '100vw', + imagesrcset: `/_next/image?url=%2Fwide.png&w=640&q=75${dpl} 640w, /_next/image?url=%2Fwide.png&w=750&q=75${dpl} 750w, /_next/image?url=%2Fwide.png&w=828&q=75${dpl} 828w, /_next/image?url=%2Fwide.png&w=1080&q=75${dpl} 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75${dpl} 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75${dpl} 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75${dpl} 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75${dpl} 3840w`, + }, + { + imagesizes: '', + imagesrcset: `/_next/image?url=%2Ftest.tiff&w=640&q=75${dpl} 1x, /_next/image?url=%2Ftest.tiff&w=828&q=75${dpl} 2x`, + }, + ]) + + expect( + await browser.elementById('basic-image').getAttribute('loading') + ).toBe(null) + expect( + await browser + .elementById('basic-image-with-crossorigin') + .getAttribute('loading') + ).toBe(null) + expect( + await browser + .elementById('basic-image-with-referrerpolicy') + .getAttribute('loading') + ).toBe(null) + expect( + await browser.elementById('load-eager').getAttribute('loading') + ).toBe(null) + expect( + await browser.elementById('responsive1').getAttribute('loading') + ).toBe(null) + expect( + await browser.elementById('responsive2').getAttribute('loading') + ).toBe(null) + expect( + await browser.elementById('belowthefold').getAttribute('loading') + ).toBe(null) + + const warnings = (await browser.log()).map((log) => log.message).join('\n') + expect(warnings).not.toMatch( + /was detected as the Largest Contentful Paint/gm + ) + + expect( + await browser.elementsByCss( + 'link[rel=preload][as=image][crossorigin=use-credentials][imagesrcset*="test.gif"]' + ) + ).toHaveLength(1) + + expect( + await browser.elementsByCss( + 'link[rel=preload][as=image][referrerpolicy="no-referrer"][imagesrcset*="test.png"]' + ) + ).toHaveLength(1) + }) + + it('should not pass through user-provided srcset (causing a flash)', async () => { + const html = await next.render('/drop-srcset') + const $html = cheerio.load(html) + + const els = [].slice.apply($html('img')) + expect(els.length).toBe(2) + + const [el, noscriptEl] = els + expect(noscriptEl.attribs.src).toBeDefined() + expect(noscriptEl.attribs.srcset).toBeDefined() + + expect(el.attribs.src).toBeDefined() + expect(el.attribs.srcset).toBeUndefined() + expect(el.attribs.srcSet).toBeUndefined() + }) + + it('should update the image on src change', async () => { + const browser = await next.browser('/update') + + await retry(async () => { + const src = await browser.eval( + `document.getElementById("update-image").src` + ) + expect(src).toMatch(/test\.jpg/) + }) + + await browser.eval(`document.getElementById("toggle").click()`) + + await retry(async () => { + const src = await browser.eval( + `document.getElementById("update-image").src` + ) + expect(src).toMatch(/test\.png/) + }) + }) + + it('should callback onLoadingComplete when image is fully loaded', async () => { + const browser = await next.browser('/on-loading-complete') + + await browser.eval( + `document.getElementById("footer").scrollIntoView({behavior: "smooth"})` + ) + + await retry(async () => { + const src = await browser.eval( + `document.getElementById("img1").currentSrc` + ) + expect(src).toMatch(/test(.*)jpg/) + }) + await retry(async () => { + const src = await browser.eval( + `document.getElementById("img2").currentSrc` + ) + expect(src).toMatch(/test(.*).png/) + }) + await retry(async () => { + const src = await browser.eval( + `document.getElementById("img3").currentSrc` + ) + expect(src).toMatch(/test\.svg/) + }) + await retry(async () => { + const src = await browser.eval( + `document.getElementById("img4").currentSrc` + ) + expect(src).toMatch(/test(.*)ico/) + }) + await retry(async () => { + const text = await browser.eval( + `document.getElementById("msg1").textContent` + ) + expect(text).toBe('loaded 1 img1 with dimensions 128x128') + }) + await retry(async () => { + const text = await browser.eval( + `document.getElementById("msg2").textContent` + ) + expect(text).toBe('loaded 1 img2 with dimensions 400x400') + }) + await retry(async () => { + const text = await browser.eval( + `document.getElementById("msg3").textContent` + ) + expect(text).toBe('loaded 1 img3 with dimensions 266x266') + }) + await retry(async () => { + const text = await browser.eval( + `document.getElementById("msg4").textContent` + ) + expect(text).toBe('loaded 1 img4 with dimensions 21x21') + }) + await retry(async () => { + const text = await browser.eval( + `document.getElementById("msg5").textContent` + ) + expect(text).toBe('loaded 1 img5 with dimensions 3x5') + }) + await retry(async () => { + const text = await browser.eval( + `document.getElementById("msg6").textContent` + ) + expect(text).toBe('loaded 1 img6 with dimensions 3x5') + }) + await retry(async () => { + const text = await browser.eval( + `document.getElementById("msg7").textContent` + ) + expect(text).toBe('loaded 1 img7 with dimensions 400x400') + }) + await retry(async () => { + const text = await browser.eval( + `document.getElementById("msg8").textContent` + ) + expect(text).toBe('loaded 1 img8 with dimensions 640x373') + }) + await retry(async () => { + const attr = await browser.eval( + `document.getElementById("img8").getAttribute("data-nimg")` + ) + expect(attr).toBe('intrinsic') + }) + await retry(async () => { + const src = await browser.eval( + `document.getElementById("img8").currentSrc` + ) + expect(src).toMatch(/wide.png/) + }) + await browser.eval('document.getElementById("toggle").click()') + await retry(async () => { + const text = await browser.eval( + `document.getElementById("msg8").textContent` + ) + expect(text).toBe('loaded 2 img8 with dimensions 400x300') + }) + await retry(async () => { + const attr = await browser.eval( + `document.getElementById("img8").getAttribute("data-nimg")` + ) + expect(attr).toBe('fixed') + }) + await retry(async () => { + const src = await browser.eval( + `document.getElementById("img8").currentSrc` + ) + expect(src).toMatch(/test-rect.jpg/) + }) + }) + + it('should callback native onLoad in most cases', async () => { + const browser = await next.browser('/on-load') + + await browser.eval( + `document.getElementById("footer").scrollIntoView({behavior: "smooth"})` + ) + + await retry(async () => { + const src = await browser.eval( + `document.getElementById("img1").currentSrc` + ) + expect(src).toMatch(/test(.*)jpg/) + }) + await retry(async () => { + const src = await browser.eval( + `document.getElementById("img2").currentSrc` + ) + expect(src).toMatch(/test(.*).png/) + }) + await retry(async () => { + const src = await browser.eval( + `document.getElementById("img3").currentSrc` + ) + expect(src).toMatch(/test\.svg/) + }) + await retry(async () => { + const src = await browser.eval( + `document.getElementById("img4").currentSrc` + ) + expect(src).toMatch(/test(.*)ico/) + }) + await retry(async () => { + const text = await browser.eval( + `document.getElementById("msg1").textContent` + ) + expect(text).toBe('loaded 1 img1 with native onLoad') + }) + await retry(async () => { + const text = await browser.eval( + `document.getElementById("msg2").textContent` + ) + expect(text).toBe('loaded 1 img2 with native onLoad') + }) + await retry(async () => { + const text = await browser.eval( + `document.getElementById("msg3").textContent` + ) + expect(text).toBe('loaded 1 img3 with native onLoad') + }) + await retry(async () => { + const text = await browser.eval( + `document.getElementById("msg4").textContent` + ) + expect(text).toBe('loaded 1 img4 with native onLoad') + }) + await retry(async () => { + const text = await browser.eval( + `document.getElementById("msg8").textContent` + ) + expect(text).toBe('loaded 1 img8 with native onLoad') + }) + await retry(async () => { + const attr = await browser.eval( + `document.getElementById("img8").getAttribute("data-nimg")` + ) + expect(attr).toBe('intrinsic') + }) + await retry(async () => { + const src = await browser.eval( + `document.getElementById("img8").currentSrc` + ) + expect(src).toMatch(/wide.png/) + }) + await browser.eval('document.getElementById("toggle").click()') + await retry(async () => { + const text = await browser.eval( + `document.getElementById("msg8").textContent` + ) + expect(text).toBe('loaded 3 img8 with native onLoad') + }) + await retry(async () => { + const attr = await browser.eval( + `document.getElementById("img8").getAttribute("data-nimg")` + ) + expect(attr).toBe('fixed') + }) + await retry(async () => { + const src = await browser.eval( + `document.getElementById("img8").currentSrc` + ) + expect(src).toMatch(/test-rect.jpg/) + }) + }) + + it('should callback native onError when error occurred while loading image', async () => { + const browser = await next.browser('/on-error') + + await retry(async () => { + const src = await browser.eval( + `document.getElementById("img1").currentSrc` + ) + expect(src).toMatch(/test\.png/) + }) + await retry(async () => { + const src = await browser.eval( + `document.getElementById("img2").currentSrc` + ) + expect(src).toMatch(/nonexistent-img\.png/) + }) + await retry(async () => { + const text = await browser.eval( + `document.getElementById("msg1").textContent` + ) + expect(text).toBe('no error occurred') + }) + await retry(async () => { + const text = await browser.eval( + `document.getElementById("msg2").textContent` + ) + expect(text).toBe('error occurred while loading img2') + }) + }) + + it('should work with image with blob src', async () => { + const browser = await next.browser('/blob') + + await retry(async () => { + const src = await browser.eval( + `document.getElementById("blob-image").src` + ) + expect(src).toMatch(/^blob:/) + }) + await retry(async () => { + const srcset = await browser.eval( + `document.getElementById("blob-image").srcset` + ) + expect(srcset).toBe('') + }) + }) + + it('should work when using flexbox', async () => { + const browser = await next.browser('/flex') + await retry(async () => { + const result = await browser.eval( + `document.getElementById('basic-image').width` + ) + expect(result).toBeGreaterThan(0) + }) + }) + + it('should work with layout-fixed so resizing window does not resize image', async () => { + const browser = await next.browser('/layout-fixed') + const width = 1200 + const height = 700 + const delta = 250 + const id = 'fixed1' + + await retry(async () => { + expect(await getSrc(browser, id)).toBe( + `/_next/image?url=%2Fwide.png&w=3840&q=75${dpl}` + ) + }) + expect(await browser.elementById(id).getAttribute('srcset')).toBe( + `/_next/image?url=%2Fwide.png&w=1200&q=75${dpl} 1x, /_next/image?url=%2Fwide.png&w=3840&q=75${dpl} 2x` + ) + expect(await browser.elementById(id).getAttribute('sizes')).toBeFalsy() + await browser.setDimensions({ + width: width + delta, + height: height + delta, + }) + expect(await getComputed(browser, id, 'width')).toBe(width) + expect(await getComputed(browser, id, 'height')).toBe(height) + await browser.setDimensions({ + width: width - delta, + height: height - delta, + }) + expect(await getComputed(browser, id, 'width')).toBe(width) + expect(await getComputed(browser, id, 'height')).toBe(height) + }) + + it('should work with layout-intrinsic so resizing window maintains image aspect ratio', async () => { + const browser = await next.browser('/layout-intrinsic') + const width = 1200 + const height = 700 + const delta = 250 + const id = 'intrinsic1' + + await retry(async () => { + expect(await getSrc(browser, id)).toBe( + `/_next/image?url=%2Fwide.png&w=3840&q=75${dpl}` + ) + }) + expect(await browser.elementById(id).getAttribute('srcset')).toBe( + `/_next/image?url=%2Fwide.png&w=1200&q=75${dpl} 1x, /_next/image?url=%2Fwide.png&w=3840&q=75${dpl} 2x` + ) + expect(await browser.elementById(id).getAttribute('sizes')).toBeFalsy() + await browser.setDimensions({ + width: width + delta, + height: height + delta, + }) + expect(await getComputed(browser, id, 'width')).toBe(width) + expect(await getComputed(browser, id, 'height')).toBe(height) + await browser.setDimensions({ + width: width - delta, + height: height - delta, + }) + const newWidth = await getComputed(browser, id, 'width') + const newHeight = await getComputed(browser, id, 'height') + expect(newWidth).toBeLessThan(width) + expect(newHeight).toBeLessThan(height) + expect(getRatio(newWidth, newHeight)).toBeCloseTo( + getRatio(width, height), + 1 + ) + }) + + it('should work with layout-responsive so resizing window maintains image aspect ratio', async () => { + const browser = await next.browser('/layout-responsive') + const width = 1200 + const height = 700 + const delta = 250 + const id = 'responsive1' + + await retry(async () => { + expect(await getSrc(browser, id)).toBe( + `/_next/image?url=%2Fwide.png&w=3840&q=75${dpl}` + ) + }) + expect(await browser.elementById(id).getAttribute('srcset')).toBe( + `/_next/image?url=%2Fwide.png&w=640&q=75${dpl} 640w, /_next/image?url=%2Fwide.png&w=750&q=75${dpl} 750w, /_next/image?url=%2Fwide.png&w=828&q=75${dpl} 828w, /_next/image?url=%2Fwide.png&w=1080&q=75${dpl} 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75${dpl} 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75${dpl} 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75${dpl} 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75${dpl} 3840w` + ) + expect(await browser.elementById(id).getAttribute('sizes')).toBe('100vw') + await browser.setDimensions({ + width: width + delta, + height: height + delta, + }) + expect(await getComputed(browser, id, 'width')).toBeGreaterThan(width) + expect(await getComputed(browser, id, 'height')).toBeGreaterThan(height) + await browser.setDimensions({ + width: width - delta, + height: height - delta, + }) + const newWidth = await getComputed(browser, id, 'width') + const newHeight = await getComputed(browser, id, 'height') + expect(newWidth).toBeLessThan(width) + expect(newHeight).toBeLessThan(height) + expect(getRatio(newWidth, newHeight)).toBeCloseTo( + getRatio(width, height), + 1 + ) + }) + + it('should work with layout-fill to fill the parent but NOT stretch with viewport', async () => { + const browser = await next.browser('/layout-fill') + const width = 600 + const height = 350 + const delta = 150 + const id = 'fill1' + + await retry(async () => { + expect(await getSrc(browser, id)).toBe( + `/_next/image?url=%2Fwide.png&w=3840&q=75${dpl}` + ) + }) + expect(await browser.elementById(id).getAttribute('srcset')).toBe( + `/_next/image?url=%2Fwide.png&w=640&q=75${dpl} 640w, /_next/image?url=%2Fwide.png&w=750&q=75${dpl} 750w, /_next/image?url=%2Fwide.png&w=828&q=75${dpl} 828w, /_next/image?url=%2Fwide.png&w=1080&q=75${dpl} 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75${dpl} 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75${dpl} 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75${dpl} 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75${dpl} 3840w` + ) + expect(await browser.elementById(id).getAttribute('sizes')).toBe('100vw') + await browser.setDimensions({ + width: width + delta, + height: height + delta, + }) + expect(await getComputed(browser, id, 'width')).toBe(width) + expect(await getComputed(browser, id, 'height')).toBe(height) + await browser.setDimensions({ + width: width - delta, + height: height - delta, + }) + const newWidth = await getComputed(browser, id, 'width') + const newHeight = await getComputed(browser, id, 'height') + expect(newWidth).toBe(width) + expect(newHeight).toBe(height) + expect(getRatio(newWidth, newHeight)).toBeCloseTo( + getRatio(width, height), + 1 + ) + }) + + it('should work with layout-fill to fill the parent and stretch with viewport', async () => { + const browser = await next.browser('/layout-fill') + const id = 'fill2' + const width = await getComputed(browser, id, 'width') + const height = await getComputed(browser, id, 'height') + await browser.eval(`document.getElementById("${id}").scrollIntoView()`) + + await retry(async () => { + expect(await getSrc(browser, id)).toBe( + `/_next/image?url=%2Fwide.png&w=3840&q=75${dpl}` + ) + }) + + await retry(async () => { + const srcset = await browser.eval( + `document.querySelector('#${id}').getAttribute('srcset')` + ) + expect(srcset).toBe( + `/_next/image?url=%2Fwide.png&w=640&q=75${dpl} 640w, /_next/image?url=%2Fwide.png&w=750&q=75${dpl} 750w, /_next/image?url=%2Fwide.png&w=828&q=75${dpl} 828w, /_next/image?url=%2Fwide.png&w=1080&q=75${dpl} 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75${dpl} 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75${dpl} 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75${dpl} 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75${dpl} 3840w` + ) + }) + + expect(await browser.elementById(id).getAttribute('sizes')).toBe('100vw') + expect(await getComputed(browser, id, 'width')).toBe(width) + expect(await getComputed(browser, id, 'height')).toBe(height) + const delta = 150 + const largeWidth = Number(width) + delta + const largeHeight = Number(height) + delta + await browser.setDimensions({ + width: largeWidth, + height: largeHeight, + }) + expect(await getComputed(browser, id, 'width')).toBe(largeWidth) + expect(await getComputed(browser, id, 'height')).toBe(largeHeight) + const smallWidth = Number(width) - delta + const smallHeight = Number(height) - delta + await browser.setDimensions({ + width: smallWidth, + height: smallHeight, + }) + expect(await getComputed(browser, id, 'width')).toBe(smallWidth) + expect(await getComputed(browser, id, 'height')).toBe(smallHeight) + + const objectFit = await browser.eval( + `document.getElementById("${id}").style.objectFit` + ) + const objectPosition = await browser.eval( + `document.getElementById("${id}").style.objectPosition` + ) + expect(objectFit).toBe('cover') + expect(objectPosition).toBe('left center') + await browser.eval(`document.getElementById("fill3").scrollIntoView()`) + await retry(async () => { + const srcset = await browser.eval( + `document.querySelector('#fill3').getAttribute('srcset')` + ) + expect(srcset).toBe( + `/_next/image?url=%2Fwide.png&w=256&q=75${dpl} 256w, /_next/image?url=%2Fwide.png&w=384&q=75${dpl} 384w, /_next/image?url=%2Fwide.png&w=640&q=75${dpl} 640w, /_next/image?url=%2Fwide.png&w=750&q=75${dpl} 750w, /_next/image?url=%2Fwide.png&w=828&q=75${dpl} 828w, /_next/image?url=%2Fwide.png&w=1080&q=75${dpl} 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75${dpl} 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75${dpl} 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75${dpl} 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75${dpl} 3840w` + ) + }) + + await browser.eval(`document.getElementById("fill4").scrollIntoView()`) + await retry(async () => { + const srcset = await browser.eval( + `document.querySelector('#fill4').getAttribute('srcset')` + ) + expect(srcset).toBe( + `/_next/image?url=%2Fwide.png&w=32&q=75${dpl} 32w, /_next/image?url=%2Fwide.png&w=48&q=75${dpl} 48w, /_next/image?url=%2Fwide.png&w=64&q=75${dpl} 64w, /_next/image?url=%2Fwide.png&w=96&q=75${dpl} 96w, /_next/image?url=%2Fwide.png&w=128&q=75${dpl} 128w, /_next/image?url=%2Fwide.png&w=256&q=75${dpl} 256w, /_next/image?url=%2Fwide.png&w=384&q=75${dpl} 384w, /_next/image?url=%2Fwide.png&w=640&q=75${dpl} 640w, /_next/image?url=%2Fwide.png&w=750&q=75${dpl} 750w, /_next/image?url=%2Fwide.png&w=828&q=75${dpl} 828w, /_next/image?url=%2Fwide.png&w=1080&q=75${dpl} 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75${dpl} 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75${dpl} 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75${dpl} 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75${dpl} 3840w` + ) + }) + }) + + it('should work with sizes and automatically use layout-responsive', async () => { + const browser = await next.browser('/sizes') + const width = 1200 + const height = 700 + const delta = 250 + const id = 'sizes1' + + await retry(async () => { + expect(await getSrc(browser, id)).toBe( + `/_next/image?url=%2Fwide.png&w=3840&q=75${dpl}` + ) + }) + expect(await browser.elementById(id).getAttribute('srcset')).toBe( + `/_next/image?url=%2Fwide.png&w=32&q=75${dpl} 32w, /_next/image?url=%2Fwide.png&w=48&q=75${dpl} 48w, /_next/image?url=%2Fwide.png&w=64&q=75${dpl} 64w, /_next/image?url=%2Fwide.png&w=96&q=75${dpl} 96w, /_next/image?url=%2Fwide.png&w=128&q=75${dpl} 128w, /_next/image?url=%2Fwide.png&w=256&q=75${dpl} 256w, /_next/image?url=%2Fwide.png&w=384&q=75${dpl} 384w, /_next/image?url=%2Fwide.png&w=640&q=75${dpl} 640w, /_next/image?url=%2Fwide.png&w=750&q=75${dpl} 750w, /_next/image?url=%2Fwide.png&w=828&q=75${dpl} 828w, /_next/image?url=%2Fwide.png&w=1080&q=75${dpl} 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75${dpl} 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75${dpl} 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75${dpl} 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75${dpl} 3840w` + ) + expect(await browser.elementById(id).getAttribute('sizes')).toBe( + '(max-width: 2048px) 1200px, 3840px' + ) + await browser.setDimensions({ + width: width + delta, + height: height + delta, + }) + expect(await getComputed(browser, id, 'width')).toBeGreaterThan(width) + expect(await getComputed(browser, id, 'height')).toBeGreaterThan(height) + await browser.setDimensions({ + width: width - delta, + height: height - delta, + }) + const newWidth = await getComputed(browser, id, 'width') + const newHeight = await getComputed(browser, id, 'height') + expect(newWidth).toBeLessThan(width) + expect(newHeight).toBeLessThan(height) + expect(getRatio(newWidth, newHeight)).toBeCloseTo( + getRatio(width, height), + 1 + ) + }) + + it('should handle the styles prop appropriately', async () => { + const browser = await next.browser('/style-prop') + + expect(await browser.elementById('with-styles').getAttribute('style')).toBe( + 'border-radius:10px;padding:0;position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%' + ) + expect( + await browser + .elementById('with-overlapping-styles-intrinsic') + .getAttribute('style') + ).toBe( + 'width:0;border-radius:10px;margin:auto;position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;display:block;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%' + ) + + expect( + await browser + .elementById('without-styles-responsive') + .getAttribute('style') + ).toBe( + 'position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%' + ) + + if (isNextDev) { + await new Promise((resolve) => setTimeout(resolve, 1000)) + const warnings = (await browser.log()) + .map((log) => log.message) + .join('\n') + expect(warnings).toMatch( + /Image with src \/test.png is assigned the following styles, which are overwritten by automatically-generated styles: padding/gm + ) + expect(warnings).toMatch( + /Image with src \/test.jpg is assigned the following styles, which are overwritten by automatically-generated styles: width, margin/gm + ) + expect(warnings).not.toMatch( + /Image with src \/test.webp is assigned the following styles/gm + ) + } + }) + + if (isNextDev) { + it('should show missing src error', async () => { + const browser = await next.browser('/missing-src') + + await waitForNoRedbox(browser) + + await retry(async () => { + const logs = (await browser.log()).map((log) => log.message).join('\n') + expect(logs).toMatch(/Image is missing required "src" property/gm) + }) + }) + + it('should show invalid src error', async () => { + const browser = await next.browser('/invalid-src') + + await waitForRedbox(browser) + expect(await getRedboxHeader(browser)).toContain( + 'Invalid src prop (https://google.com/test.png) on `next/image`, hostname "google.com" is not configured under images in your `next.config.js`' + ) + }) + + it('should show invalid src error when protocol-relative', async () => { + const browser = await next.browser('/invalid-src-proto-relative') + + await waitForRedbox(browser) + expect(await getRedboxHeader(browser)).toContain( + 'Failed to parse src "//assets.example.com/img.jpg" on `next/image`, protocol-relative URL (//) must be changed to an absolute URL (http:// or https://)' + ) + }) + + it('should show error when string src and placeholder=blur and blurDataURL is missing', async () => { + const browser = await next.browser('/invalid-placeholder-blur') + + await waitForRedbox(browser) + expect(await getRedboxHeader(browser)).toContain( + `Image with src "/test.png" has "placeholder='blur'" property but is missing the "blurDataURL" property.` + ) + }) + + it('should show error when not numeric string width or height', async () => { + const browser = await next.browser('/invalid-width-or-height') + + await waitForRedbox(browser) + expect(await getRedboxHeader(browser)).toContain( + `Image with src "/test.jpg" has invalid "width" or "height" property. These should be numeric values.` + ) + }) + + it('should show error when static import and placeholder=blur and blurDataUrl is missing', async () => { + const browser = await next.browser('/invalid-placeholder-blur-static') + + await waitForRedbox(browser) + expect(await getRedboxHeader(browser)).toMatch( + /Image with src "(.*)bmp" has "placeholder='blur'" property but is missing the "blurDataURL" property/ + ) + }) + + it('should warn when img with layout=responsive is inside flex container', async () => { + const browser = await next.browser('/layout-responsive-inside-flex') + await browser.eval(`document.getElementById("img").scrollIntoView()`) + await retry(async () => { + const logs = (await browser.log()).map((log) => log.message).join('\n') + expect(logs).toMatch( + /Image with src (.*)jpg(.*) may not render properly as a child of a flex container. Consider wrapping the image with a div to configure the width/gm + ) + }) + await waitForNoRedbox(browser) + }) + + it('should warn when img with layout=fill is inside a container without position relative', async () => { + const browser = await next.browser('/layout-fill-inside-nonrelative') + await browser.eval(`document.querySelector("footer").scrollIntoView()`) + await new Promise((resolve) => setTimeout(resolve, 1000)) + const warnings = (await browser.log()) + .map((log) => log.message) + .join('\n') + expect(warnings).toMatch( + /Image with src (.*)jpg(.*) may not render properly with a parent using position:"static". Consider changing the parent style to position:"relative"/gm + ) + expect(warnings).not.toMatch( + /Image with src (.*)png(.*) may not render properly/gm + ) + expect(warnings).not.toMatch( + /Image with src (.*)avif(.*) may not render properly/gm + ) + expect(warnings).not.toMatch( + /Image with src (.*)webp(.*) may not render properly/gm + ) + await waitForNoRedbox(browser) + }) + + it('should warn when using a very small image with placeholder=blur', async () => { + const browser = await next.browser('/small-img-import') + + const warnings = (await browser.log()) + .map((log) => log.message) + .join('\n') + await waitForNoRedbox(browser) + expect(warnings).toMatch( + /Image with src (.*)jpg(.*) is smaller than 40x40. Consider removing(.*)/gm + ) + }) + + it('should warn when quality is 50', async () => { + const browser = await next.browser('/quality-50') + + const warnings = (await browser.log()) + .map((log) => log.message) + .join('\n') + await waitForNoRedbox(browser) + expect(warnings).toMatch( + /Image with src (.*)jpg(.*) is using quality "50" which is not configured in images.qualities \[75\]. Please update your config to \[50, 75\]./gm + ) + }) + + it('should not warn when Image is child of p', async () => { + const browser = await next.browser('/inside-paragraph') + + const warnings = (await browser.log()) + .map((log) => log.message) + .join('\n') + await waitForNoRedbox(browser) + expect(warnings).not.toMatch( + /Expected server HTML to contain a matching/gm + ) + expect(warnings).not.toMatch(/cannot appear as a descendant/gm) + }) + + it('should warn when priority prop is missing on LCP image', async () => { + const browser = await next.browser('/priority-missing-warning') + await retry(async () => { + const result = await browser.eval( + `document.getElementById('responsive').naturalWidth` + ) + expect(result).toBeGreaterThan(0) + }) + await new Promise((resolve) => setTimeout(resolve, 1000)) + const warnings = (await browser.log()) + .map((log) => log.message) + .join('\n') + await waitForNoRedbox(browser) + expect(warnings).toMatch( + /Image with src (.*)test(.*) was detected as the Largest Contentful Paint/gm + ) + }) + + it('should warn when loader is missing width', async () => { + const browser = await next.browser('/invalid-loader') + await browser.eval(`document.querySelector("footer").scrollIntoView()`) + const warnings = (await browser.log()) + .map((log) => log.message) + .join('\n') + await waitForNoRedbox(browser) + expect(warnings).toMatch( + /Image with src (.*)png(.*) has a "loader" property that does not implement width/gm + ) + expect(warnings).not.toMatch( + /Image with src (.*)jpg(.*) has a "loader" property that does not implement width/gm + ) + expect(warnings).not.toMatch( + /Image with src (.*)webp(.*) has a "loader" property that does not implement width/gm + ) + expect(warnings).not.toMatch( + /Image with src (.*)gif(.*) has a "loader" property that does not implement width/gm + ) + expect(warnings).not.toMatch( + /Image with src (.*)tiff(.*) has a "loader" property that does not implement width/gm + ) + }) + + it('should warn when using sizes with incorrect layout', async () => { + const browser = await next.browser('/invalid-sizes') + await browser.eval(`document.querySelector("footer").scrollIntoView()`) + const warnings = (await browser.log()) + .map((log) => log.message) + .join('\n') + await waitForNoRedbox(browser) + expect(warnings).toMatch( + /Image with src (.*)png(.*) has "sizes" property but it will be ignored/gm + ) + expect(warnings).toMatch( + /Image with src (.*)jpg(.*) has "sizes" property but it will be ignored/gm + ) + expect(warnings).not.toMatch( + /Image with src (.*)webp(.*) has "sizes" property but it will be ignored/gm + ) + expect(warnings).not.toMatch( + /Image with src (.*)gif(.*) has "sizes" property but it will be ignored/gm + ) + }) + + it('should not warn when svg, even if with loader prop or without', async () => { + const browser = await next.browser('/loader-svg') + await browser.eval(`document.querySelector("footer").scrollIntoView()`) + const warnings = (await browser.log()) + .map((log) => log.message) + .join('\n') + await waitForNoRedbox(browser) + expect(warnings).not.toMatch( + /Image with src (.*) has a "loader" property that does not implement width/gm + ) + expect(await browser.elementById('with-loader').getAttribute('src')).toBe( + '/test.svg?size=256' + ) + expect( + await browser.elementById('with-loader').getAttribute('srcset') + ).toBe('/test.svg?size=128 1x, /test.svg?size=256 2x') + expect( + await browser.elementById('without-loader').getAttribute('src') + ).toBe('/test.svg') + expect( + await browser.elementById('without-loader').getAttribute('srcset') + ).toBe('/test.svg 1x, /test.svg 2x') + }) + + it('should warn at most once even after state change', async () => { + const browser = await next.browser('/warning-once') + await browser.eval(`document.querySelector("footer").scrollIntoView()`) + await browser.eval(`document.querySelector("button").click()`) + await browser.eval(`document.querySelector("button").click()`) + const count = await browser.eval( + `document.querySelector("button").textContent` + ) + expect(count).toBe('Count: 2') + await retry(async () => { + const result = await browser.eval( + 'document.getElementById("w").naturalWidth' + ) + expect(result).toBeGreaterThan(0) + }) + await new Promise((resolve) => setTimeout(resolve, 1000)) + const warnings = (await browser.log()) + .map((log) => log.message) + .filter((log) => log.startsWith('Image with src')) + + expect(warnings[0]).toMatch( + 'Image with src "/test.png" is using next/legacy/image which is deprecated and will be removed in a future version of Next.js.' + ) + expect(warnings[1]).toMatch( + 'Image with src "/test.png" has "sizes" property but it will be ignored.' + ) + expect(warnings[2]).toMatch( + 'Image with src "/test.png" was detected as the Largest Contentful Paint (LCP).' + ) + expect(warnings.length).toBe(3) + }) + } else { + it('should not create an image folder in server/chunks', async () => { + expect(await next.hasFile('.next/server/chunks/static/media')).toBeFalsy() + }) + } + + it('should correctly ignore prose styles', async () => { + const browser = await next.browser('/prose') + + const id = 'prose-image' + + await retry(async () => { + const result = await browser.eval( + `document.getElementById(${JSON.stringify(id)}).naturalWidth` + ) + expect(result).toBeGreaterThan(0) + }) + + await new Promise((resolve) => setTimeout(resolve, 1000)) + + const computedWidth = await getComputed(browser, id, 'width') + const computedHeight = await getComputed(browser, id, 'height') + expect(getRatio(computedWidth, computedHeight)).toBeCloseTo(1, 1) + }) + + it('should apply style inheritance for img elements but not wrapper elements', async () => { + const browser = await next.browser('/style-inheritance') + + await browser.eval( + `document.querySelector("footer").scrollIntoView({behavior: "smooth"})` + ) + + const imagesWithIds = await browser.eval(` + function foo() { + const imgs = document.querySelectorAll("img[id]"); + for (let img of imgs) { + const br = window.getComputedStyle(img).getPropertyValue("border-radius"); + if (!br) return 'no-border-radius'; + if (br !== '139px') return br; + } + return true; + }() + `) + expect(imagesWithIds).toBe(true) + + const allSpans = await browser.eval(` + function foo() { + const spans = document.querySelectorAll("span"); + for (let span of spans) { + const m = window.getComputedStyle(span).getPropertyValue("margin"); + if (m && m !== '0px') return m; + } + return false; + }() + `) + expect(allSpans).toBe(false) + }) + + it('should apply filter style after image loads', async () => { + const browser = await next.browser('/style-filter') + await retry(async () => { + const src = await getSrc(browser, 'img-plain') + expect(src).toMatch(/^\/_next\/image/) + }) + await retry(async () => { + const src = await getSrc(browser, 'img-blur') + expect(src).toMatch(/^\/_next\/image/) + }) + await new Promise((resolve) => setTimeout(resolve, 1000)) + + expect(await getComputedStyle(browser, 'img-plain', 'filter')).toBe( + 'opacity(0.5)' + ) + expect( + await getComputedStyle(browser, 'img-plain', 'background-size') + ).toBe('30%') + expect( + await getComputedStyle(browser, 'img-plain', 'background-image') + ).toMatch('iVBORw0KGgo=') + expect( + await getComputedStyle(browser, 'img-plain', 'background-position') + ).toBe('1px 2px') + + expect(await getComputedStyle(browser, 'img-blur', 'filter')).toBe( + 'opacity(0.5)' + ) + expect(await getComputedStyle(browser, 'img-blur', 'background-size')).toBe( + '30%' + ) + expect( + await getComputedStyle(browser, 'img-blur', 'background-image') + ).toMatch('iVBORw0KGgo=') + expect( + await getComputedStyle(browser, 'img-blur', 'background-position') + ).toBe('1px 2px') + }) + + it('should emit image for next/dynamic with non ssr case', async () => { + const browser = await next.browser('/dynamic-static-img') + const img = await browser.elementById('dynamic-loaded-static-jpg') + const src = await img.getAttribute('src') + const res = await next.fetch(src) + expect(res.status).toBe(200) + }) + + if (!isNextDev) { + it('should correctly rotate image', async () => { + const browser = await next.browser('/rotated') + + const id = 'exif-rotation-image' + + await retry(async () => { + const result = await browser.eval( + `document.getElementById(${JSON.stringify(id)}).naturalWidth` + ) + expect(result).toBeGreaterThan(0) + }) + + await new Promise((resolve) => setTimeout(resolve, 1000)) + + const computedWidth = await getComputed(browser, id, 'width') + const computedHeight = await getComputed(browser, id, 'height') + expect(getRatio(computedWidth, computedHeight)).toBeCloseTo(0.5625, 1) + }) + } + + it('should have blurry placeholder when enabled', async () => { + const html = await next.render('/blurry-placeholder') + const $html = cheerio.load(html) + + $html('noscript > img').attr('id', 'unused') + + expect($html('#blurry-placeholder')[0].attribs.style).toContain( + `background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='400' viewBox='0 0 400 400'%3E%3Cfilter id='blur' filterUnits='userSpaceOnUse' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20' edgeMode='duplicate' /%3E%3CfeComponentTransfer%3E%3CfeFuncA type='discrete' tableValues='1 1' /%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Cimage filter='url(%23blur)' href='data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMDAwMDAwQEBAQFBQUFBQcHBgYHBwsICQgJCAsRCwwLCwwLEQ8SDw4PEg8bFRMTFRsfGhkaHyYiIiYwLTA+PlT/wAALCAAKAAoBAREA/8QAMwABAQEAAAAAAAAAAAAAAAAAAAcJEAABAwUAAwAAAAAAAAAAAAAFAAYRAQMEEyEVMlH/2gAIAQEAAD8Az1bLPaxhiuk0QdeCOLDtHixN2dmd2bsc5FPX7VTREX//2Q==' x='0' y='0' height='100%25' width='100%25'/%3E%3C/svg%3E")` + ) + + expect($html('#blurry-placeholder')[0].attribs.style).toContain( + `background-position:0% 0%` + ) + + expect( + $html('#blurry-placeholder-tall-centered')[0].attribs.style + ).toContain(`background-position:center`) + + expect($html('#blurry-placeholder-with-lazy')[0].attribs.style).toContain( + `background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='400' viewBox='0 0 400 400'%3E%3Cfilter id='blur' filterUnits='userSpaceOnUse' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20' edgeMode='duplicate' /%3E%3CfeComponentTransfer%3E%3CfeFuncA type='discrete' tableValues='1 1' /%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Cimage filter='url(%23blur)' href='data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMDAwMDAwQEBAQFBQUFBQcHBgYHBwsICQgJCAsRCwwLCwwLEQ8SDw4PEg8bFRMTFRsfGhkaHyYiIiYwLTA+PlT/wAALCAAKAAoBAREA/8QAMwABAQEAAAAAAAAAAAAAAAAAAAcJEAABAwUAAwAAAAAAAAAAAAAFAAYRAQMEEyEVMlH/2gAIAQEAAD8Az1bLPaxhiuk0QdeCOLDtHixN2dmd2bsc5FPX7VTREX//2Q==' x='0' y='0' height='100%25' width='100%25'/%3E%3C/svg%3E")` + ) + }) + + it('should not use blurry placeholder for