diff --git a/packages/next/client/components/layout-router.tsx b/packages/next/client/components/layout-router.tsx index f19e762eebf6..698b2e7ef8c4 100644 --- a/packages/next/client/components/layout-router.tsx +++ b/packages/next/client/components/layout-router.tsx @@ -278,9 +278,13 @@ interface RedirectBoundaryProps { children: React.ReactNode } -function InfinitePromiseComponent() { - use(createInfinitePromise()) - return <> +function HandleRedirect({ redirect }: { redirect: string }) { + const router = useContext(AppRouterContext) + + useEffect(() => { + router.replace(redirect, {}) + }, [redirect, router]) + return null } class RedirectErrorBoundary extends React.Component< @@ -297,20 +301,14 @@ class RedirectErrorBoundary extends React.Component< const url = error.digest.split(';')[1] return { redirect: url } } - // Re-throw if error is not for 404 + // Re-throw if error is not for redirect throw error } render() { const redirect = this.state.redirect if (redirect !== null) { - setTimeout(() => { - // @ts-ignore startTransition exists - React.startTransition(() => { - this.props.router.replace(redirect, {}) - }) - }) - return + return } return this.props.children @@ -348,7 +346,12 @@ class NotFoundErrorBoundary extends React.Component< render() { if (this.state.notFoundTriggered) { - return this.props.notFound + return ( + <> + + {this.props.notFound} + + ) } return this.props.children diff --git a/packages/next/client/components/react-dev-overlay/hot-reloader.tsx b/packages/next/client/components/react-dev-overlay/hot-reloader.tsx index b982fa1d5090..e7e90806662e 100644 --- a/packages/next/client/components/react-dev-overlay/hot-reloader.tsx +++ b/packages/next/client/components/react-dev-overlay/hot-reloader.tsx @@ -445,6 +445,16 @@ export default function HotReload({ }) const handleOnUnhandledError = useCallback((ev) => { + if ( + ev.error && + ev.error.digest && + (ev.error.digest.startsWith('NEXT_REDIRECT') || + ev.error.digest === 'NEXT_NOT_FOUND') + ) { + ev.preventDefault() + return + } + hadRuntimeError = true onUnhandledError(dispatch, ev) }, []) diff --git a/test/e2e/app-dir/app/app/not-found/clientcomponent/a.js b/test/e2e/app-dir/app/app/not-found/clientcomponent/page.js similarity index 100% rename from test/e2e/app-dir/app/app/not-found/clientcomponent/a.js rename to test/e2e/app-dir/app/app/not-found/clientcomponent/page.js diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index 9d98f0400fec..134dab910a10 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -1693,32 +1693,44 @@ describe('app dir', () => { expect( await browser.waitForElementByCss('#not-found-component').text() ).toBe('Not Found!') + expect( + await browser + .waitForElementByCss('meta[name="robots"]') + .getAttribute('content') + ).toBe('noindex') }) - it.skip('should trigger not-found in a client component', async () => { + it('should trigger not-found in a client component', async () => { const browser = await webdriver(next.url, '/not-found/clientcomponent') expect( await browser.waitForElementByCss('#not-found-component').text() ).toBe('Not Found!') + expect( + await browser + .waitForElementByCss('meta[name="robots"]') + .getAttribute('content') + ).toBe('noindex') }) - ;(isDev ? it.skip : it)( - 'should trigger not-found client-side', - async () => { - const browser = await webdriver(next.url, '/not-found/client-side') + it('should trigger not-found client-side', async () => { + const browser = await webdriver(next.url, '/not-found/client-side') + await browser + .elementByCss('button') + .click() + .waitForElementByCss('#not-found-component') + expect(await browser.elementByCss('#not-found-component').text()).toBe( + 'Not Found!' + ) + expect( await browser - .elementByCss('button') - .click() - .waitForElementByCss('#not-found-component') - expect( - await browser.elementByCss('#not-found-component').text() - ).toBe('Not Found!') - } - ) + .waitForElementByCss('meta[name="robots"]') + .getAttribute('content') + ).toBe('noindex') + }) }) describe('redirect', () => { describe('components', () => { - it.skip('should redirect in a server component', async () => { + it('should redirect in a server component', async () => { const browser = await webdriver(next.url, '/redirect/servercomponent') await browser.waitForElementByCss('#result-page') expect(await browser.elementByCss('#result-page').text()).toBe( @@ -1726,7 +1738,7 @@ describe('app dir', () => { ) }) - it.skip('should redirect in a client component', async () => { + it('should redirect in a client component', async () => { const browser = await webdriver(next.url, '/redirect/clientcomponent') await browser.waitForElementByCss('#result-page') expect(await browser.elementByCss('#result-page').text()).toBe( @@ -1735,12 +1747,13 @@ describe('app dir', () => { }) // TODO-APP: Enable in development - ;(isDev ? it.skip : it)('should redirect client-side', async () => { + it('should redirect client-side', async () => { const browser = await webdriver(next.url, '/redirect/client-side') await browser .elementByCss('button') .click() .waitForElementByCss('#result-page') + // eslint-disable-next-line jest/no-standalone-expect expect(await browser.elementByCss('#result-page').text()).toBe( 'Result Page' )