Files
next.js/test/development/browser-logs/browser-logs.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

419 lines
14 KiB
TypeScript
Raw Permalink 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 { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'e2e-utils'
import webdriver from 'next-webdriver'
import { join } from 'path'
import stripAnsi from 'strip-ansi'
import { retry } from 'next-test-utils'
const bundlerName = process.env.IS_TURBOPACK_TEST ? 'Turbopack' : 'Webpack'
const enableNewScrollHandler =
process.env.__NEXT_EXPERIMENTAL_APP_NEW_SCROLL_HANDLER === 'true'
const innerScrollAndMaybeFocusHandlerName = enableNewScrollHandler
? 'InnerScrollHandlerNew'
: 'InnerScrollAndFocusHandlerOld'
function setupLogCapture() {
const logs: string[] = []
const originalStdout = process.stdout.write
const originalStderr = process.stderr.write
const capture = (chunk: any) => {
logs.push(stripAnsi(chunk.toString()))
return true
}
process.stdout.write = function (chunk: any) {
capture(chunk)
return originalStdout.call(this, chunk)
}
process.stderr.write = function (chunk: any) {
capture(chunk)
return originalStderr.call(this, chunk)
}
const restore = () => {
process.stdout.write = originalStdout
process.stderr.write = originalStderr
}
const clearLogs = () => {
logs.length = 0
}
return { logs, restore, clearLogs }
}
describe(`Terminal Logging (${bundlerName})`, () => {
describe('Pages Router', () => {
let next: NextInstance
let logs: string[] = []
let logCapture: ReturnType<typeof setupLogCapture>
let browser = null
beforeAll(async () => {
logCapture = setupLogCapture()
logs = logCapture.logs
next = await createNext({
files: {
pages: new FileRef(join(__dirname, 'fixtures/pages')),
'next.config.js': new FileRef(
join(__dirname, 'fixtures/next.config.js')
),
},
})
})
afterAll(async () => {
logCapture.restore()
await next.destroy()
})
beforeEach(() => {
logCapture.clearLogs()
})
afterEach(async () => {
if (browser) {
await browser.close()
browser = null
}
})
it('should forward client component logs', async () => {
browser = await webdriver(next.url, '/pages-client-log')
await browser.waitForElementByCss('#log-button')
await browser.elementByCss('#log-button').click()
await retry(() => {
const logOutput = logs.join('')
expect(logOutput).toContain(
'[browser] Log from pages router client component'
)
})
})
it('should handle circular references safely', async () => {
browser = await webdriver(next.url, '/circular-refs')
await browser.waitForElementByCss('#circular-button')
await browser.elementByCss('#circular-button').click()
await retry(() => {
const logOutput = logs.join('\n')
expect(logOutput).toContain('[browser] Circular object:')
expect(logOutput).toContain('[Circular]')
})
})
it('should respect default depth limit', async () => {
browser = await webdriver(next.url, '/deep-objects')
await browser.waitForElementByCss('#deep-button')
await browser.elementByCss('#deep-button').click()
await retry(() => {
const logOutput = logs.join('\n')
expect(logOutput).toContain('[browser] Deep object: {')
expect(logOutput).toContain('level1: {')
expect(logOutput).toContain('level2: { level3: { level4: { level5:')
expect(logOutput).toContain("'[Object]'")
})
})
it('should show source-mapped errors in pages router', async () => {
browser = await webdriver(next.url, '/pages-client-error')
await browser.waitForElementByCss('#error-button')
logCapture.clearLogs()
await browser.elementByCss('#error-button').click()
await retry(() => {
const logOutput = logs.join('\n')
const browserErrorPattern =
/\[browser\] Uncaught Error: Client error in pages router\n\s+at throwClientError \(pages\/pages-client-error\.js:2:\d+\)\n\s+at callClientError \(pages\/pages-client-error\.js:6:\d+\)/
expect(logOutput).toMatch(browserErrorPattern)
})
})
it('should show source-mapped errors for server errors from pages router ', async () => {
const outputIndex = logs.length
browser = await webdriver(next.url, '/pages-server-error')
await retry(() => {
const newLogs = logs.slice(outputIndex).join('\n')
const browserErrorPattern =
/\[browser\] Uncaught Error: Server error in pages router\n\s+at throwPagesServerError \(pages\/pages-server-error\.js:2:\d+\)\n\s+at callPagesServerError \(pages\/pages-server-error\.js:6:\d+\)/
expect(newLogs).toMatch(browserErrorPattern)
})
})
})
describe('App Router - Server Components', () => {
let next: NextInstance
let logs: string[] = []
let logCapture: ReturnType<typeof setupLogCapture>
beforeAll(async () => {
logCapture = setupLogCapture()
logs = logCapture.logs
next = await createNext({
files: {
app: new FileRef(join(__dirname, 'fixtures/app')),
'next.config.js': new FileRef(
join(__dirname, 'fixtures/next.config.js')
),
},
})
})
afterAll(async () => {
logCapture.restore()
await next.destroy()
})
beforeEach(() => {
logCapture.clearLogs()
})
it('should not re-log server component logs', async () => {
const outputIndex = logs.length
await next.render('/server-log')
await retry(() => {
const newLogs = logs.slice(outputIndex).join('')
expect(newLogs).toContain('Server component console.log')
}, 2000)
const newLogs = logs.slice(outputIndex).join('')
expect(newLogs).not.toContain('[browser] Server component console.log')
expect(newLogs).not.toContain('[browser] Server component console.error')
})
it('should show source-mapped errors for server components', async () => {
const outputIndex = logs.length
const browser = await webdriver(next.url, '/server-error')
await retry(() => {
const newLogs = logs.slice(outputIndex).join('\n')
const browserErrorPattern =
/\[browser\] Uncaught Error: Server component error in app router\n\s+at throwServerError \(app\/server-error\/page\.js:2:\d+\)\n\s+at callServerError \(app\/server-error\/page\.js:6:\d+\)\n\s+at ServerErrorPage \(app\/server-error\/page\.js:10:\d+\)/
expect(newLogs).toMatch(browserErrorPattern)
})
await browser.close()
})
})
describe('App Router - Client Components', () => {
let next: NextInstance
let logs: string[] = []
let logCapture: ReturnType<typeof setupLogCapture>
beforeAll(async () => {
logCapture = setupLogCapture()
logs = logCapture.logs
next = await createNext({
files: {
app: new FileRef(join(__dirname, 'fixtures/app')),
'next.config.js': new FileRef(
join(__dirname, 'fixtures/next.config.js')
),
},
})
})
afterAll(async () => {
logCapture.restore()
await next.destroy()
})
beforeEach(() => {
logCapture.clearLogs()
})
it('should forward client component logs in app router', async () => {
const browser = await webdriver(next.url, '/client-log')
await browser.waitForElementByCss('#log-button')
await browser.elementByCss('#log-button').click()
await retry(() => {
const logOutput = logs.join('')
expect(logOutput).toContain(
'[browser] Client component log from app router'
)
})
await browser.close()
})
it('should show source-mapped errors for client components', async () => {
const browser = await webdriver(next.url, '/client-error')
await browser.waitForElementByCss('#error-button')
logCapture.clearLogs()
await browser.elementByCss('#error-button').click()
await retry(() => {
const logOutput = logs.join('\n')
const browserErrorPattern =
/\[browser\] Uncaught Error: Client component error in app router\n\s+at throwError \(app\/client-error\/page\.js:4:\d+\)\n\s+at callError \(app\/client-error\/page\.js:8:\d+\)/
expect(logOutput).toMatch(browserErrorPattern)
})
await browser.close()
})
})
describe('App Router - Hydration Errors', () => {
let next: NextInstance
let logs: string[] = []
let logCapture: ReturnType<typeof setupLogCapture>
beforeAll(async () => {
logCapture = setupLogCapture()
logs = logCapture.logs
next = await createNext({
files: {
app: new FileRef(join(__dirname, 'fixtures/app')),
'next.config.js': new FileRef(
join(__dirname, 'fixtures/next.config.js')
),
},
})
})
afterAll(async () => {
logCapture.restore()
await next.destroy()
})
beforeEach(() => {
logCapture.clearLogs()
})
it('should show hydration errors with owner stack trace', async () => {
const browser = await webdriver(next.url, '/hydration-error')
let hydrationErrorLog = ''
await retry(() => {
const logOutput = logs.join('\n')
// Find the hydration error log entry
// Stop at: another [browser] log, status indicators (○ ),
// or timestamp-prefixed logs (e.g. "[12:34:56.789Z] Browser Log: ...")
const hydrationMatch = logOutput.match(
/\[browser\].*Hydration[\s\S]*?(?=\n\[browser\]|\n *○|\n *|\n *\[\d|$)/
)
expect(hydrationMatch).not.toBeNull()
hydrationErrorLog = hydrationMatch![0]
// Verify the Page component is in the forwarded stack trace with source location
expect(hydrationErrorLog).toMatch(/Page/)
expect(hydrationErrorLog).toMatch(/app\/hydration-error\/page/)
})
// Assert the entire hydration error message including owner stack trace
expect(hydrationErrorLog).toMatchInlineSnapshot(`
"[browser] Uncaught Error: Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
- A server/client branch \`if (typeof window !== 'undefined')\`.
- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
https://react.dev/link/hydration-mismatch
...
<RenderFromTemplateContext>
<ScrollAndMaybeFocusHandler segmentPath={[...]}>
<${innerScrollAndMaybeFocusHandlerName} segmentPath={[...]} focusAndScrollRef={{apply:false, ...}}>
<ErrorBoundary errorComponent={undefined} errorStyles={undefined} errorScripts={undefined}>
<LoadingBoundary name="hydration-..." loading={null}>
<HTTPAccessFallbackBoundary notFound={undefined} forbidden={undefined} unauthorized={undefined}>
<RedirectBoundary>
<RedirectErrorBoundary router={{...}}>
<InnerLayoutRouter url="/hydration..." tree={[...]} params={{}} cacheNode={{rsc:{...}, ...}} ...>
<SegmentViewNode type="page" pagePath="hydration-...">
<SegmentTrieNode>
<ClientPageRoot Component={function Page} serverProvidedParams={{...}}>
<Page params={Promise} searchParams={Promise}>
<div>
<p>
+ client
- server
...
...
...
at <unknown> (https://react.dev/link/hydration-mismatch)
at p (<anonymous>)
at Page (app/hydration-error/page.js:7:7)
5 | return (
6 | <div>
> 7 | <p>{isClient ? 'client' : 'server'}</p>
| ^
8 | </div>
9 | )
10 | }
"
`)
await browser.close()
})
})
describe('App Router - Edge Runtime', () => {
let next: NextInstance
let logs: string[] = []
let logCapture: ReturnType<typeof setupLogCapture>
beforeAll(async () => {
logCapture = setupLogCapture()
logs = logCapture.logs
next = await createNext({
files: {
app: new FileRef(join(__dirname, 'fixtures/app')),
'next.config.js': new FileRef(
join(__dirname, 'fixtures/next.config.js')
),
},
})
})
afterAll(async () => {
logCapture.restore()
await next.destroy()
})
beforeEach(() => {
logCapture.clearLogs()
})
it('should handle edge runtime errors with source mapping', async () => {
const browser = await webdriver(next.url, '/edge-deep-stack')
await retry(() => {
const logOutput = logs.join('\n')
const browserErrorPattern =
/\[browser\] Uncaught Error: Deep stack error during render\n\s+at functionA \(app\/edge-deep-stack\/page\.js:6:\d+\)\n\s+at functionB \(app\/edge-deep-stack\/page\.js:10:\d+\)\n\s+at functionC \(app\/edge-deep-stack\/page\.js:14:\d+\)\n\s+at EdgeDeepStackPage \(app\/edge-deep-stack\/page\.js:18:\d+\)/
expect(logOutput).toMatch(browserErrorPattern)
})
await browser.close()
})
})
})