import { createNext, FileRef } from 'e2e-utils' import crypto from 'crypto' import { NextInstance } from 'test/lib/next-modes/base' import { check, fetchViaHTTP, getRedboxHeader, hasRedbox, renderViaHTTP, waitFor } from 'next-test-utils' import path from 'path' import cheerio from 'cheerio' import webdriver from 'next-webdriver' describe('app dir', () => { const isDev = (global as any).isNextDev let next: NextInstance function runTests() { beforeAll(async () => { next = await createNext({ files: new FileRef(path.join(__dirname, 'app')), dependencies: { swr: '2.0.0-rc.0', react: 'latest', 'react-dom': 'latest', sass: 'latest', }, }) }, 600000) afterAll(async () => { await next.destroy() }) if (!(global as any).isNextDeploy) { it('should not share edge workers', async () => { const controller1 = new AbortController() const controller2 = new AbortController() fetchViaHTTP(next.url, '/slow-page-no-loading', undefined, { signal: controller1.signal, }).catch(() => {}) fetchViaHTTP(next.url, '/slow-page-no-loading', undefined, { signal: controller2.signal, }).catch(() => {}) await waitFor(1000) controller1.abort() const controller3 = new AbortController() fetchViaHTTP(next.url, '/slow-page-no-loading', undefined, { signal: controller3.signal, }).catch(() => {}) await waitFor(1000) controller2.abort() controller3.abort() const res = await fetchViaHTTP(next.url, '/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 ((global as any).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 application/octet-stream for flight', async () => { const res = await fetchViaHTTP( next.url, '/dashboard/deployments/123', {}, { headers: { ['RSC'.toString()]: '1', }, }, ) expect(res.headers.get('Content-Type')).toBe('application/octet-stream') }) it('should use application/octet-stream for flight with edge runtime', async () => { const res = await fetchViaHTTP( next.url, '/dashboard', {}, { headers: { ['RSC'.toString()]: '1', }, }, ) expect(res.headers.get('Content-Type')).toBe('application/octet-stream') }) it('should pass props from getServerSideProps in root layout', async () => { const html = await renderViaHTTP(next.url, '/dashboard') const $ = cheerio.load(html) expect($('title').text()).toBe('hello world') }) it('should serve from pages', async () => { const html = await renderViaHTTP(next.url, '/') expect(html).toContain('hello from pages/index') // esm imports should work fine in pages/ expect(html).toContain('swr-index') }) it('should serve dynamic route from pages', async () => { const html = await renderViaHTTP(next.url, '/blog/first') expect(html).toContain('hello from pages/blog/[slug]') }) it('should serve from public', async () => { const html = await renderViaHTTP(next.url, '/hello.txt') expect(html).toContain('hello world') }) it('should serve from app', async () => { const html = await renderViaHTTP(next.url, '/dashboard') expect(html).toContain('hello from app/dashboard') }) if (!(global as any).isNextDeploy) { it('should serve /index as separate page', async () => { const html = await renderViaHTTP(next.url, '/dashboard/index') expect(html).toContain('hello from app/dashboard/index') // should load chunks generated via async import correctly with React.lazy expect(html).toContain('hello from lazy') // should support `dynamic` in both server and client components expect(html).toContain('hello from dynamic on server') expect(html).toContain('hello from dynamic on client') }) it('should serve polyfills for browsers that do not support modules', async () => { const html = await renderViaHTTP(next.url, '/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 webdriver(next.url, '/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 webdriver(next.url, '/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 webdriver(next.url, '/error/client-component') await browser.elementByCss('#error-trigger-button').click() if (isDev) { expect(await hasRedbox(browser)).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 webdriver(next.url, '/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)).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 webdriver(next.url, '/error/global-error-boundary') await browser.elementByCss('#error-trigger-button').click() // .waitForElementByCss('body') if (isDev) { expect(await hasRedbox(browser)).toBe(true) console.log('getRedboxHeader', await getRedboxHeader(browser)) // expect(await getRedboxHeader(browser)).toMatch(/An error occurred: this is a test/) } else { expect(await browser.waitForElementByCss('body').elementByCss('body').text()).toBe( 'Application error: a client-side exception has occurred (see the browser console for more information).', ) } }) if (!isDev) { it('should allow resetting error boundary', async () => { const browser = await webdriver(next.url, '/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 webdriver(next.url, '/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 webdriver(next.url, '/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 webdriver(next.url, '/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 webdriver(next.url, '/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 webdriver(next.url, '/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) }) }) describe('should support React fetch instrumentation', () => { it('server component', async () => { const browser = await webdriver(next.url, '/react-fetch/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 webdriver(next.url, '/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() expect(val1).toBe(val2) }) // TODO-APP: React doesn't have fetch deduping for client components yet. it.skip('client component', async () => { const browser = await webdriver(next.url, '/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 webdriver(next.url, '/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(() => renderViaHTTP(next.url, '/loading-bug/electronics')), ) for (const text of fetches) { const $ = cheerio.load(text) expect($('#category-id').text()).toBe('electronicsabc') } }) it('should handle as on next/link', async () => { const browser = await webdriver(next.url, '/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 webdriver(next.url, '/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 webdriver(next.url, '/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('not-found', () => { it('should trigger not-found in a server component', async () => { const browser = await webdriver(next.url, '/not-found/servercomponent') expect(await browser.waitForElementByCss('#not-found-component').text()).toBe('Not Found!') expect(await browser.waitForElementByCss('meta[name="robots"]').getAttribute('content')).toBe('noindex') }) 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') }) 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.waitForElementByCss('meta[name="robots"]').getAttribute('content')).toBe('noindex') }) }) describe('bots', () => { if (!(global as any).isNextDeploy) { it('should block rendering for bots and return 404 status', async () => { const res = await fetchViaHTTP(next.url, '/not-found/servercomponent', '', { headers: { 'User-Agent': 'Googlebot', }, }) expect(res.status).toBe(404) expect(await res.text()).toInclude('"noindex"') }) } }) describe('redirect', () => { describe('components', () => { 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('Result Page') }) 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('Result Page') }) // TODO-APP: Enable in development 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') }) }) describe('next.config.js redirects', () => { it('should redirect from next.config.js', async () => { const browser = await webdriver(next.url, '/redirect/a') expect(await browser.elementByCss('h1').text()).toBe('Dashboard') expect(await browser.url()).toBe(next.url + '/dashboard') }) it('should redirect from next.config.js with link navigation', async () => { const browser = await webdriver(next.url, '/redirect/next-config-redirect') await browser.elementByCss('#redirect-a').click().waitForElementByCss('h1') expect(await browser.elementByCss('h1').text()).toBe('Dashboard') expect(await browser.url()).toBe(next.url + '/dashboard') }) }) describe('middleware redirects', () => { it('should redirect from middleware', async () => { const browser = await webdriver(next.url, '/redirect-middleware-to-dashboard') expect(await browser.elementByCss('h1').text()).toBe('Dashboard') expect(await browser.url()).toBe(next.url + '/dashboard') }) it('should redirect from middleware with link navigation', async () => { const browser = await webdriver(next.url, '/redirect/next-middleware-redirect') await browser.elementByCss('#redirect-middleware').click().waitForElementByCss('h1') expect(await browser.elementByCss('h1').text()).toBe('Dashboard') expect(await browser.url()).toBe(next.url + '/dashboard') }) }) }) describe('nested navigation', () => { it('should navigate to nested pages', async () => { const browser = await webdriver(next.url, '/nested-navigation') expect(await browser.elementByCss('h1').text()).toBe('Home') const pages = [ ['Electronics', ['Phones', 'Tablets', 'Laptops']], ['Clothing', ['Tops', 'Shorts', 'Shoes']], ['Books', ['Fiction', 'Biography', 'Education']], ] as const for (const [category, subCategories] of pages) { expect( await browser .elementByCss(`a[href="/nested-navigation/${category.toLowerCase()}"]`) .click() .waitForElementByCss(`#all-${category.toLowerCase()}`) .text(), ).toBe(`All ${category}`) for (const subcategory of subCategories) { expect( await browser .elementByCss(`a[href="/nested-navigation/${category.toLowerCase()}/${subcategory.toLowerCase()}"]`) .click() .waitForElementByCss(`#${subcategory.toLowerCase()}`) .text(), ).toBe(`${subcategory}`) } } }) }) describe('next/script', () => { if (!(global as any).isNextDeploy) { it('should support next/script and render in correct order', async () => { const browser = await webdriver(next.url, '/script') // Wait for lazyOnload scripts to be ready. await new Promise((resolve) => setTimeout(resolve, 1000)) expect(await browser.eval(`window._script_order`)).toStrictEqual([1, 1.5, 2, 2.5, 'render', 3, 4]) }) } it('should insert preload tags for beforeInteractive and afterInteractive scripts', async () => { const html = await renderViaHTTP(next.url, '/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('