import { createNextDescribe } from 'e2e-utils' import crypto from 'crypto' import { check, getRedboxHeader, hasRedbox, waitFor } from 'next-test-utils' import cheerio from 'cheerio' import stripAnsi from 'strip-ansi' createNextDescribe( 'app dir', { files: __dirname, }, ({ next, isNextDev: isDev, isNextStart, isNextDeploy }) => { it('should have correct searchParams and params (server)', async () => { const html = await next.render('/dynamic/category-1/id-2?query1=value2') const $ = cheerio.load(html) expect(JSON.parse($('#id-page-params').text())).toEqual({ category: 'category-1', id: 'id-2', }) expect(JSON.parse($('#search-params').text())).toEqual({ query1: 'value2', }) }) it('should have correct searchParams and params (client)', async () => { const browser = await next.browser( '/dynamic-client/category-1/id-2?query1=value2' ) const html = await browser.eval('document.documentElement.innerHTML') const $ = cheerio.load(html) expect(JSON.parse($('#id-page-params').text())).toEqual({ category: 'category-1', id: 'id-2', }) expect(JSON.parse($('#search-params').text())).toEqual({ query1: 'value2', }) }) if (!isDev) { it('should successfully detect app route during prefetch', async () => { const browser = await next.browser('/') await check(async () => { const found = await browser.eval( '!!window.next.router.components["/dashboard"]' ) return found ? 'success' : await browser.eval('Object.keys(window.next.router.components)') }, 'success') await browser.elementByCss('a').click() await browser.waitForElementByCss('#from-dashboard') }) } it('should encode chunk path correctly', async () => { await next.fetch('/dynamic-client/first/second') const browser = await next.browser('/') const requests = [] browser.on('request', (req) => { requests.push(req.url()) }) await browser.eval( 'window.location.href = "/dynamic-client/first/second"' ) await check(async () => { return requests.some( (req) => req.includes(encodeURI('/[category]/[id]')) && req.endsWith('.js') ) ? 'found' : JSON.stringify(requests) }, 'found') }) it.each([ { pathname: '/redirect-1' }, { pathname: '/redirect-2' }, { pathname: '/blog/old-post' }, { pathname: '/redirect-3/some' }, { pathname: '/redirect-4' }, ])( 'should match redirects in pages correctly $path', async ({ pathname }) => { let browser = await next.browser('/') await browser.eval(`next.router.push("${pathname}")`) await check(async () => { const href = await browser.eval('location.href') return href.includes('example.vercel.sh') ? 'yes' : href }, 'yes') if (pathname.includes('/blog')) { browser = await next.browser('/blog/first') await browser.eval('window.beforeNav = 1') // check 5 times to ensure a reload didn't occur for (let i = 0; i < 5; i++) { await waitFor(500) expect( await browser.eval('document.documentElement.innerHTML') ).toContain('hello from pages/blog/[slug]') expect(await browser.eval('window.beforeNav')).toBe(1) } } } ) it('should not apply client router filter on shallow', async () => { const browser = await next.browser('/') await browser.eval('window.beforeNav = 1') await check(async () => { await browser.eval( `window.next.router.push('/', '/redirect-1', { shallow: true })` ) return await browser.eval('window.location.pathname') }, '/redirect-1') expect(await browser.eval('window.beforeNav')).toBe(1) }) if (isDev) { it('should not have duplicate config warnings', async () => { await next.fetch('/') expect( stripAnsi(next.cliOutput).match( /You have enabled experimental feature/g ).length ).toBe(1) expect( stripAnsi(next.cliOutput).match( /Experimental features are not covered by semver/g ).length ).toBe(1) }) } if (!isNextDeploy) { it('should not share edge workers', async () => { const controller1 = new AbortController() const controller2 = new AbortController() next .fetch('/slow-page-no-loading', { signal: controller1.signal, }) .catch(() => {}) next .fetch('/slow-page-no-loading', { signal: controller2.signal, }) .catch(() => {}) await waitFor(1000) controller1.abort() const controller3 = new AbortController() next .fetch('/slow-page-no-loading', { signal: controller3.signal, }) .catch(() => {}) await waitFor(1000) controller2.abort() controller3.abort() const res = await next.fetch('/slow-page-no-loading') expect(res.status).toBe(200) expect(await res.text()).toContain('hello from slow page') expect(next.cliOutput).not.toContain( 'A separate worker must be used for each render' ) }) } if (isNextStart) { it('should generate build traces correctly', async () => { const trace = JSON.parse( await next.readFile( '.next/server/app/dashboard/deployments/[id]/page.js.nft.json' ) ) as { files: string[] } expect(trace.files.some((file) => file.endsWith('data.json'))).toBe( true ) }) } it('should use text/x-component for flight', async () => { const res = await next.fetch('/dashboard/deployments/123', { headers: { ['RSC'.toString()]: '1', }, }) expect(res.headers.get('Content-Type')).toBe('text/x-component') }) it('should use text/x-component for flight with edge runtime', async () => { const res = await next.fetch('/dashboard', { headers: { ['RSC'.toString()]: '1', }, }) expect(res.headers.get('Content-Type')).toBe('text/x-component') }) it('should return the `vary` header from edge runtime', async () => { const res = await next.fetch('/dashboard') expect(res.headers.get('x-edge-runtime')).toBe('1') expect(res.headers.get('vary')).toBe( isNextDeploy ? 'RSC, Next-Router-State-Tree, Next-Router-Prefetch' : 'RSC, Next-Router-State-Tree, Next-Router-Prefetch, Accept-Encoding' ) }) it('should return the `vary` header from pages for flight requests', async () => { const res = await next.fetch('/', { headers: { ['RSC'.toString()]: '1', }, }) expect(res.headers.get('vary')).toBe( isNextDeploy ? 'RSC, Next-Router-State-Tree, Next-Router-Prefetch' : 'RSC, Next-Router-State-Tree, Next-Router-Prefetch, Accept-Encoding' ) }) it('should pass props from getServerSideProps in root layout', async () => { const $ = await next.render$('/dashboard') expect($('title').first().text()).toBe('hello world') }) it('should serve from pages', async () => { const html = await next.render('/') expect(html).toContain('hello from pages/index') }) it('should serve dynamic route from pages', async () => { const html = await next.render('/blog/first') expect(html).toContain('hello from pages/blog/[slug]') }) it('should serve from public', async () => { const html = await next.render('/hello.txt') expect(html).toContain('hello world') }) it('should serve from app', async () => { const html = await next.render('/dashboard') expect(html).toContain('hello from app/dashboard') }) if (!isNextDeploy) { it('should serve /index as separate page', async () => { const stderr = [] next.on('stderr', (err) => { stderr.push(err) }) const html = await next.render('/dashboard/index') expect(html).toContain('hello from app/dashboard/index') expect(stderr.some((err) => err.includes('Invalid hook call'))).toBe( false ) }) it('should serve polyfills for browsers that do not support modules', async () => { const html = await next.render('/dashboard/index') expect(html).toMatch( /"'` ) expect(res.status).toBe(500) }) } ) describe('template component', () => { it('should render the template that holds state in a client component and reset on navigation', async () => { const browser = await next.browser('/template/clientcomponent') expect(await browser.elementByCss('h1').text()).toBe('Template 0') await browser.elementByCss('button').click() expect(await browser.elementByCss('h1').text()).toBe('Template 1') await browser.elementByCss('#link').click() await browser.waitForElementByCss('#other-page') expect(await browser.elementByCss('h1').text()).toBe('Template 0') await browser.elementByCss('button').click() expect(await browser.elementByCss('h1').text()).toBe('Template 1') await browser.elementByCss('#link').click() await browser.waitForElementByCss('#page') expect(await browser.elementByCss('h1').text()).toBe('Template 0') }) // TODO-APP: disable failing test and investigate later ;(isDev ? it.skip : it)( 'should render the template that is a server component and rerender on navigation', async () => { const browser = await next.browser('/template/servercomponent') // eslint-disable-next-line jest/no-standalone-expect expect(await browser.elementByCss('h1').text()).toStartWith( 'Template' ) const currentTime = await browser .elementByCss('#performance-now') .text() await browser.elementByCss('#link').click() await browser.waitForElementByCss('#other-page') // eslint-disable-next-line jest/no-standalone-expect expect(await browser.elementByCss('h1').text()).toStartWith( 'Template' ) // template should rerender on navigation even when it's a server component // eslint-disable-next-line jest/no-standalone-expect expect(await browser.elementByCss('#performance-now').text()).toBe( currentTime ) await browser.elementByCss('#link').click() await browser.waitForElementByCss('#page') // eslint-disable-next-line jest/no-standalone-expect expect(await browser.elementByCss('#performance-now').text()).toBe( currentTime ) } ) }) describe('error component', () => { it('should trigger error component when an error happens during rendering', async () => { const browser = await next.browser('/error/client-component') await browser.elementByCss('#error-trigger-button').click() if (isDev) { // TODO: investigate desired behavior here as it is currently // minimized by default // expect(await hasRedbox(browser, true)).toBe(true) // expect(await getRedboxHeader(browser)).toMatch(/this is a test/) } else { await browser expect( await browser .waitForElementByCss('#error-boundary-message') .elementByCss('#error-boundary-message') .text() ).toBe('An error occurred: this is a test') } }) it('should trigger error component when an error happens during server components rendering', async () => { const browser = await next.browser('/error/server-component') if (isDev) { expect( await browser .waitForElementByCss('#error-boundary-message') .elementByCss('#error-boundary-message') .text() ).toBe('this is a test') expect( await browser.waitForElementByCss('#error-boundary-digest').text() // Digest of the error message should be stable. ).not.toBe('') // TODO-APP: ensure error overlay is shown for errors that happened before/during hydration // expect(await hasRedbox(browser, true)).toBe(true) // expect(await getRedboxHeader(browser)).toMatch(/this is a test/) } else { await browser expect( await browser.waitForElementByCss('#error-boundary-message').text() ).toBe( 'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.' ) expect( await browser.waitForElementByCss('#error-boundary-digest').text() // Digest of the error message should be stable. ).not.toBe('') } }) it('should use default error boundary for prod and overlay for dev when no error component specified', async () => { const browser = await next.browser( '/error/global-error-boundary/client' ) await browser.elementByCss('#error-trigger-button').click() if (isDev) { expect(await hasRedbox(browser, true)).toBe(true) expect(await getRedboxHeader(browser)).toMatch(/this is a test/) } else { expect( await browser.waitForElementByCss('body').elementByCss('h2').text() ).toBe( 'Application error: a client-side exception has occurred (see the browser console for more information).' ) } }) it('should display error digest for error in server component with default error boundary', async () => { const browser = await next.browser( '/error/global-error-boundary/server' ) if (isDev) { expect(await hasRedbox(browser, true)).toBe(true) expect(await getRedboxHeader(browser)).toMatch(/custom server error/) } else { expect( await browser.waitForElementByCss('body').elementByCss('h2').text() ).toBe( 'Application error: a client-side exception has occurred (see the browser console for more information).' ) expect( await browser.waitForElementByCss('body').elementByCss('p').text() ).toMatch(/Digest: \w+/) } }) if (!isDev) { it('should allow resetting error boundary', async () => { const browser = await next.browser('/error/client-component') // Try triggering and resetting a few times in a row for (let i = 0; i < 5; i++) { await browser .elementByCss('#error-trigger-button') .click() .waitForElementByCss('#error-boundary-message') expect( await browser.elementByCss('#error-boundary-message').text() ).toBe('An error occurred: this is a test') await browser .elementByCss('#reset') .click() .waitForElementByCss('#error-trigger-button') expect( await browser.elementByCss('#error-trigger-button').text() ).toBe('Trigger Error!') } }) it('should hydrate empty shell to handle server-side rendering errors', async () => { const browser = await next.browser( '/error/ssr-error-client-component' ) const logs = await browser.log() const errors = logs .filter((x) => x.source === 'error') .map((x) => x.message) .join('\n') expect(errors).toInclude('Error during SSR') }) } }) describe('known bugs', () => { describe('should support React cache', () => { it('server component', async () => { const browser = await next.browser('/react-cache/server-component') const val1 = await browser.elementByCss('#value-1').text() const val2 = await browser.elementByCss('#value-2').text() expect(val1).toBe(val2) }) it('server component client-navigation', async () => { const browser = await next.browser('/react-cache') await browser .elementByCss('#to-server-component') .click() .waitForElementByCss('#value-1', 10000) const val1 = await browser.elementByCss('#value-1').text() const val2 = await browser.elementByCss('#value-2').text() expect(val1).toBe(val2) }) it('client component', async () => { const browser = await next.browser('/react-cache/client-component') const val1 = await browser.elementByCss('#value-1').text() const val2 = await browser.elementByCss('#value-2').text() expect(val1).toBe(val2) }) it('client component client-navigation', async () => { const browser = await next.browser('/react-cache') await browser .elementByCss('#to-client-component') .click() .waitForElementByCss('#value-1', 10000) const val1 = await browser.elementByCss('#value-1').text() const val2 = await browser.elementByCss('#value-2').text() expect(val1).toBe(val2) }) it('middleware overriding headers', async () => { const browser = await next.browser('/searchparams-normalization-bug') await browser.eval(`window.didFullPageTransition = 'no'`) expect(await browser.elementByCss('#header-empty').text()).toBe( 'Header value: empty' ) expect( await browser .elementByCss('#button-a') .click() .waitForElementByCss('#header-a') .text() ).toBe('Header value: a') expect( await browser .elementByCss('#button-b') .click() .waitForElementByCss('#header-b') .text() ).toBe('Header value: b') expect( await browser .elementByCss('#button-c') .click() .waitForElementByCss('#header-c') .text() ).toBe('Header value: c') expect(await browser.eval(`window.didFullPageTransition`)).toBe('no') }) }) describe('should support React fetch instrumentation', () => { it('server component', async () => { const browser = await next.browser('/react-fetch/server-component') const val1 = await browser.elementByCss('#value-1').text() const val2 = await browser.elementByCss('#value-2').text() // TODO: enable when fetch cache is enabled in dev if (!isDev) { expect(val1).toBe(val2) } }) it('server component client-navigation', async () => { const browser = await next.browser('/react-fetch') await browser .elementByCss('#to-server-component') .click() .waitForElementByCss('#value-1', 10000) const val1 = await browser.elementByCss('#value-1').text() const val2 = await browser.elementByCss('#value-2').text() // TODO: enable when fetch cache is enabled in dev if (!isDev) { expect(val1).toBe(val2) } }) // TODO-APP: React doesn't have fetch deduping for client components yet. it.skip('client component', async () => { const browser = await next.browser('/react-fetch/client-component') const val1 = await browser.elementByCss('#value-1').text() const val2 = await browser.elementByCss('#value-2').text() expect(val1).toBe(val2) }) // TODO-APP: React doesn't have fetch deduping for client components yet. it.skip('client component client-navigation', async () => { const browser = await next.browser('/react-fetch') await browser .elementByCss('#to-client-component') .click() .waitForElementByCss('#value-1', 10000) const val1 = await browser.elementByCss('#value-1').text() const val2 = await browser.elementByCss('#value-2').text() expect(val1).toBe(val2) }) }) it('should not share flight data between requests', async () => { const fetches = await Promise.all( [...new Array(5)].map(() => next.render('/loading-bug/electronics')) ) for (const text of fetches) { const $ = cheerio.load(text) expect($('#category-id').text()).toBe('electronicsabc') } }) it('should handle router.refresh without resetting state', async () => { const browser = await next.browser( '/navigation/refresh/navigate-then-refresh-bug' ) await browser .elementByCss('#to-route') // Navigate to the page .click() // Wait for new page to be loaded .waitForElementByCss('#refresh-page') // Click the refresh button to trigger a refresh .click() // Wait for element that is shown when refreshed and verify text expect(await browser.waitForElementByCss('#refreshed').text()).toBe( 'Refreshed page successfully!' ) expect( await browser.eval( `window.getComputedStyle(document.querySelector('h1')).backgroundColor` ) ).toBe('rgb(34, 139, 34)') }) it('should handle as on next/link', async () => { const browser = await next.browser('/link-with-as') expect( await browser .elementByCss('#link-to-info-123') .click() .waitForElementByCss('#message') .text() ).toBe(`hello from app/dashboard/deployments/info/[id]. ID is: 123`) }) it('should handle next/link back to initially loaded page', async () => { const browser = await next.browser('/linking/about') expect( await browser .elementByCss('a[href="/linking"]') .click() .waitForElementByCss('#home-page') .text() ).toBe(`Home page`) expect( await browser .elementByCss('a[href="/linking/about"]') .click() .waitForElementByCss('#about-page') .text() ).toBe(`About page`) }) it('should not do additional pushState when already on the page', async () => { const browser = await next.browser('/linking/about') const goToLinkingPage = async () => { expect( await browser .elementByCss('a[href="/linking"]') .click() .waitForElementByCss('#home-page') .text() ).toBe(`Home page`) } await goToLinkingPage() await waitFor(1000) await goToLinkingPage() await waitFor(1000) await goToLinkingPage() await waitFor(1000) expect( await browser.back().waitForElementByCss('#about-page', 2000).text() ).toBe(`About page`) }) }) describe('next/script', () => { if (!isNextDeploy) { it('should support next/script and render in correct order', async () => { const browser = await next.browser('/script') // Wait for lazyOnload scripts to be ready. await check(async () => { expect(await browser.eval(`window._script_order`)).toStrictEqual([ 1, 1.5, 2, 2.5, 'render', 3, 4, ]) return 'yes' }, 'yes') }) } it('should insert preload tags for beforeInteractive and afterInteractive scripts', async () => { const html = await next.render('/script') expect(html).toContain( '' ) expect(html).toContain( '' ) expect(html).toContain( '' ) // test4.js has lazyOnload which doesn't need to be preloaded expect(html).not.toContain( '