import { nextTestSetup } from 'e2e-utils' import { check } from 'next-test-utils' describe('app dir - not-found - basic', () => { const { next, isNextDev, isNextStart, skipped } = nextTestSetup({ files: __dirname, skipDeployment: true, }) if (skipped) { return } it("should propagate notFound errors past a segment's error boundary", async () => { let browser = await next.browser('/error-boundary') await browser.elementByCss('button').click() expect(await browser.elementByCss('h1').text()).toBe('Root Not Found') browser = await next.browser('/error-boundary/nested/nested-2') await browser.elementByCss('button').click() expect(await browser.elementByCss('h1').text()).toBe( 'Not Found (error-boundary/nested)' ) browser = await next.browser('/error-boundary/nested/trigger-not-found') expect(await browser.elementByCss('h1').text()).toBe( 'Not Found (error-boundary/nested)' ) }) it('should return 404 status code for custom not-found page', async () => { const res = await next.fetch('/_not-found') expect(res.status).toBe(404) }) if (isNextStart) { it('should include not found client reference manifest in the file trace', async () => { const fileTrace = JSON.parse( await next.readFile('.next/server/app/_not-found/page.js.nft.json') ) const isTraced = fileTrace.files.some((filePath) => filePath.includes('page_client-reference-manifest.js') ) expect(isTraced).toBe(true) }) it('should not output /404 in tree view logs', async () => { const output = await next.cliOutput expect(output).not.toContain('○ /404') }) it('should use root not-found content for 404 html', async () => { // static /404 page will use /_not-found content const page404 = await next.readFile('.next/server/pages/404.html') expect(page404).toContain('Root Not Found') }) } const runTests = ({ isEdge }: { isEdge: boolean }) => { it('should return 404 status if notFound() is called in shell', async () => { const res = await next.fetch('/shell-not-found') expect(res.status).toBe(404) expect(await res.text()).toInclude('"noindex"') }) it('should use the not-found page for non-matching routes', async () => { const browser = await next.browser('/random-content') expect(await browser.elementByCss('h1').text()).toContain( 'Root Not Found' ) // should contain root layout content expect(await browser.elementByCss('#layout-nav').text()).toBe('Navbar') }) it('should match dynamic route not-found boundary correctly', async () => { // `/dynamic` display works const browserDynamic = await next.browser('/dynamic') expect(await browserDynamic.elementByCss('main').text()).toBe('dynamic') // `/dynamic/404` calling notFound() will match the same level not-found boundary const browserDynamic404 = await next.browser('/dynamic/404') expect(await browserDynamic404.elementByCss('#not-found').text()).toBe( 'dynamic/[id] not found' ) const browserDynamicId = await next.browser('/dynamic/123') expect(await browserDynamicId.elementByCss('#page').text()).toBe( 'dynamic [id]' ) }) it('should escalate notFound to parent layout if no not-found boundary present in current layer', async () => { const browserDynamic = await next.browser( '/dynamic-layout-without-not-found' ) expect(await browserDynamic.elementByCss('h1').text()).toBe( 'Dynamic with Layout' ) // no not-found boundary in /dynamic-layout-without-not-found, escalate to parent layout to render root not-found const browserDynamicId = await next.browser( '/dynamic-layout-without-not-found/404' ) expect(await browserDynamicId.elementByCss('h1').text()).toBe( 'Root Not Found' ) const browserDynamic404 = await next.browser( '/dynamic-layout-without-not-found/123' ) expect(await browserDynamic404.elementByCss('#page').text()).toBe( 'dynamic-layout-without-not-found [id]' ) }) if (isNextDev) { it('should not reload the page', async () => { const browser = await next.browser('/random-content') const timestamp = await browser.elementByCss('#timestamp').text() await new Promise((resolve) => { setTimeout(resolve, 3000) }) await check(async () => { const newTimestamp = await browser.elementByCss('#timestamp').text() return newTimestamp !== timestamp ? 'failure' : 'success' }, 'success') }) // Disabling for Edge because it is too flakey. // @TODO investigate a proper for fix for this flake if (!isEdge) { it('should render the 404 page when the file is removed, and restore the page when re-added', async () => { const browser = await next.browser('/') await check(() => browser.elementByCss('h1').text(), 'My page') await next.renameFile('./app/page.js', './app/foo.js') await check(() => browser.elementByCss('h1').text(), 'Root Not Found') await next.renameFile('./app/foo.js', './app/page.js') await check(() => browser.elementByCss('h1').text(), 'My page') }) } } if (!isNextDev && !isEdge) { it('should create the 404 mapping and copy the file to pages', async () => { const html = await next.readFile('.next/server/pages/404.html') expect(html).toContain('Root Not Found') expect( await next.readFile('.next/server/pages-manifest.json') ).toContain('"pages/404.html"') }) } } describe('with default runtime', () => { runTests({ isEdge: false }) }) describe('with runtime = edge', () => { let originalLayout = '' beforeAll(async () => { await next.stop() originalLayout = await next.readFile('app/layout.js') await next.patchFile( 'app/layout.js', `export const runtime = 'edge'\n${originalLayout}` ) await next.start() }) afterAll(async () => { await next.patchFile('app/layout.js', originalLayout) }) runTests({ isEdge: true }) }) })