Skip to content

Commit

Permalink
Fix incorrect error count (vercel#75759)
Browse files Browse the repository at this point in the history
  • Loading branch information
huozhi authored Feb 7, 2025
1 parent 7fcf142 commit b9277bf
Show file tree
Hide file tree
Showing 12 changed files with 99 additions and 18 deletions.
3 changes: 3 additions & 0 deletions packages/next/src/client/components/errors/stitched-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export function getReactStitchedError<T = unknown>(err: T): Error | T {
}

function appendOwnerStack(error: Error) {
if (!React.captureOwnerStack) {
return
}
let stack = error.stack || ''
// This module is only bundled in development mode so this is safe.
const ownerStack = React.captureOwnerStack()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ export function useErrorHandler(
)

// Reset error queues.
errorQueue.splice(0, 0)
rejectionQueue.splice(0, 0)
errorQueue.splice(0, errorQueue.length)
rejectionQueue.splice(0, rejectionQueue.length)
}
}, [handleOnUnhandledError, handleOnUnhandledRejection])
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ function getErrorSignature(ev: SupportedErrorEvent): string {
case ACTION_UNHANDLED_REJECTION: {
return `${event.reason.name}::${event.reason.message}::${event.reason.stack}`
}
default: {
}
default:
break
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _: never = event as never
const _ = event satisfies never
return ''
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ export function Errors({
return (
<Toast
data-nextjs-toast
data-issues
className={`nextjs-toast-errors-parent${hasStaticIndicator ? ' nextjs-error-with-static' : ''}`}
onClick={fullscreen}
>
Expand All @@ -256,7 +257,8 @@ export function Errors({
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
<span>
{readyErrors.length} issue{readyErrors.length > 1 ? 's' : ''}
<span data-issues-count>{readyErrors.length}</span> issue
{readyErrors.length > 1 ? 's' : ''}
</span>
<button
data-nextjs-toast-errors-hide-button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ function pushErrorFilterDuplicates(
return [
...errors.filter((e) => {
// Filter out duplicate errors
return e.event.reason !== err.event.reason
return e.event.reason.stack !== err.event.reason.stack
}),
err,
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ describe('Dynamic IO Dev Errors', () => {
it('should show a red box error on client navigations', async () => {
const browser = await next.browser('/no-error')

expect(await hasErrorToast(browser)).toBe(false)
await retry(async () => {
expect(await hasErrorToast(browser)).toBe(false)
})

await browser.elementByCss("[href='/error']").click()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Page() {
return (
<p>
<p>nest</p>
</p>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use client'

export default function Page() {
return <p>{typeof window === 'undefined' ? 'server' : 'client'}</p>
}
8 changes: 8 additions & 0 deletions test/development/app-dir/hydration-error-count/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ReactNode } from 'react'
export default function Root({ children }: { children: ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { nextTestSetup } from 'e2e-utils'
import { hasErrorToast, getToastErrorCount, retry } from 'next-test-utils'

describe('hydration-error-count', () => {
const { next } = nextTestSetup({
files: __dirname,
})

// These error count should be consistent between normal mode and when reactOwnerStack is enabled (PPR testing)
it('should have correct hydration error count for bad nesting', async () => {
const browser = await next.browser('/bad-nesting')

await retry(async () => {
await hasErrorToast(browser)
const totalErrorCount = await getToastErrorCount(browser)

// One hydration error and one warning
expect(totalErrorCount).toBe(2)
})
})

it('should have correct hydration error count for html diff', async () => {
const browser = await next.browser('/html-diff')

await retry(async () => {
await hasErrorToast(browser)
const totalErrorCount = await getToastErrorCount(browser)

// One hydration error and one warning
expect(totalErrorCount).toBe(1)
})
})
})
6 changes: 6 additions & 0 deletions test/development/app-dir/hydration-error-count/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {}

module.exports = nextConfig
35 changes: 25 additions & 10 deletions test/lib/next-test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -887,17 +887,32 @@ export async function assertNoRedbox(browser: BrowserInterface) {
export async function hasErrorToast(
browser: BrowserInterface
): Promise<boolean> {
return (
return Boolean(
await browser.eval(() => {
const portal = [].slice
.call(document.querySelectorAll('nextjs-portal'))
.find((p) => p.shadowRoot.querySelector('[data-issues]'))

const root = portal?.shadowRoot
const node = root?.querySelector('[data-issues-count]')
return !!node
})
)
}

export async function getToastErrorCount(
browser: BrowserInterface
): Promise<number> {
return parseInt(
(await browser.eval(() => {
return Boolean(
[].slice.call(document.querySelectorAll('nextjs-portal')).find((p) =>
p.shadowRoot.querySelector(
// TODO(jiwon): data-nextjs-toast may not be an error indicator in new UI
isNewDevOverlay ? '[data-issues]' : '[data-nextjs-toast]'
)
)
)
})) ?? false // When browser.eval() throws, it returns null.
const portal = [].slice
.call(document.querySelectorAll('nextjs-portal'))
.find((p) => p.shadowRoot.querySelector('[data-issues]'))

const root = portal?.shadowRoot
const node = root?.querySelector('[data-issues-count]')
return node?.innerText || '0'
})) ?? 0
)
}

Expand Down

0 comments on commit b9277bf

Please sign in to comment.