Files
next.js/test/development/server-side-dev-errors/server-side-dev-errors.test.ts
T
Tim NeutkensandGitHub 8141dcf12e Convert test/integration to isolated tests (#93247)
### 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/<name>/test/index.test.{js,ts}` was rewritten into a
single `<name>.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: <port|url> }` 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.
2026-05-11 13:55:47 +02:00

455 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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:')
})
})