Files
next.js/test/development/acceptance-app/ReactRefreshLogBox.test.ts
Arian Tron 61f56f997c
Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled
first commit
2026-03-10 19:37:31 +03:30

1802 lines
48 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.
/* eslint-env jest */
import { createSandbox } from 'development-sandbox'
import { FileRef, nextTestSetup } from 'e2e-utils'
import { getToastErrorCount, retry } from 'next-test-utils'
import path from 'path'
import { outdent } from 'outdent'
describe('ReactRefreshLogBox app', () => {
const { next, isTurbopack, isRspack } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
patchFileDelay: 1000,
})
test('should strip whitespace correctly with newline', async () => {
await using sandbox = await createSandbox(next)
const { browser, session } = sandbox
await session.patch(
'index.js',
outdent`
export default function Page() {
return (
<>
<p>index page</p>
<a onClick={() => {
throw new Error('idk')
}}>
click me
</a>
</>
)
}
`
)
await browser.elementByCss('a').click()
if (isTurbopack) {
await expect(browser).toDisplayRedbox(
`"Expected Redbox but found no visible one."`
)
} else {
await expect(browser).toDisplayRedbox(
`"Expected Redbox but found no visible one."`
)
}
})
// https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554137807
test('module init error not shown', async () => {
// Start here:
await using sandbox = await createSandbox(next)
const { browser, session } = sandbox
// We start here.
await session.patch(
'index.js',
outdent`
import * as React from 'react';
class ClassDefault extends React.Component {
render() {
return <h1>Default Export</h1>;
}
}
export default ClassDefault;
`
)
expect(
await session.evaluate(() => document.querySelector('h1').textContent)
).toBe('Default Export')
// Add a throw in module init phase:
await session.patch(
'index.js',
outdent`
// top offset for snapshot
import * as React from 'react';
throw new Error('no')
class ClassDefault extends React.Component {
render() {
return <h1>Default Export</h1>;
}
}
export default ClassDefault;
`
)
if (isTurbopack) {
await expect(browser).toDisplayRedbox(`
{
"description": "no",
"environmentLabel": null,
"label": "Runtime Error",
"source": "index.js (3:7) @ module evaluation
> 3 | throw new Error('no')
| ^",
"stack": [
"module evaluation index.js (3:7)",
"module evaluation app/page.js (2:1)",
],
}
`)
} else if (isRspack) {
await expect(browser).toDisplayRedbox(`
{
"description": "no",
"environmentLabel": null,
"label": "Runtime Error",
"source": "index.js (3:7) @ eval
> 3 | throw new Error('no')
| ^",
"stack": [
"eval index.js (3:7)",
"<FIXME-next-dist-dir>",
"<FIXME-next-dist-dir>",
"<FIXME-next-dist-dir>",
"eval ./app/page.js",
"<FIXME-next-dist-dir>",
"<FIXME-next-dist-dir>",
"<FIXME-next-dist-dir>",
],
}
`)
} else {
await expect(browser).toDisplayRedbox(`
[
{
"description": "no",
"environmentLabel": null,
"label": "Runtime Error",
"source": "index.js (3:7) @ eval
> 3 | throw new Error('no')
| ^",
"stack": [
"eval index.js (3:7)",
"<FIXME-next-dist-dir>",
"eval ./app/page.js",
"<FIXME-next-dist-dir>",
],
},
{
"description": "no",
"environmentLabel": null,
"label": "Runtime Error",
"source": "index.js (3:7) @ eval
> 3 | throw new Error('no')
| ^",
"stack": [
"eval index.js (3:7)",
"<FIXME-next-dist-dir>",
"eval ./app/page.js",
"<FIXME-next-dist-dir>",
],
},
]
`)
}
})
// https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554152127
test('boundaries', async () => {
await using sandbox = await createSandbox(next)
const { browser, session } = sandbox
await session.write(
'FunctionDefault.js',
outdent`
export default function FunctionDefault() {
return <h2>hello</h2>
}
`
)
await session.patch(
'index.js',
outdent`
import FunctionDefault from './FunctionDefault.js'
import * as React from 'react'
class ErrorBoundary extends React.Component {
constructor() {
super()
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return {
hasError: true,
error
};
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
function App() {
return (
<ErrorBoundary fallback={<h2>error</h2>}>
<FunctionDefault />
</ErrorBoundary>
);
}
export default App;
`
)
expect(
await session.evaluate(() => document.querySelector('h2').textContent)
).toBe('hello')
await session.write(
'FunctionDefault.js',
`export default function FunctionDefault() { throw new Error('no'); }`
)
if (isTurbopack) {
await expect(browser).toDisplayRedbox(
`"Expected Redbox but found no visible one."`
)
} else {
await expect(browser).toDisplayRedbox(
`"Expected Redbox but found no visible one."`
)
}
})
// TODO: investigate why this fails when running outside of the Next.js
// monorepo e.g. fails when using pnpm create next-app
// https://github.com/vercel/next.js/pull/23203
test.skip('internal package errors', async () => {
await using sandbox = await createSandbox(next)
const { browser, session } = sandbox
// Make a react build-time error.
await session.patch(
'index.js',
outdent`
export default function FunctionNamed() {
return <div>{{}}</div>
}
`
)
await expect(browser).toDisplayRedbox()
})
test('unterminated JSX', async () => {
await using sandbox = await createSandbox(next)
const { browser, session } = sandbox
await session.patch(
'index.js',
outdent`
export default () => {
return (
<div>
<p>lol</p>
</div>
)
}
`
)
await session.waitForNoRedbox()
await session.patch(
'index.js',
outdent`
export default () => {
return (
<div>
<p>lol</p>
div
)
}
`
)
if (isTurbopack) {
await expect(browser).toDisplayRedbox(`
{
"description": "Unexpected token. Did you mean \`{'}'}\` or \`&rbrace;\`?",
"environmentLabel": null,
"label": "Build Error",
"source": "./index.js (7:1)
Unexpected token. Did you mean \`{'}'}\` or \`&rbrace;\`?
> 7 | }
| ^",
"stack": [],
}
`)
} else if (isRspack) {
await expect({ browser, next }).toDisplayRedbox(`
{
"description": " ╰─▶ × Error: x Unexpected token. Did you mean \`{'}'}\` or \`&rbrace;\`?",
"environmentLabel": null,
"label": "Build Error",
"source": "./index.js
╰─▶ × Error: x Unexpected token. Did you mean \`{'}'}\` or \`&rbrace;\`?
│ ,-[7:1]
│ 4 | <p>lol</p>
│ 5 | div
│ 6 | )
│ 7 | }
│ : ^
\`----
│ x Expected '</', got '<eof>'
│ ,-[7:1]
│ 4 | <p>lol</p>
│ 5 | div
│ 6 | )
│ 7 | }
\`----
│ Caused by:
│ Syntax Error
Import trace for requested module:
./index.js
./app/page.js",
"stack": [],
}
`)
} else {
await expect(browser).toDisplayRedbox(`
{
"description": " x Unexpected token. Did you mean \`{'}'}\` or \`&rbrace;\`?",
"environmentLabel": null,
"label": "Build Error",
"source": "./index.js
Error: x Unexpected token. Did you mean \`{'}'}\` or \`&rbrace;\`?
,-[7:1]
4 | <p>lol</p>
5 | div
6 | )
7 | }
: ^
\`----
x Expected '</', got '<eof>'
,-[7:1]
4 | <p>lol</p>
5 | div
6 | )
7 | }
\`----
Caused by:
Syntax Error
Import trace for requested module:
./index.js
./app/page.js",
"stack": [],
}
`)
}
})
// Module trace is only available with webpack 5
test('conversion to class component (1)', async () => {
await using sandbox = await createSandbox(next)
const { browser, session } = sandbox
await session.write(
'Child.js',
outdent`
export default function ClickCount() {
return <p>hello</p>
}
`
)
await session.patch(
'index.js',
outdent`
import Child from './Child';
export default function Home() {
return (
<div>
<Child />
</div>
)
}
`
)
await session.waitForNoRedbox()
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('hello')
await session.patch(
'Child.js',
outdent`
import { Component } from 'react';
export default class ClickCount extends Component {
render() {
throw new Error()
}
}
`
)
if (isTurbopack) {
// TODO(veil): Possibly https://linear.app/vercel/issue/NEXT-4411
await expect(browser).toDisplayRedbox(`
{
"description": "",
"environmentLabel": null,
"label": "Runtime Error",
"source": "Child.js (4:11) @ ClickCount.render
> 4 | throw new Error()
| ^",
"stack": [
"ClickCount.render Child.js (4:11)",
"Home index.js (6:7)",
"<FIXME-file-protocol>",
],
}
`)
} else {
await expect(browser).toDisplayRedbox(`
{
"description": "",
"environmentLabel": null,
"label": "Runtime Error",
"source": "Child.js (4:11) @ ClickCount.render
> 4 | throw new Error()
| ^",
"stack": [
"ClickCount.render Child.js (4:11)",
"Home index.js (6:7)",
"Page app/page.js (4:10)",
],
}
`)
}
await session.patch(
'Child.js',
outdent`
import { Component } from 'react';
export default class ClickCount extends Component {
render() {
return <p>hello new</p>
}
}
`
)
await session.waitForNoRedbox()
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('hello new')
})
test('css syntax errors', async () => {
await using sandbox = await createSandbox(next)
const { browser, session } = sandbox
await session.write('index.module.css', `.button {}`)
await session.patch(
'index.js',
outdent`
import './index.module.css';
export default () => {
return (
<div>
<p>lol</p>
</div>
)
}
`
)
await session.waitForNoRedbox()
await session.patch('index.module.css', `.button`)
if (isTurbopack) {
await expect(browser).toDisplayRedbox(`
{
"description": "Parsing CSS source code failed",
"environmentLabel": null,
"label": "Build Error",
"source": "./index.module.css (1:8)
Parsing CSS source code failed
> 1 | .button
| ^",
"stack": [],
}
`)
} else if (isRspack) {
await expect({ browser, next }).toDisplayRedbox(`
{
"description": " ╰─▶ × SyntaxError",
"environmentLabel": null,
"label": "Build Error",
"source": "./index.module.css
╰─▶ × SyntaxError
│ (1:1) <FIXME-project-root>/index.module.css Unknown word
│ > 1 | .button
│ | ^
Import trace for requested module:
./index.module.css
./index.js
./app/page.js",
"stack": [],
}
`)
} else {
await expect({ browser, next }).toDisplayRedbox(`
{
"description": "Syntax error: <FIXME-project-root>/index.module.css Unknown word",
"environmentLabel": null,
"label": "Build Error",
"source": "./index.module.css (1:1)
Syntax error: <FIXME-project-root>/index.module.css Unknown word
> 1 | .button
| ^",
"stack": [],
}
`)
}
// Checks for selectors that can't be prefixed.
// Selector "button" is not pure (pure selectors must contain at least one local class or id)
await session.patch('index.module.css', `button {}`)
if (isTurbopack) {
await expect(browser).toDisplayRedbox(`
{
"description": "Transforming CSS failed",
"environmentLabel": null,
"label": "Build Error",
"source": "./index.module.css
Transforming CSS failed
Selector "button" is not pure. Pure selectors must contain at least one local class or id.
Import traces:
Client Component Browser:
./index.module.css [Client Component Browser]
./index.js [Client Component Browser]
./app/page.js [Client Component Browser]
./app/page.js [Server Component]
Client Component SSR:
./index.module.css [Client Component SSR]
./index.js [Client Component SSR]
./app/page.js [Client Component SSR]
./app/page.js [Server Component]",
"stack": [],
}
`)
} else if (isRspack) {
await expect(browser).toDisplayRedbox(`
{
"description": " ╰─▶ × CssSyntaxError",
"environmentLabel": null,
"label": "Build Error",
"source": "./index.module.css
╰─▶ × CssSyntaxError
│ (1:1) Selector "button" is not pure (pure selectors must contain at least one local class or id)
│ > 1 | button {}
│ | ^
Import trace for requested module:
./index.module.css
./index.js
./app/page.js",
"stack": [],
}
`)
} else {
await expect(browser).toDisplayRedbox(`
{
"description": "Syntax error: Selector "button" is not pure (pure selectors must contain at least one local class or id)",
"environmentLabel": null,
"label": "Build Error",
"source": "./index.module.css (1:1)
Syntax error: Selector "button" is not pure (pure selectors must contain at least one local class or id)
> 1 | button {}
| ^",
"stack": [],
}
`)
}
})
it('logbox: anchors links in error messages', async () => {
await using sandbox = await createSandbox(next)
const { browser, session } = sandbox
await session.patch(
'index.js',
outdent`
import { useCallback } from 'react'
export default function Index() {
const boom = useCallback(() => {
throw new Error('end https://nextjs.org')
}, [])
return (
<main>
<button onClick={boom}>Boom!</button>
</main>
)
}
`
)
await browser.elementByCss('button').click()
// TODO(veil): Why Owner Stack location different?
if (isTurbopack) {
await expect(browser).toDisplayCollapsedRedbox(`
{
"description": "end https://nextjs.org",
"environmentLabel": null,
"label": "Runtime Error",
"source": "index.js (5:11) @ Index.useCallback[boom]
> 5 | throw new Error('end https://nextjs.org')
| ^",
"stack": [
"Index.useCallback[boom] index.js (5:11)",
"button <anonymous>",
"Index index.js (9:7)",
"Page index.js (9:30)",
],
}
`)
} else {
await expect(browser).toDisplayCollapsedRedbox(`
{
"description": "end https://nextjs.org",
"environmentLabel": null,
"label": "Runtime Error",
"source": "index.js (5:11) @ Index.useCallback[boom]
> 5 | throw new Error('end https://nextjs.org')
| ^",
"stack": [
"Index.useCallback[boom] index.js (5:11)",
"button <anonymous>",
"Index index.js (9:7)",
"Page app/page.js (4:10)",
],
}
`)
}
expect(
await session.evaluate(
() =>
document
.querySelector('body nextjs-portal')
.shadowRoot.querySelectorAll('#nextjs__container_errors_desc a')
.length
)
).toBe(1)
expect(
await session.evaluate(
() =>
(
document
.querySelector('body nextjs-portal')
.shadowRoot.querySelector(
'#nextjs__container_errors_desc a:nth-of-type(1)'
) as any
).href
)
).toMatchInlineSnapshot(`"https://nextjs.org/"`)
await session.patch(
'index.js',
outdent`
import { useCallback } from 'react'
export default function Index() {
const boom = useCallback(() => {
throw new Error('https://nextjs.org start')
}, [])
return (
<main>
<button onClick={boom}>Boom!</button>
</main>
)
}
`
)
await browser.elementByCss('button').click()
// TODO(veil): Why Owner Stack location different?
if (isTurbopack) {
await expect(browser).toDisplayRedbox(`
{
"description": "https://nextjs.org start",
"environmentLabel": null,
"label": "Runtime Error",
"source": "index.js (5:11) @ Index.useCallback[boom]
> 5 | throw new Error('https://nextjs.org start')
| ^",
"stack": [
"Index.useCallback[boom] index.js (5:11)",
"button <anonymous>",
"Index index.js (9:7)",
"Page index.js (9:30)",
],
}
`)
} else {
await expect(browser).toDisplayRedbox(`
{
"description": "https://nextjs.org start",
"environmentLabel": null,
"label": "Runtime Error",
"source": "index.js (5:11) @ Index.useCallback[boom]
> 5 | throw new Error('https://nextjs.org start')
| ^",
"stack": [
"Index.useCallback[boom] index.js (5:11)",
"button <anonymous>",
"Index index.js (9:7)",
"Page app/page.js (4:10)",
],
}
`)
}
expect(
await session.evaluate(
() =>
document
.querySelector('body nextjs-portal')
.shadowRoot.querySelectorAll('#nextjs__container_errors_desc a')
.length
)
).toBe(1)
expect(
await session.evaluate(
() =>
(
document
.querySelector('body nextjs-portal')
.shadowRoot.querySelector(
'#nextjs__container_errors_desc a:nth-of-type(1)'
) as any
).href
)
).toMatchInlineSnapshot(`"https://nextjs.org/"`)
await session.patch(
'index.js',
outdent`
import { useCallback } from 'react'
export default function Index() {
const boom = useCallback(() => {
throw new Error('middle https://nextjs.org end')
}, [])
return (
<main>
<button onClick={boom}>Boom!</button>
</main>
)
}
`
)
await browser.elementByCss('button').click()
// TODO(veil): Why Owner Stack location different?
if (isTurbopack) {
await expect(browser).toDisplayRedbox(`
{
"description": "middle https://nextjs.org end",
"environmentLabel": null,
"label": "Runtime Error",
"source": "index.js (5:11) @ Index.useCallback[boom]
> 5 | throw new Error('middle https://nextjs.org end')
| ^",
"stack": [
"Index.useCallback[boom] index.js (5:11)",
"button <anonymous>",
"Index index.js (9:7)",
"Page index.js (9:30)",
],
}
`)
} else {
await expect(browser).toDisplayRedbox(`
{
"description": "middle https://nextjs.org end",
"environmentLabel": null,
"label": "Runtime Error",
"source": "index.js (5:11) @ Index.useCallback[boom]
> 5 | throw new Error('middle https://nextjs.org end')
| ^",
"stack": [
"Index.useCallback[boom] index.js (5:11)",
"button <anonymous>",
"Index index.js (9:7)",
"Page app/page.js (4:10)",
],
}
`)
}
expect(
await session.evaluate(
() =>
document
.querySelector('body nextjs-portal')
.shadowRoot.querySelectorAll('#nextjs__container_errors_desc a')
.length
)
).toBe(1)
expect(
await session.evaluate(
() =>
(
document
.querySelector('body nextjs-portal')
.shadowRoot.querySelector(
'#nextjs__container_errors_desc a:nth-of-type(1)'
) as any
).href
)
).toMatchInlineSnapshot(`"https://nextjs.org/"`)
await session.patch(
'index.js',
outdent`
import { useCallback } from 'react'
export default function Index() {
const boom = useCallback(() => {
throw new Error('multiple https://nextjs.org links http://example.com')
}, [])
return (
<main>
<button onClick={boom}>Boom!</button>
</main>
)
}
`
)
await browser.elementByCss('button').click()
// TODO(veil): Why Owner Stack location different?
if (isTurbopack) {
await expect(browser).toDisplayRedbox(`
{
"description": "multiple https://nextjs.org links http://example.com",
"environmentLabel": null,
"label": "Runtime Error",
"source": "index.js (5:11) @ Index.useCallback[boom]
> 5 | throw new Error('multiple https://nextjs.org links http://example.com')
| ^",
"stack": [
"Index.useCallback[boom] index.js (5:11)",
"button <anonymous>",
"Index index.js (9:7)",
"Page index.js (9:30)",
],
}
`)
} else {
await expect(browser).toDisplayRedbox(`
{
"description": "multiple https://nextjs.org links http://example.com",
"environmentLabel": null,
"label": "Runtime Error",
"source": "index.js (5:11) @ Index.useCallback[boom]
> 5 | throw new Error('multiple https://nextjs.org links http://example.com')
| ^",
"stack": [
"Index.useCallback[boom] index.js (5:11)",
"button <anonymous>",
"Index index.js (9:7)",
"Page app/page.js (4:10)",
],
}
`)
}
expect(
await session.evaluate(
() =>
document
.querySelector('body nextjs-portal')
.shadowRoot.querySelectorAll('#nextjs__container_errors_desc a')
.length
)
).toBe(2)
expect(
await session.evaluate(
() =>
(
document
.querySelector('body nextjs-portal')
.shadowRoot.querySelector(
'#nextjs__container_errors_desc a:nth-of-type(1)'
) as any
).href
)
).toMatchInlineSnapshot(`"https://nextjs.org/"`)
expect(
await session.evaluate(
() =>
(
document
.querySelector('body nextjs-portal')
.shadowRoot.querySelector(
'#nextjs__container_errors_desc a:nth-of-type(2)'
) as any
).href
)
).toBe('http://example.com/')
})
// TODO-APP: Catch errors that happen before useEffect
test.skip('non-Error errors are handled properly', async () => {
await using sandbox = await createSandbox(next)
const { browser, session } = sandbox
await session.patch(
'index.js',
outdent`
export default () => {
throw {'a': 1, 'b': 'x'};
return (
<div>hello</div>
)
}
`
)
await expect(browser).toDisplayRedbox()
// fix previous error
await session.patch(
'index.js',
outdent`
export default () => {
return (
<div>hello</div>
)
}
`
)
await session.waitForNoRedbox()
await session.patch(
'index.js',
outdent`
class Hello {}
export default () => {
throw Hello
return (
<div>hello</div>
)
}
`
)
await expect(browser).toDisplayRedbox()
// fix previous error
await session.patch(
'index.js',
outdent`
export default () => {
return (
<div>hello</div>
)
}
`
)
await session.waitForNoRedbox()
await session.patch(
'index.js',
outdent`
export default () => {
throw "string error"
return (
<div>hello</div>
)
}
`
)
await expect(browser).toDisplayRedbox()
// fix previous error
await session.patch(
'index.js',
outdent`
export default () => {
return (
<div>hello</div>
)
}
`
)
await session.waitForNoRedbox()
await session.patch(
'index.js',
outdent`
export default () => {
throw null
return (
<div>hello</div>
)
}
`
)
await expect(browser).toDisplayRedbox()
})
test('Should not show __webpack_exports__ when exporting anonymous arrow function', async () => {
await using sandbox = await createSandbox(next)
const { browser, session } = sandbox
await session.patch(
'index.js',
outdent`
export default () => {
if (typeof window !== 'undefined') {
throw new Error('test')
}
return null
}
`
)
if (isTurbopack) {
await expect(browser).toDisplayRedbox(`
{
"description": "test",
"environmentLabel": null,
"label": "Runtime Error",
"source": "index.js (3:11) @ {default export}
> 3 | throw new Error('test')
| ^",
"stack": [
"{default export} index.js (3:11)",
"Page app/page.js (2:1)",
],
}
`)
} else if (isRspack) {
await expect(browser).toDisplayRedbox(`
{
"description": "test",
"environmentLabel": null,
"label": "Runtime Error",
"source": "index.js (3:11) @ __rspack_default_export
> 3 | throw new Error('test')
| ^",
"stack": [
"__rspack_default_export index.js (3:11)",
"Page app/page.js (4:10)",
],
}
`)
} else {
await expect(browser).toDisplayRedbox(`
{
"description": "test",
"environmentLabel": null,
"label": "Runtime Error",
"source": "index.js (3:11) @ default
> 3 | throw new Error('test')
| ^",
"stack": [
"default index.js (3:11)",
"Page app/page.js (4:10)",
],
}
`)
}
})
test('Unhandled errors and rejections opens up in the minimized state', async () => {
await using sandbox = await createSandbox(next)
const { session, browser } = sandbox
const file = outdent`
export default function Index() {
//
setTimeout(() => {
throw new Error('Unhandled error')
}, 0)
setTimeout(() => {
Promise.reject(new Error('Undhandled rejection'))
}, 0)
return (
<>
<button
id="unhandled-error"
onClick={() => {
throw new Error('Unhandled error')
}}
>
Unhandled error
</button>
<button
id="unhandled-rejection"
onClick={() => {
Promise.reject(new Error('Undhandled rejection'))
}}
>
Unhandled rejection
</button>
</>
)
}
`
await session.patch('index.js', file)
// Unhandled error and rejection in setTimeout
await retry(async () => {
expect(await getToastErrorCount(browser)).toBe(2)
})
// Unhandled error in event handler
await browser.elementById('unhandled-error').click()
await retry(async () => {
expect(await getToastErrorCount(browser)).toBe(3)
})
// Unhandled rejection in event handler
await browser.elementById('unhandled-rejection').click()
await retry(async () => {
expect(await getToastErrorCount(browser)).toBe(4)
})
await session.waitForNoRedbox()
// Add Component error
await session.patch(
'index.js',
file.replace(
'//',
"if (typeof window !== 'undefined') throw new Error('Component error')"
)
)
// Render error should "win" and show up in fullscreen
// TODO(veil): Why Owner Stack location different?
if (isTurbopack) {
await expect(browser).toDisplayRedbox(`
{
"description": "Component error",
"environmentLabel": null,
"label": "Runtime Error",
"source": "index.js (2:44) @ Index
> 2 | if (typeof window !== 'undefined') throw new Error('Component error')
| ^",
"stack": [
"Index index.js (2:44)",
"Page index.js (16:8)",
],
}
`)
} else {
await expect(browser).toDisplayRedbox(`
{
"description": "Component error",
"environmentLabel": null,
"label": "Runtime Error",
"source": "index.js (2:44) @ Index
> 2 | if (typeof window !== 'undefined') throw new Error('Component error')
| ^",
"stack": [
"Index index.js (2:44)",
"Page app/page.js (4:10)",
],
}
`)
}
})
test('Call stack for client error', async () => {
await using sandbox = await createSandbox(
next,
new Map([
[
'app/page.js',
outdent`
'use client'
export default function Page() {
if (typeof window !== 'undefined') {
throw new Error('Client error')
}
return null
}
`,
],
])
)
const { browser } = sandbox
await expect(browser).toDisplayRedbox(`
{
"description": "Client error",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/page.js (4:11) @ Page
> 4 | throw new Error('Client error')
| ^",
"stack": [
"Page app/page.js (4:11)",
],
}
`)
})
test('Call stack for server error', async () => {
await using sandbox = await createSandbox(
next,
new Map([
[
'app/page.js',
outdent`
export default function Page() {
throw new Error('Server error')
}
`,
],
])
)
const { browser } = sandbox
await expect(browser).toDisplayRedbox(`
{
"description": "Server error",
"environmentLabel": "Server",
"label": "Runtime Error",
"source": "app/page.js (2:9) @ Page
> 2 | throw new Error('Server error')
| ^",
"stack": [
"Page app/page.js (2:9)",
],
}
`)
})
test('should hide unrelated frames in stack trace with unknown anonymous calls', async () => {
await using sandbox = await createSandbox(
next,
new Map([
[
'app/page.js',
outdent`
export default function Page() {
try {
(function() {
throw new Error("This is an error from an anonymous function");
})();
} catch (e) {
throw e
}
}
`,
],
])
)
const { browser } = sandbox
// TODO(veil): Turbopack uses correct name
// TODO(veil): Column of 2nd frame should be 7
await expect(browser).toDisplayRedbox(`
{
"description": "This is an error from an anonymous function",
"environmentLabel": "Server",
"label": "Runtime Error",
"source": "app/page.js (4:13) @ ${isTurbopack ? '<anonymous>' : 'eval'}
> 4 | throw new Error("This is an error from an anonymous function");
| ^",
"stack": [
"${isTurbopack ? '<anonymous>' : 'eval'} app/page.js (4:13)",
"Page app/page.js (5:${isTurbopack ? '6' : '5'})",
],
}
`)
})
it('should hide unrelated frames in stack trace with nodejs internal calls', async () => {
await using sandbox = await createSandbox(
next,
new Map([
[
'app/page.js',
// Node.js will throw an error about the invalid URL since this is a server component
outdent`
export default function Page() {
new URL("/", "invalid");
}`,
],
])
)
const { browser } = sandbox
await expect(browser).toDisplayRedbox(`
{
"description": "Invalid URL",
"environmentLabel": "Server",
"label": "Runtime TypeError",
"source": "app/page.js (2:3) @ Page
> 2 | new URL("/", "invalid");
| ^",
"stack": [
"Page app/page.js (2:3)",
],
}
`)
})
test('Server component errors should open up in fullscreen', async () => {
await using sandbox = await createSandbox(
next,
new Map([
// Start with error
[
'app/page.js',
outdent`
export default function Page() {
throw new Error('Server component error')
return <p id="text">Hello world</p>
}
`,
],
])
)
const { session, browser } = sandbox
await expect(browser).toDisplayRedbox(
`
{
"description": "Server component error",
"environmentLabel": "Server",
"label": "<FIXME-excluded-label>",
"source": "app/page.js (2:9) @ Page
> 2 | throw new Error('Server component error')
| ^",
"stack": [
"Page app/page.js (2:9)",
],
}
`,
// FIXME: `label` is flaking between "Runtime Error" and "Recoverable Error"
{ label: false }
)
// Remove error
await session.patch(
'app/page.js',
outdent`
export default function Page() {
return <p id="text">Hello world</p>
}
`
)
expect(await browser.waitForElementByCss('#text').text()).toBe(
'Hello world'
)
await session.waitForNoRedbox()
// Re-add error
await session.patch(
'app/page.js',
outdent`
export default function Page() {
throw new Error('Server component error!')
return <p id="text">Hello world</p>
}
`
)
await expect(browser).toDisplayRedbox(
`
{
"description": "Server component error!",
"environmentLabel": "Server",
"label": "<FIXME-excluded-label>",
"source": "app/page.js (2:9) @ Page
> 2 | throw new Error('Server component error!')
| ^",
"stack": [
"Page app/page.js (2:9)",
],
}
`,
// FIXME: `label` is flaking between "Runtime Error" and "Recoverable Error"
{ label: false }
)
})
test('Import trace when module not found in layout', async () => {
await using sandbox = await createSandbox(
next,
new Map([['app/module.js', `import "non-existing-module"`]])
)
const { browser, session } = sandbox
await session.patch(
'app/layout.js',
outdent`
import "./module"
export default function RootLayout({ children }) {
return (
<html>
<head></head>
<body>{children}</body>
</html>
)
}
`
)
if (isTurbopack) {
await expect(browser).toDisplayRedbox(`
{
"description": "Module not found: Can't resolve 'non-existing-module'",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/module.js (1:1)
Module not found: Can't resolve 'non-existing-module'
> 1 | import "non-existing-module"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^",
"stack": [],
}
`)
} else if (isRspack) {
await expect({ browser, next }).toDisplayRedbox(`
{
"description": " × Module not found: Can't resolve 'non-existing-module' in '<FIXME-project-root>/app'",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/module.js
× Module not found: Can't resolve 'non-existing-module' in '<FIXME-project-root>/app'
╭────
1 │ import "non-existing-module";
· ─────────────────────────────
╰────
Import trace for requested module:
./app/module.js
./app/layout.js",
"stack": [],
}
`)
} else {
await expect(browser).toDisplayRedbox(`
{
"description": "Module not found: Can't resolve 'non-existing-module'",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/module.js (1:1)
Module not found: Can't resolve 'non-existing-module'
> 1 | import "non-existing-module"
| ^",
"stack": [],
}
`)
}
})
test("Can't resolve @import in CSS file", async () => {
await using sandbox = await createSandbox(
next,
new Map([
['app/styles1.css', '@import "./styles2.css"'],
['app/styles2.css', '@import "./boom.css"'],
])
)
const { browser, session } = sandbox
await session.patch(
'app/layout.js',
outdent`
import "./styles1.css"
export default function RootLayout({ children }) {
return (
<html>
<head></head>
<body>{children}</body>
</html>
)
}
`
)
// Wait for patch to apply and new error to show.
if (isTurbopack) {
await expect(browser).toDisplayRedbox(`
{
"description": "Module not found: Can't resolve './boom.css'",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/styles2.css (1:1)
Module not found: Can't resolve './boom.css'
> 1 | @import "./boom.css"
| ^",
"stack": [],
}
`)
} else if (isRspack) {
await expect({ browser, next }).toDisplayRedbox(`
{
"description": "Failed to compile",
"environmentLabel": null,
"label": "Build Error",
"source": "╰─▶ × Error: RspackResolver(NotFound("./boom.css"))",
"stack": [],
}
`)
} else {
await expect(browser).toDisplayRedbox(`
{
"description": "Module not found: Can't resolve './boom.css'",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/styles2.css
Module not found: Can't resolve './boom.css'
https://nextjs.org/docs/messages/module-not-found
Import trace for requested module:
./app/styles1.css",
"stack": [],
}
`)
}
})
// TODO: The error overlay is not closed when restoring the working code.
for (const type of ['server' /* , 'client' */]) {
test(`${type} component can recover from error thrown in the module`, async () => {
await using sandbox = await createSandbox(next, undefined, '/' + type)
const { browser, session } = sandbox
await next.patchFile('index.js', "throw new Error('module error')")
await retry(async () => {
// Should use `await expect(browser).toDisplayRedbox()`
await session.waitForRedbox()
})
if (isRspack) {
await expect({ browser, next }).toDisplayRedbox(`
{
"code": "E394",
"description": "module error",
"environmentLabel": null,
"label": "Runtime Error",
"source": "index.js (1:7) @ eval
> 1 | throw new Error('module error')
| ^",
"stack": [
"eval index.js (1:7)",
"<FIXME-next-dist-dir>",
"<FIXME-next-dist-dir>",
"eval ./app/server/page.js",
"<FIXME-next-dist-dir>",
"<FIXME-next-dist-dir>",
],
}
`)
} else if (!isTurbopack) {
await expect({ browser, next }).toDisplayRedbox(`
{
"code": "E394",
"description": "module error",
"environmentLabel": null,
"label": "Runtime Error",
"source": "index.js (1:7) @ eval
> 1 | throw new Error('module error')
| ^",
"stack": [
"eval index.js (1:7)",
"<FIXME-next-dist-dir>",
"eval ./app/server/page.js",
"<FIXME-next-dist-dir>",
],
}
`)
}
await next.patchFile(
'index.js',
'export default function Page() {return <p>hello world</p>}'
)
await session.waitForNoRedbox()
})
}
test('Should show error location for server actions in client component', async () => {
await using sandbox = await createSandbox(
next,
new Map([
[
'app/actions.ts',
`"use server";
export async function serverAction(a) {
throw new Error("server action was here");
}`,
],
[
'app/page.js',
`"use client";
import { serverAction } from "./actions";
export default function Home() {
return (
<>
<form action={serverAction}>
<button id="trigger-action">Submit</button>
</form>
</>
);
}`,
],
])
)
const { browser } = sandbox
await browser.elementByCss('#trigger-action').click()
await expect(browser).toDisplayRedbox(`
{
"description": "server action was here",
"environmentLabel": "Server",
"label": "Runtime Error",
"source": "app/actions.ts (4:9) @ serverAction
> 4 | throw new Error("server action was here");
| ^",
"stack": [
"serverAction app/actions.ts (4:9)",
"form <anonymous>",
"Home app/page.js (7:7)",
],
}
`)
})
test('Should show error location for server actions in server component', async () => {
await using sandbox = await createSandbox(
next,
new Map([
[
'app/actions.ts',
`"use server";
export async function serverAction(a) {
throw new Error("server action was here");
}`,
],
[
'app/page.js',
`import { serverAction } from "./actions";
export default function Home() {
return (
<>
<form action={serverAction}>
<button id="trigger-action">Submit</button>
</form>
</>
);
}`,
],
])
)
const { browser } = sandbox
await browser.elementByCss('#trigger-action').click()
await expect(browser).toDisplayRedbox(`
{
"description": "server action was here",
"environmentLabel": "Server",
"label": "Runtime Error",
"source": "app/actions.ts (4:9) @ serverAction
> 4 | throw new Error("server action was here");
| ^",
"stack": [
"serverAction app/actions.ts (4:9)",
"form <anonymous>",
"Home app/page.js (6:7)",
],
}
`)
})
test('should collapse bundler internal stack frames', async () => {
await using sandbox = await createSandbox(
next,
new Map([
[
'app/utils.ts',
`throw new Error('utils error')
export function foo(){}
`,
],
[
'app/page.js',
`"use client";
import { foo } from "./utils";
export default function Home() {
foo();
return "hello";
}`,
],
])
)
const { browser } = sandbox
if (isTurbopack) {
// FIXME: display the sourcemapped stack frames
await expect(browser).toDisplayRedbox(`
{
"description": "utils error",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/utils.ts (1:7) @ module evaluation
> 1 | throw new Error('utils error')
| ^",
"stack": [
"module evaluation app/utils.ts (1:7)",
"module evaluation app/page.js (2:1)",
],
}
`)
} else if (isRspack) {
await expect(browser).toDisplayRedbox(`
{
"description": "utils error",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/utils.ts (1:7) @ eval
> 1 | throw new Error('utils error')
| ^",
"stack": [
"eval app/utils.ts (1:7)",
"<FIXME-next-dist-dir>",
"<FIXME-next-dist-dir>",
"<FIXME-next-dist-dir>",
"eval ./app/page.js",
"<FIXME-next-dist-dir>",
"<FIXME-next-dist-dir>",
"<FIXME-next-dist-dir>",
],
}
`)
} else {
// FIXME: Webpack stack frames are not source mapped
await expect(browser).toDisplayRedbox(`
[
{
"description": "utils error",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/utils.ts (1:7) @ eval
> 1 | throw new Error('utils error')
| ^",
"stack": [
"eval app/utils.ts (1:7)",
"<FIXME-next-dist-dir>",
"eval ./app/page.js",
"<FIXME-next-dist-dir>",
],
},
{
"description": "utils error",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/utils.ts (1:7) @ eval
> 1 | throw new Error('utils error')
| ^",
"stack": [
"eval app/utils.ts (1:7)",
"<FIXME-next-dist-dir>",
"eval ./app/page.js",
"<FIXME-next-dist-dir>",
],
},
]
`)
}
})
})