diff --git a/packages/next/client/components/react-dev-overlay/internal/ReactDevOverlay.tsx b/packages/next/client/components/react-dev-overlay/internal/ReactDevOverlay.tsx index ee85548be45103f..931a1c93ca74517 100644 --- a/packages/next/client/components/react-dev-overlay/internal/ReactDevOverlay.tsx +++ b/packages/next/client/components/react-dev-overlay/internal/ReactDevOverlay.tsx @@ -76,9 +76,9 @@ class ReactDevOverlay extends React.PureComponent< ) : hasBuildError ? ( ) : hasRuntimeErrors ? ( - + ) : reactError ? ( - + ) : undefined} ) : undefined} diff --git a/packages/next/client/components/react-dev-overlay/internal/container/Errors.tsx b/packages/next/client/components/react-dev-overlay/internal/container/Errors.tsx index 84d83cd46f58c65..22d0c90777f4043 100644 --- a/packages/next/client/components/react-dev-overlay/internal/container/Errors.tsx +++ b/packages/next/client/components/react-dev-overlay/internal/container/Errors.tsx @@ -24,10 +24,15 @@ export type SupportedErrorEvent = { id: number event: UnhandledErrorAction | UnhandledRejectionAction } -export type ErrorsProps = { errors: SupportedErrorEvent[] } +export type ErrorsProps = { + errors: SupportedErrorEvent[] + initialDisplayState: DisplayState +} type ReadyErrorEvent = ReadyRuntimeError +type DisplayState = 'minimized' | 'fullscreen' | 'hidden' + function getErrorSignature(ev: SupportedErrorEvent): string { const { event } = ev switch (event.type) { @@ -73,7 +78,10 @@ const HotlinkedText: React.FC<{ ) } -export const Errors: React.FC = function Errors({ errors }) { +export const Errors: React.FC = function Errors({ + errors, + initialDisplayState, +}) { const [lookups, setLookups] = React.useState( {} as { [eventId: string]: ReadyErrorEvent } ) @@ -137,9 +145,8 @@ export const Errors: React.FC = function Errors({ errors }) { } }, [nextError]) - const [displayState, setDisplayState] = React.useState< - 'minimized' | 'fullscreen' | 'hidden' - >('fullscreen') + const [displayState, setDisplayState] = + React.useState(initialDisplayState) const [activeIdx, setActiveIndex] = React.useState(0) const previous = React.useCallback((e?: MouseEvent | TouchEvent) => { e?.preventDefault() diff --git a/test/development/acceptance-app/ReactRefreshLogBox.test.ts b/test/development/acceptance-app/ReactRefreshLogBox.test.ts index 3a372d42d427698..6e73c94dfa900eb 100644 --- a/test/development/acceptance-app/ReactRefreshLogBox.test.ts +++ b/test/development/acceptance-app/ReactRefreshLogBox.test.ts @@ -49,7 +49,7 @@ describe('ReactRefreshLogBox app', () => { ) await session.evaluate(() => document.querySelector('a').click()) - expect(await session.hasRedbox(true)).toBe(true) + await session.waitForAndOpenRuntimeError() expect(await session.getRedboxSource()).toMatchSnapshot() await cleanup() @@ -481,7 +481,7 @@ describe('ReactRefreshLogBox app', () => { ) await new Promise((resolve) => setTimeout(resolve, 1000)) - expect(await session.hasRedbox(true)).toBe(true) + await session.waitForAndOpenRuntimeError() if (process.platform === 'win32') { expect(await session.getRedboxSource()).toMatchSnapshot() } else { @@ -568,7 +568,7 @@ describe('ReactRefreshLogBox app', () => { `export default function FunctionDefault() { throw new Error('no'); }` ) - expect(await session.hasRedbox(true)).toBe(true) + await session.waitForAndOpenRuntimeError() expect(await session.getRedboxSource()).toMatchSnapshot() expect( await session.evaluate(() => document.querySelector('h2').textContent) @@ -770,9 +770,8 @@ describe('ReactRefreshLogBox app', () => { ` ) - expect(await session.hasRedbox()).toBe(false) await session.evaluate(() => document.querySelector('button').click()) - expect(await session.hasRedbox(true)).toBe(true) + await session.waitForAndOpenRuntimeError() const header = await session.getRedboxDescription() expect(header).toMatchSnapshot() @@ -816,9 +815,8 @@ describe('ReactRefreshLogBox app', () => { ` ) - expect(await session.hasRedbox()).toBe(false) await session.evaluate(() => document.querySelector('button').click()) - expect(await session.hasRedbox(true)).toBe(true) + await session.waitForAndOpenRuntimeError() const header2 = await session.getRedboxDescription() expect(header2).toMatchSnapshot() @@ -862,9 +860,8 @@ describe('ReactRefreshLogBox app', () => { ` ) - expect(await session.hasRedbox()).toBe(false) await session.evaluate(() => document.querySelector('button').click()) - expect(await session.hasRedbox(true)).toBe(true) + await session.waitForAndOpenRuntimeError() const header3 = await session.getRedboxDescription() expect(header3).toMatchSnapshot() @@ -908,9 +905,8 @@ describe('ReactRefreshLogBox app', () => { ` ) - expect(await session.hasRedbox()).toBe(false) await session.evaluate(() => document.querySelector('button').click()) - expect(await session.hasRedbox(true)).toBe(true) + await session.waitForAndOpenRuntimeError() const header4 = await session.getRedboxDescription() expect(header4).toMatchInlineSnapshot( @@ -1085,4 +1081,76 @@ describe('ReactRefreshLogBox app', () => { await cleanup() }) + + test('Unhandled errors and rejections opens up in the minimized state', async () => { + const { session, browser, cleanup } = await sandbox(next) + + const file = ` + export default function Index() { + // + setTimeout(() => { + throw new Error('Unhandled error') + }, 0) + setTimeout(() => { + Promise.reject(new Error('Undhandled rejection')) + }, 0) + return ( + <> + + + + ) + } + ` + + await session.patch('index.js', file) + + // Unhandled error and rejection in setTimeout + expect( + await browser.waitForElementByCss('.nextjs-toast-errors').text() + ).toBe('2 errors') + + // Unhandled error in event handler + await browser.elementById('unhandled-error').click() + await check( + () => browser.elementByCss('.nextjs-toast-errors').text(), + /3 errors/ + ) + + // Unhandled rejection in event handler + await browser.elementById('unhandled-rejection').click() + await check( + () => browser.elementByCss('.nextjs-toast-errors').text(), + /4 errors/ + ) + expect(await session.hasRedbox()).toBe(false) + + // 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 + expect(await session.hasRedbox(true)).toBe(true) + + await cleanup() + }) }) diff --git a/test/development/acceptance-app/helpers.ts b/test/development/acceptance-app/helpers.ts index f0787fb84b76dad..c97d311cb6bc74f 100644 --- a/test/development/acceptance-app/helpers.ts +++ b/test/development/acceptance-app/helpers.ts @@ -112,6 +112,9 @@ export async function sandbox( } return source }, + async waitForAndOpenRuntimeError() { + return browser.waitForElementByCss('[data-nextjs-toast]').click() + }, }, async cleanup() { await browser.close()