diff --git a/test/development/acceptance-app/rsc-runtime-errors.test.ts b/test/development/acceptance-app/rsc-runtime-errors.test.ts index 644d297517..d11dbf4502 100644 --- a/test/development/acceptance-app/rsc-runtime-errors.test.ts +++ b/test/development/acceptance-app/rsc-runtime-errors.test.ts @@ -1,6 +1,6 @@ import path from 'path' import { outdent } from 'outdent' -import { FileRef, createNextDescribe } from 'e2e-utils' +import { FileRef, nextTestSetup } from 'e2e-utils' import { check, getRedboxDescription, @@ -10,41 +10,40 @@ import { retry, } from 'next-test-utils' -createNextDescribe( - 'Error overlay - RSC runtime errors', - { +describe('Error overlay - RSC runtime errors', () => { + const { next } = nextTestSetup({ files: new FileRef(path.join(__dirname, 'fixtures', 'rsc-runtime-errors')), - }, - ({ next }) => { - it('should show runtime errors if invalid client API from node_modules is executed', async () => { - await next.patchFile( - 'app/server/page.js', - outdent` + }) + + it('should show runtime errors if invalid client API from node_modules is executed', async () => { + await next.patchFile( + 'app/server/page.js', + outdent` import { callClientApi } from 'client-package' export default function Page() { callClientApi() return 'page' } ` - ) + ) - const browser = await next.browser('/server') + const browser = await next.browser('/server') - await check( - async () => ((await hasRedbox(browser)) ? 'success' : 'fail'), - /success/ - ) - const errorDescription = await getRedboxDescription(browser) + await check( + async () => ((await hasRedbox(browser)) ? 'success' : 'fail'), + /success/ + ) + const errorDescription = await getRedboxDescription(browser) - expect(errorDescription).toContain( - `Error: useState only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/react-client-hook-in-server-component` - ) - }) + expect(errorDescription).toContain( + `Error: useState only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/react-client-hook-in-server-component` + ) + }) - it('should show runtime errors if invalid server API from node_modules is executed', async () => { - await next.patchFile( - 'app/client/page.js', - outdent` + it('should show runtime errors if invalid server API from node_modules is executed', async () => { + await next.patchFile( + 'app/client/page.js', + outdent` 'use client' import { callServerApi } from 'server-package' export default function Page() { @@ -52,101 +51,100 @@ createNextDescribe( return 'page' } ` - ) + ) - const browser = await next.browser('/client') + const browser = await next.browser('/client') - await check( - async () => ((await hasRedbox(browser)) ? 'success' : 'fail'), - /success/ - ) - const errorDescription = await getRedboxDescription(browser) + await check( + async () => ((await hasRedbox(browser)) ? 'success' : 'fail'), + /success/ + ) + const errorDescription = await getRedboxDescription(browser) - expect(errorDescription).toContain( - 'Error: `cookies` was called outside a request scope. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context' - ) - }) + expect(errorDescription).toContain( + 'Error: `cookies` was called outside a request scope. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context' + ) + }) - it('should show source code for jsx errors from server component', async () => { - await next.patchFile( - 'app/server/page.js', - outdent` + it('should show source code for jsx errors from server component', async () => { + await next.patchFile( + 'app/server/page.js', + outdent` export default function Page() { return
{alert('warn')}
} ` - ) + ) - const browser = await next.browser('/server') - await check( - async () => ((await hasRedbox(browser)) ? 'success' : 'fail'), - /success/ - ) + const browser = await next.browser('/server') + await check( + async () => ((await hasRedbox(browser)) ? 'success' : 'fail'), + /success/ + ) - const errorDescription = await getRedboxDescription(browser) + const errorDescription = await getRedboxDescription(browser) - expect(errorDescription).toContain(`Error: alert is not defined`) - }) + expect(errorDescription).toContain(`Error: alert is not defined`) + }) - it('should show the userland code error trace when fetch failed error occurred', async () => { - await next.patchFile( - 'app/server/page.js', - outdent` + it('should show the userland code error trace when fetch failed error occurred', async () => { + await next.patchFile( + 'app/server/page.js', + outdent` export default async function Page() { await fetch('http://locahost:3000/xxxx') return 'page' } ` - ) - const browser = await next.browser('/server') - await check( - async () => ((await hasRedbox(browser)) ? 'success' : 'fail'), - /success/ - ) + ) + const browser = await next.browser('/server') + await check( + async () => ((await hasRedbox(browser)) ? 'success' : 'fail'), + /success/ + ) - const source = await getRedboxSource(browser) - // Can show the original source code - expect(source).toContain('app/server/page.js') - expect(source).toContain(`await fetch('http://locahost:3000/xxxx')`) - }) + const source = await getRedboxSource(browser) + // Can show the original source code + expect(source).toContain('app/server/page.js') + expect(source).toContain(`await fetch('http://locahost:3000/xxxx')`) + }) - it('should contain nextjs version check in error overlay', async () => { - await next.patchFile( - 'app/server/page.js', - outdent` + it('should contain nextjs version check in error overlay', async () => { + await next.patchFile( + 'app/server/page.js', + outdent` export default function Page() { throw new Error('test') } ` - ) - const browser = await next.browser('/server') + ) + const browser = await next.browser('/server') - await retry(async () => { - expect(await hasRedbox(browser)).toBe(true) - }) - const versionText = await getVersionCheckerText(browser) - await expect(versionText).toMatch(/Next.js \([\w.-]+\)/) + await retry(async () => { + expect(await hasRedbox(browser)).toBe(true) }) + const versionText = await getVersionCheckerText(browser) + await expect(versionText).toMatch(/Next.js \([\w.-]+\)/) + }) - it('should not show the bundle layer info in the file trace', async () => { - await next.patchFile( - 'app/server/page.js', - outdent` + it('should not show the bundle layer info in the file trace', async () => { + await next.patchFile( + 'app/server/page.js', + outdent` export default function Page() { throw new Error('test') } ` - ) - const browser = await next.browser('/server') + ) + const browser = await next.browser('/server') - await retry(async () => { - expect(await hasRedbox(browser)).toBe(true) - }) - const source = await getRedboxSource(browser) - expect(source).toContain('app/server/page.js') - expect(source).not.toContain('//app/server/page.js') - // Does not contain webpack traces in file path - expect(source).not.toMatch(/webpack(-internal:)?\/\//) + await retry(async () => { + expect(await hasRedbox(browser)).toBe(true) }) - } -) + const source = await getRedboxSource(browser) + expect(source).toContain('app/server/page.js') + expect(source).not.toContain('//app/server/page.js') + // Does not contain webpack traces in file path + expect(source).not.toMatch(/webpack(-internal:)?\/\//) + }) +}) diff --git a/test/development/acceptance/component-stack.test.ts b/test/development/acceptance/component-stack.test.ts index 65fca9d4c3..7553c7c63a 100644 --- a/test/development/acceptance/component-stack.test.ts +++ b/test/development/acceptance/component-stack.test.ts @@ -1,21 +1,20 @@ /* eslint-env jest */ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { getRedboxComponentStack, hasRedbox } from 'next-test-utils' import path from 'path' -createNextDescribe( - 'Component Stack in error overlay', - { +describe('Component Stack in error overlay', () => { + const { next } = nextTestSetup({ files: path.join(__dirname, 'fixtures', 'component-stack'), - }, - ({ next }) => { - it('should show a component stack on hydration error', async () => { - const browser = await next.browser('/') + }) - expect(await hasRedbox(browser)).toBe(true) + it('should show a component stack on hydration error', async () => { + const browser = await next.browser('/') - if (process.env.TURBOPACK) { - expect(await getRedboxComponentStack(browser)).toMatchInlineSnapshot(` + expect(await hasRedbox(browser)).toBe(true) + + if (process.env.TURBOPACK) { + expect(await getRedboxComponentStack(browser)).toMatchInlineSnapshot(` "... @@ -26,8 +25,8 @@ createNextDescribe( "server" "client"" `) - } else { - expect(await getRedboxComponentStack(browser)).toMatchInlineSnapshot(` + } else { + expect(await getRedboxComponentStack(browser)).toMatchInlineSnapshot(` "
@@ -36,7 +35,6 @@ createNextDescribe( "server" "client"" `) - } - }) - } -) + } + }) +}) diff --git a/test/development/app-dir/app-routes-error/index.test.ts b/test/development/app-dir/app-routes-error/index.test.ts index 8e7c6ec3e5..22221f4632 100644 --- a/test/development/app-dir/app-routes-error/index.test.ts +++ b/test/development/app-dir/app-routes-error/index.test.ts @@ -1,40 +1,38 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { check } from 'next-test-utils' -createNextDescribe( - 'app-dir - app routes errors', - { +describe('app-dir - app routes errors', () => { + const { next } = nextTestSetup({ files: __dirname, - }, - ({ next }) => { - describe('bad lowercase exports', () => { - it.each([ - ['get'], - ['head'], - ['options'], - ['post'], - ['put'], - ['delete'], - ['patch'], - ])( - 'should print an error when using lowercase %p in dev', - async (method: string) => { - await next.fetch('/lowercase/' + method) + }) - await check(() => { - expect(next.cliOutput).toContain( - `Detected lowercase method '${method}' in` - ) - expect(next.cliOutput).toContain( - `Export the uppercase '${method.toUpperCase()}' method name to fix this error.` - ) - expect(next.cliOutput).toMatch( - /Detected lowercase method '.+' in '.+\/route\.js'\. Export the uppercase '.+' method name to fix this error\./ - ) - return 'yes' - }, 'yes') - } - ) - }) - } -) + describe('bad lowercase exports', () => { + it.each([ + ['get'], + ['head'], + ['options'], + ['post'], + ['put'], + ['delete'], + ['patch'], + ])( + 'should print an error when using lowercase %p in dev', + async (method: string) => { + await next.fetch('/lowercase/' + method) + + await check(() => { + expect(next.cliOutput).toContain( + `Detected lowercase method '${method}' in` + ) + expect(next.cliOutput).toContain( + `Export the uppercase '${method.toUpperCase()}' method name to fix this error.` + ) + expect(next.cliOutput).toMatch( + /Detected lowercase method '.+' in '.+\/route\.js'\. Export the uppercase '.+' method name to fix this error\./ + ) + return 'yes' + }, 'yes') + } + ) + }) +}) diff --git a/test/development/app-dir/dynamic-error-trace/index.test.ts b/test/development/app-dir/dynamic-error-trace/index.test.ts index 607a92b769..6ec0d6191a 100644 --- a/test/development/app-dir/dynamic-error-trace/index.test.ts +++ b/test/development/app-dir/dynamic-error-trace/index.test.ts @@ -1,4 +1,4 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { getRedboxCallStack, hasRedbox, @@ -7,9 +7,8 @@ import { getRedboxSource, } from 'next-test-utils' -createNextDescribe( - 'app dir - dynamic error trace', - { +describe('app dir - dynamic error trace', () => { + const { next, skipped } = nextTestSetup({ files: __dirname, dependencies: { swr: 'latest', @@ -25,18 +24,18 @@ createNextDescribe( startCommand: (global as any).isNextDev ? 'pnpm dev' : 'pnpm start', buildCommand: 'pnpm build', skipDeployment: true, - }, - ({ next }) => { - it('should show the error trace', async () => { - const browser = await next.browser('/') - await hasRedbox(browser) - await expandCallStack(browser) - const callStack = await getRedboxCallStack(browser) + }) + if (skipped) return - expect(callStack).toContain('node_modules/headers-lib/index.mjs') + it('should show the error trace', async () => { + const browser = await next.browser('/') + await hasRedbox(browser) + await expandCallStack(browser) + const callStack = await getRedboxCallStack(browser) - const source = await getRedboxSource(browser) - expect(source).toContain('app/lib.js') - }) - } -) + expect(callStack).toContain('node_modules/headers-lib/index.mjs') + + const source = await getRedboxSource(browser) + expect(source).toContain('app/lib.js') + }) +}) diff --git a/test/development/app-dir/edge-errors-hmr/index.test.ts b/test/development/app-dir/edge-errors-hmr/index.test.ts index 756bc2c876..37ef617add 100644 --- a/test/development/app-dir/edge-errors-hmr/index.test.ts +++ b/test/development/app-dir/edge-errors-hmr/index.test.ts @@ -1,40 +1,38 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { hasRedbox } from 'next-test-utils' -createNextDescribe( - 'develop - app-dir - edge errros hmr', - { +describe('develop - app-dir - edge errros hmr', () => { + const { next } = nextTestSetup({ files: __dirname, - }, - ({ next }) => { - it('should recover from build errors when server component error', async () => { - const browser = await next.browser('/') - const clientComponentSource = await next.readFile('app/comp.server.js') + }) - await next.patchFile('app/comp.server.js', (content) => { - return content.replace('{/* < */}', '<') // uncomment - }) + it('should recover from build errors when server component error', async () => { + const browser = await next.browser('/') + const clientComponentSource = await next.readFile('app/comp.server.js') - expect(await hasRedbox(browser)).toBe(true) - - await next.patchFile('app/comp.server.js', clientComponentSource) - - expect(await hasRedbox(browser)).toBe(false) + await next.patchFile('app/comp.server.js', (content) => { + return content.replace('{/* < */}', '<') // uncomment }) - it('should recover from build errors when client component error', async () => { - const browser = await next.browser('/') - const clientComponentSource = await next.readFile('app/comp.client.js') + expect(await hasRedbox(browser)).toBe(true) - await next.patchFile('app/comp.client.js', (content) => { - return content.replace('{/* < */}', '<') // uncomment - }) + await next.patchFile('app/comp.server.js', clientComponentSource) - expect(await hasRedbox(browser)).toBe(true) + expect(await hasRedbox(browser)).toBe(false) + }) - await next.patchFile('app/comp.client.js', clientComponentSource) + it('should recover from build errors when client component error', async () => { + const browser = await next.browser('/') + const clientComponentSource = await next.readFile('app/comp.client.js') - expect(await hasRedbox(browser)).toBe(false) + await next.patchFile('app/comp.client.js', (content) => { + return content.replace('{/* < */}', '<') // uncomment }) - } -) + + expect(await hasRedbox(browser)).toBe(true) + + await next.patchFile('app/comp.client.js', clientComponentSource) + + expect(await hasRedbox(browser)).toBe(false) + }) +}) diff --git a/test/development/app-dir/hmr-move-file/hmr-move-file.test.ts b/test/development/app-dir/hmr-move-file/hmr-move-file.test.ts index d204c1d1b1..8e0c038b35 100644 --- a/test/development/app-dir/hmr-move-file/hmr-move-file.test.ts +++ b/test/development/app-dir/hmr-move-file/hmr-move-file.test.ts @@ -1,28 +1,26 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { hasRedbox } from 'next-test-utils' -createNextDescribe( - 'HMR Move File', - { +describe('HMR Move File', () => { + const { next } = nextTestSetup({ files: __dirname, - }, - ({ next }) => { - it('should work when moving a component to another directory', async () => { - const browser = await next.browser('/') - expect(await browser.elementByCss('#hello-world-button').text()).toBe( - 'hello world' - ) + }) - await next.renameFile('app/button.tsx', 'app/button2.tsx') - await next.patchFile('app/page.tsx', (content) => - content.replace('./button', './button2') - ) + it('should work when moving a component to another directory', async () => { + const browser = await next.browser('/') + expect(await browser.elementByCss('#hello-world-button').text()).toBe( + 'hello world' + ) - expect(await hasRedbox(browser)).toBe(false) + await next.renameFile('app/button.tsx', 'app/button2.tsx') + await next.patchFile('app/page.tsx', (content) => + content.replace('./button', './button2') + ) - expect(await browser.elementByCss('#hello-world-button').text()).toBe( - 'hello world' - ) - }) - } -) + expect(await hasRedbox(browser)).toBe(false) + + expect(await browser.elementByCss('#hello-world-button').text()).toBe( + 'hello world' + ) + }) +}) diff --git a/test/development/app-dir/multiple-compiles-single-route/multiple-compiles-single-route.test.ts b/test/development/app-dir/multiple-compiles-single-route/multiple-compiles-single-route.test.ts index b4a4090b06..2bd80803fa 100644 --- a/test/development/app-dir/multiple-compiles-single-route/multiple-compiles-single-route.test.ts +++ b/test/development/app-dir/multiple-compiles-single-route/multiple-compiles-single-route.test.ts @@ -1,20 +1,18 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' -createNextDescribe( - 'multiple-compiles-single-route', - { +describe('multiple-compiles-single-route', () => { + const { next } = nextTestSetup({ files: __dirname, - }, - ({ next }) => { - // Recommended for tests that check HTML. Cheerio is a HTML parser that has a jQuery like API. - it('should not compile additional matching paths', async () => { - const logs: string[] = [] - next.on('stdout', (log) => { - logs.push(log) - }) - await next.render('/about') - // Check if `/[slug]` is mentioned in the logs as being compiled - expect(logs.some((log) => log.includes('/[slug]'))).toBe(false) + }) + + // Recommended for tests that check HTML. Cheerio is a HTML parser that has a jQuery like API. + it('should not compile additional matching paths', async () => { + const logs: string[] = [] + next.on('stdout', (log) => { + logs.push(log) }) - } -) + await next.render('/about') + // Check if `/[slug]` is mentioned in the logs as being compiled + expect(logs.some((log) => log.includes('/[slug]'))).toBe(false) + }) +}) diff --git a/test/development/app-dir/strict-mode-enabled-by-default/strict-mode-enabled-by-default.test.ts b/test/development/app-dir/strict-mode-enabled-by-default/strict-mode-enabled-by-default.test.ts index 5f3d42c92f..95b88e6711 100644 --- a/test/development/app-dir/strict-mode-enabled-by-default/strict-mode-enabled-by-default.test.ts +++ b/test/development/app-dir/strict-mode-enabled-by-default/strict-mode-enabled-by-default.test.ts @@ -1,27 +1,24 @@ import { type BrowserInterface } from 'next-webdriver' -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { check } from 'next-test-utils' -createNextDescribe( - 'Strict Mode enabled by default', - { +describe('Strict Mode enabled by default', () => { + const { next } = nextTestSetup({ files: __dirname, - }, - ({ next }) => { - // experimental react is having issues with this use effect - // @acdlite will take a look - // TODO: remove this after react fixes the issue in experimental build. - if (process.env.__NEXT_EXPERIMENTAL_PPR) { - it('skip test for PPR', () => {}) - return - } - // Recommended for tests that need a full browser - it('should work using browser', async () => { - const browser: BrowserInterface = await next.browser('/') - await check(async () => { - const text = await browser.elementByCss('p').text() - return text === '2' ? 'success' : `failed: ${text}` - }, 'success') - }) + }) + // experimental react is having issues with this use effect + // @acdlite will take a look + // TODO: remove this after react fixes the issue in experimental build. + if (process.env.__NEXT_EXPERIMENTAL_PPR) { + it('skip test for PPR', () => {}) + return } -) + // Recommended for tests that need a full browser + it('should work using browser', async () => { + const browser: BrowserInterface = await next.browser('/') + await check(async () => { + const text = await browser.elementByCss('p').text() + return text === '2' ? 'success' : `failed: ${text}` + }, 'success') + }) +}) diff --git a/test/development/app-hmr/hmr.test.ts b/test/development/app-hmr/hmr.test.ts index e40bc09b45..18cc88166d 100644 --- a/test/development/app-hmr/hmr.test.ts +++ b/test/development/app-hmr/hmr.test.ts @@ -1,96 +1,94 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { check, waitFor } from 'next-test-utils' const envFile = '.env.development.local' -createNextDescribe( - `app-dir-hmr`, - { +describe(`app-dir-hmr`, () => { + const { next } = nextTestSetup({ files: __dirname, - }, - ({ next }) => { - describe('filesystem changes', () => { - it('should not continously poll when hitting a not found page', async () => { - let requestCount = 0 + }) - const browser = await next.browser('/does-not-exist', { - beforePageLoad(page) { - page.on('request', (request) => { - const url = new URL(request.url()) - if (url.pathname === '/does-not-exist') { - requestCount++ - } - }) - }, - }) - const body = await browser.elementByCss('body').text() - expect(body).toContain('404') + describe('filesystem changes', () => { + it('should not continously poll when hitting a not found page', async () => { + let requestCount = 0 - await waitFor(3000) - - expect(requestCount).toBe(1) + const browser = await next.browser('/does-not-exist', { + beforePageLoad(page) { + page.on('request', (request) => { + const url = new URL(request.url()) + if (url.pathname === '/does-not-exist') { + requestCount++ + } + }) + }, }) + const body = await browser.elementByCss('body').text() + expect(body).toContain('404') - it('should not break when renaming a folder', async () => { - const browser = await next.browser('/folder') - const text = await browser.elementByCss('h1').text() - expect(text).toBe('Hello') + await waitFor(3000) - // Rename folder - await next.renameFolder('app/folder', 'app/folder-renamed') - - try { - // Should be 404 in a few seconds - await check(async () => { - const body = await browser.elementByCss('body').text() - expect(body).toContain('404') - return 'success' - }, 'success') - - // The new page should be rendered - const newHTML = await next.render('/folder-renamed') - expect(newHTML).toContain('Hello') - } finally { - // Rename it back - await next.renameFolder('app/folder-renamed', 'app/folder') - } - }) - - it('should update server components pages when env files is changed (nodejs)', async () => { - const envContent = await next.readFile(envFile) - const browser = await next.browser('/env/node') - expect(await browser.elementByCss('p').text()).toBe('mac') - await next.patchFile(envFile, 'MY_DEVICE="ipad"') - - try { - await check(async () => { - expect(await browser.elementByCss('p').text()).toBe('ipad') - return 'success' - }, /success/) - } finally { - await next.patchFile(envFile, envContent) - } - }) - - it('should update server components pages when env files is changed (edge)', async () => { - const envContent = await next.readFile(envFile) - const browser = await next.browser('/env/edge') - expect(await browser.elementByCss('p').text()).toBe('mac') - await next.patchFile(envFile, 'MY_DEVICE="ipad"') - - try { - await check(async () => { - expect(await browser.elementByCss('p').text()).toBe('ipad') - return 'success' - }, /success/) - } finally { - await next.patchFile(envFile, envContent) - } - }) - - it('should have no unexpected action error for hmr', async () => { - expect(next.cliOutput).not.toContain('Unexpected action') - }) + expect(requestCount).toBe(1) }) - } -) + + it('should not break when renaming a folder', async () => { + const browser = await next.browser('/folder') + const text = await browser.elementByCss('h1').text() + expect(text).toBe('Hello') + + // Rename folder + await next.renameFolder('app/folder', 'app/folder-renamed') + + try { + // Should be 404 in a few seconds + await check(async () => { + const body = await browser.elementByCss('body').text() + expect(body).toContain('404') + return 'success' + }, 'success') + + // The new page should be rendered + const newHTML = await next.render('/folder-renamed') + expect(newHTML).toContain('Hello') + } finally { + // Rename it back + await next.renameFolder('app/folder-renamed', 'app/folder') + } + }) + + it('should update server components pages when env files is changed (nodejs)', async () => { + const envContent = await next.readFile(envFile) + const browser = await next.browser('/env/node') + expect(await browser.elementByCss('p').text()).toBe('mac') + await next.patchFile(envFile, 'MY_DEVICE="ipad"') + + try { + await check(async () => { + expect(await browser.elementByCss('p').text()).toBe('ipad') + return 'success' + }, /success/) + } finally { + await next.patchFile(envFile, envContent) + } + }) + + it('should update server components pages when env files is changed (edge)', async () => { + const envContent = await next.readFile(envFile) + const browser = await next.browser('/env/edge') + expect(await browser.elementByCss('p').text()).toBe('mac') + await next.patchFile(envFile, 'MY_DEVICE="ipad"') + + try { + await check(async () => { + expect(await browser.elementByCss('p').text()).toBe('ipad') + return 'success' + }, /success/) + } finally { + await next.patchFile(envFile, envContent) + } + }) + + it('should have no unexpected action error for hmr', async () => { + expect(next.cliOutput).not.toContain('Unexpected action') + }) + }) +}) diff --git a/test/development/app-render-error-log/app-render-error-log.test.ts b/test/development/app-render-error-log/app-render-error-log.test.ts index f587fbf9a1..517193371d 100644 --- a/test/development/app-render-error-log/app-render-error-log.test.ts +++ b/test/development/app-render-error-log/app-render-error-log.test.ts @@ -1,42 +1,39 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { check } from 'next-test-utils' -createNextDescribe( - 'app-render-error-log', - { +describe('app-render-error-log', () => { + const { next } = nextTestSetup({ files: __dirname, - }, - ({ next }) => { - it('should log the correct values on app-render error', async () => { - const outputIndex = next.cliOutput.length - await next.fetch('/') + }) + it('should log the correct values on app-render error', async () => { + const outputIndex = next.cliOutput.length + await next.fetch('/') - await check(() => next.cliOutput.slice(outputIndex), /at Page/) - const cliOutput = next.cliOutput.slice(outputIndex) + await check(() => next.cliOutput.slice(outputIndex), /at Page/) + const cliOutput = next.cliOutput.slice(outputIndex) - await check(() => cliOutput, /digest:/) - expect(cliOutput).toInclude('Error: boom') - expect(cliOutput).toInclude('at fn2 (./app/fn.ts') - expect(cliOutput).toMatch(/at (Module\.)?fn1 \(\.\/app\/fn\.ts/) - expect(cliOutput).toInclude('at Page (./app/page.tsx') + await check(() => cliOutput, /digest:/) + expect(cliOutput).toInclude('Error: boom') + expect(cliOutput).toInclude('at fn2 (./app/fn.ts') + expect(cliOutput).toMatch(/at (Module\.)?fn1 \(\.\/app\/fn\.ts/) + expect(cliOutput).toInclude('at Page (./app/page.tsx') - expect(cliOutput).not.toInclude('webpack-internal') - }) + expect(cliOutput).not.toInclude('webpack-internal') + }) - it('should log the correct values on app-render error with edge runtime', async () => { - const outputIndex = next.cliOutput.length - await next.fetch('/edge') + it('should log the correct values on app-render error with edge runtime', async () => { + const outputIndex = next.cliOutput.length + await next.fetch('/edge') - await check(() => next.cliOutput.slice(outputIndex), /at EdgePage/) - const cliOutput = next.cliOutput.slice(outputIndex) + await check(() => next.cliOutput.slice(outputIndex), /at EdgePage/) + const cliOutput = next.cliOutput.slice(outputIndex) - await check(() => cliOutput, /digest:/) - expect(cliOutput).toInclude('Error: boom') - expect(cliOutput).toInclude('at fn2 (./app/fn.ts') - expect(cliOutput).toMatch(/at (Module\.)?fn1 \(\.\/app\/fn\.ts/) - expect(cliOutput).toInclude('at EdgePage (./app/edge/page.tsx') + await check(() => cliOutput, /digest:/) + expect(cliOutput).toInclude('Error: boom') + expect(cliOutput).toInclude('at fn2 (./app/fn.ts') + expect(cliOutput).toMatch(/at (Module\.)?fn1 \(\.\/app\/fn\.ts/) + expect(cliOutput).toInclude('at EdgePage (./app/edge/page.tsx') - expect(cliOutput).not.toInclude('webpack-internal') - }) - } -) + expect(cliOutput).not.toInclude('webpack-internal') + }) +}) diff --git a/test/development/basic/asset-prefix/asset-prefix.test.ts b/test/development/basic/asset-prefix/asset-prefix.test.ts index 936314f535..80a52e5c33 100644 --- a/test/development/basic/asset-prefix/asset-prefix.test.ts +++ b/test/development/basic/asset-prefix/asset-prefix.test.ts @@ -1,28 +1,26 @@ import { join } from 'path' -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { check } from 'next-test-utils' -createNextDescribe( - 'asset-prefix', - { +describe('asset-prefix', () => { + const { next } = nextTestSetup({ files: join(__dirname, 'fixture'), - }, - ({ next }) => { - it('should load the app properly without reloading', async () => { - const browser = await next.browser('/') - await browser.eval(`window.__v = 1`) + }) - expect(await browser.elementByCss('div').text()).toBe('Hello World') + it('should load the app properly without reloading', async () => { + const browser = await next.browser('/') + await browser.eval(`window.__v = 1`) - await check(async () => { - const logs = await browser.log() - const hasError = logs.some((log) => - log.message.includes('Failed to fetch') - ) - return hasError ? 'error' : 'success' - }, 'success') + expect(await browser.elementByCss('div').text()).toBe('Hello World') - expect(await browser.eval(`window.__v`)).toBe(1) - }) - } -) + await check(async () => { + const logs = await browser.log() + const hasError = logs.some((log) => + log.message.includes('Failed to fetch') + ) + return hasError ? 'error' : 'success' + }, 'success') + + expect(await browser.eval(`window.__v`)).toBe(1) + }) +}) diff --git a/test/development/basic/barrel-optimization/barrel-optimization-mui.test.ts b/test/development/basic/barrel-optimization/barrel-optimization-mui.test.ts index 054a7c8ca8..c6ba2ab60e 100644 --- a/test/development/basic/barrel-optimization/barrel-optimization-mui.test.ts +++ b/test/development/basic/barrel-optimization/barrel-optimization-mui.test.ts @@ -1,13 +1,13 @@ import { join } from 'path' -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { hasRedbox } from 'next-test-utils' + // Skipped in Turbopack, will be added later. ;(process.env.TURBOPACK ? describe.skip : describe)( 'Skipped in Turbopack', () => { - createNextDescribe( - 'optimizePackageImports - mui', - { + describe('optimizePackageImports - mui', () => { + const { next } = nextTestSetup({ env: { NEXT_TEST_MODE: '1', }, @@ -18,31 +18,30 @@ import { hasRedbox } from 'next-test-utils' '@emotion/react': '11.11.1', '@emotion/styled': '11.11.0', }, - }, - ({ next }) => { - it('should support MUI', async () => { - let logs = '' - next.on('stdout', (log) => { - logs += log - }) + }) - // Ensure that MUI is working - const $ = await next.render$('/mui') - expect(await $('#button').text()).toContain('button') - expect(await $('#typography').text()).toContain('typography') - - const browser = await next.browser('/mui') - expect(await hasRedbox(browser)).toBe(false) - - const modules = [...logs.matchAll(/\((\d+) modules\)/g)] - expect(modules.length).toBeGreaterThanOrEqual(1) - for (const [, moduleCount] of modules) { - // Ensure that the number of modules is less than 1500 - otherwise we're - // importing the entire library. - expect(parseInt(moduleCount)).toBeLessThan(1500) - } + it('should support MUI', async () => { + let logs = '' + next.on('stdout', (log) => { + logs += log }) - } - ) + + // Ensure that MUI is working + const $ = await next.render$('/mui') + expect(await $('#button').text()).toContain('button') + expect(await $('#typography').text()).toContain('typography') + + const browser = await next.browser('/mui') + expect(await hasRedbox(browser)).toBe(false) + + const modules = [...logs.matchAll(/\((\d+) modules\)/g)] + expect(modules.length).toBeGreaterThanOrEqual(1) + for (const [, moduleCount] of modules) { + // Ensure that the number of modules is less than 1500 - otherwise we're + // importing the entire library. + expect(parseInt(moduleCount)).toBeLessThan(1500) + } + }) + }) } ) diff --git a/test/development/basic/barrel-optimization/barrel-optimization-tremor.test.ts b/test/development/basic/barrel-optimization/barrel-optimization-tremor.test.ts index dbe3c32246..442bc481fc 100644 --- a/test/development/basic/barrel-optimization/barrel-optimization-tremor.test.ts +++ b/test/development/basic/barrel-optimization/barrel-optimization-tremor.test.ts @@ -1,10 +1,9 @@ import { join } from 'path' -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { hasRedbox } from 'next-test-utils' -createNextDescribe( - 'optimizePackageImports - mui', - { +describe('optimizePackageImports - mui', () => { + const { next } = nextTestSetup({ env: { NEXT_TEST_MODE: '1', }, @@ -16,12 +15,11 @@ createNextDescribe( '@remixicon/react': '^4.2.0', '@tremor/react': '^3.14.1', }, - }, - ({ next }) => { - it('should work', async () => { - // Without barrel optimization, the reproduction breaks. - const browser = await next.browser('/') - expect(await hasRedbox(browser)).toBe(false) - }) - } -) + }) + + it('should work', async () => { + // Without barrel optimization, the reproduction breaks. + const browser = await next.browser('/') + expect(await hasRedbox(browser)).toBe(false) + }) +}) diff --git a/test/development/basic/barrel-optimization/barrel-optimization.test.ts b/test/development/basic/barrel-optimization/barrel-optimization.test.ts index 60ee953b56..ef263753b6 100644 --- a/test/development/basic/barrel-optimization/barrel-optimization.test.ts +++ b/test/development/basic/barrel-optimization/barrel-optimization.test.ts @@ -1,12 +1,11 @@ import { join } from 'path' -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' // Skipped in Turbopack, will be added later. ;(process.env.TURBOPACK ? describe.skip : describe)( 'Skipped in Turbopack', () => { - createNextDescribe( - 'optimizePackageImports - basic', - { + describe('optimizePackageImports - basic', () => { + const { next } = nextTestSetup({ env: { NEXT_TEST_MODE: '1', }, @@ -18,114 +17,113 @@ import { createNextDescribe } from 'e2e-utils' '@visx/visx': '3.3.0', 'recursive-barrel': '1.0.0', }, - }, - ({ next }) => { - it('app - should render the icons correctly without creating all the modules', async () => { - let logs = '' - next.on('stdout', (log) => { - logs += log - }) + }) - const html = await next.render('/') - - // Ensure the icons are rendered - expect(html).toContain(' { + let logs = '' + next.on('stdout', (log) => { + logs += log }) - it('pages - should render the icons correctly without creating all the modules', async () => { - let logs = '' - next.on('stdout', (log) => { - logs += log - }) + const html = await next.render('/') - const html = await next.render('/pages-route') + // Ensure the icons are rendered + expect(html).toContain(' { + let logs = '' + next.on('stdout', (log) => { + logs += log }) - it('app - should optimize recursive wildcard export mapping', async () => { - let logs = '' - next.on('stdout', (log) => { - logs += log - }) + const html = await next.render('/pages-route') - await next.render('/recursive-barrel-app') + // Ensure the icons are rendered + expect(html).toContain(' { + let logs = '' + next.on('stdout', (log) => { + logs += log }) - it('pages - should optimize recursive wildcard export mapping', async () => { - let logs = '' - next.on('stdout', (log) => { - logs += log - }) + await next.render('/recursive-barrel-app') - await next.render('/recursive-barrel') + const modules = [...logs.matchAll(/\((\d+) modules\)/g)] - const modules = [...logs.matchAll(/\((\d+) modules\)/g)] + expect(modules.length).toBeGreaterThanOrEqual(1) + for (const [, moduleCount] of modules) { + // Ensure that the number of modules is less than 1000 - otherwise we're + // importing the entire library. + expect(parseInt(moduleCount)).toBeLessThan(1000) + } + }) - expect(modules.length).toBeGreaterThanOrEqual(1) - for (const [, moduleCount] of modules) { - // Ensure that the number of modules is less than 1000 - otherwise we're - // importing the entire library. - expect(parseInt(moduleCount)).toBeLessThan(1000) - } + it('pages - should optimize recursive wildcard export mapping', async () => { + let logs = '' + next.on('stdout', (log) => { + logs += log }) - it('should handle recursive wildcard exports', async () => { - const html = await next.render('/recursive') - expect(html).toContain('

42

') - }) + await next.render('/recursive-barrel') - it('should support visx', async () => { - const html = await next.render('/visx') - expect(html).toContain(' { - const html = await next.render('/client') - expect(html).toContain('this is a client component') - }) + expect(modules.length).toBeGreaterThanOrEqual(1) + for (const [, moduleCount] of modules) { + // Ensure that the number of modules is less than 1000 - otherwise we're + // importing the entire library. + expect(parseInt(moduleCount)).toBeLessThan(1000) + } + }) - it('should support "use client" directive in barrel file', async () => { - const html = await next.render('/client-boundary') - expect(html).toContain('') - }) - } - ) + it('should handle recursive wildcard exports', async () => { + const html = await next.render('/recursive') + expect(html).toContain('

42

') + }) + + it('should support visx', async () => { + const html = await next.render('/visx') + expect(html).toContain(' { + const html = await next.render('/client') + expect(html).toContain('this is a client component') + }) + + it('should support "use client" directive in barrel file', async () => { + const html = await next.render('/client-boundary') + expect(html).toContain('') + }) + }) } ) diff --git a/test/development/basic/define-class-fields/define-class-fields.test.ts b/test/development/basic/define-class-fields/define-class-fields.test.ts index 9b8f78bb4f..837a9cf3e2 100644 --- a/test/development/basic/define-class-fields/define-class-fields.test.ts +++ b/test/development/basic/define-class-fields/define-class-fields.test.ts @@ -1,11 +1,10 @@ import { join } from 'path' import webdriver from 'next-webdriver' -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { check } from 'next-test-utils' -createNextDescribe( - 'useDefineForClassFields SWC option', - { +describe('useDefineForClassFields SWC option', () => { + const { next } = nextTestSetup({ files: join(__dirname, 'fixture'), dependencies: { mobx: '6.3.7', @@ -14,67 +13,66 @@ createNextDescribe( '@types/node': 'latest', 'mobx-react': '7.2.1', }, - }, - ({ next }) => { - it('tsx should compile with useDefineForClassFields enabled', async () => { - let browser - try { - browser = await webdriver(next.url, '/') - await browser.elementByCss('#action').click() - await check( - () => browser.elementByCss('#name').text(), - /this is my name: next/ - ) - } finally { - if (browser) { - await browser.close() - } + }) + + it('tsx should compile with useDefineForClassFields enabled', async () => { + let browser + try { + browser = await webdriver(next.url, '/') + await browser.elementByCss('#action').click() + await check( + () => browser.elementByCss('#name').text(), + /this is my name: next/ + ) + } finally { + if (browser) { + await browser.close() } - }) - - it("Initializes resident to undefined after the call to 'super()' when with useDefineForClassFields enabled", async () => { - let browser - try { - browser = await webdriver(next.url, '/animal') - expect(await browser.elementByCss('#dog').text()).toBe('') - expect(await browser.elementByCss('#dogDecl').text()).toBe('dog') - } finally { - if (browser) { - await browser.close() - } - } - }) - - async function matchLogs$(browser) { - let data_foundLog = false - let name_foundLog = false - - const browserLogs = await browser.log('browser') - - browserLogs.forEach((log) => { - if (log.message.includes('data changed')) { - data_foundLog = true - } - if (log.message.includes('name changed')) { - name_foundLog = true - } - }) - return [data_foundLog, name_foundLog] } + }) - it('set accessors from base classes won’t get triggered with useDefineForClassFields enabled', async () => { - let browser - try { - browser = await webdriver(next.url, '/derived') - await matchLogs$(browser).then(([data_foundLog, name_foundLog]) => { - expect(data_foundLog).toBe(true) - expect(name_foundLog).toBe(false) - }) - } finally { - if (browser) { - await browser.close() - } + it("Initializes resident to undefined after the call to 'super()' when with useDefineForClassFields enabled", async () => { + let browser + try { + browser = await webdriver(next.url, '/animal') + expect(await browser.elementByCss('#dog').text()).toBe('') + expect(await browser.elementByCss('#dogDecl').text()).toBe('dog') + } finally { + if (browser) { + await browser.close() + } + } + }) + + async function matchLogs$(browser) { + let data_foundLog = false + let name_foundLog = false + + const browserLogs = await browser.log('browser') + + browserLogs.forEach((log) => { + if (log.message.includes('data changed')) { + data_foundLog = true + } + if (log.message.includes('name changed')) { + name_foundLog = true } }) + return [data_foundLog, name_foundLog] } -) + + it('set accessors from base classes won’t get triggered with useDefineForClassFields enabled', async () => { + let browser + try { + browser = await webdriver(next.url, '/derived') + await matchLogs$(browser).then(([data_foundLog, name_foundLog]) => { + expect(data_foundLog).toBe(true) + expect(name_foundLog).toBe(false) + }) + } finally { + if (browser) { + await browser.close() + } + } + }) +}) diff --git a/test/development/basic/emotion-swc/emotion-swc.test.ts b/test/development/basic/emotion-swc/emotion-swc.test.ts index 69eed995d7..4503c45a32 100644 --- a/test/development/basic/emotion-swc/emotion-swc.test.ts +++ b/test/development/basic/emotion-swc/emotion-swc.test.ts @@ -1,24 +1,22 @@ import { join } from 'path' -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' -createNextDescribe( - 'emotion SWC option', - { +describe('emotion SWC option', () => { + const { next } = nextTestSetup({ files: join(__dirname, 'fixture'), dependencies: { '@emotion/react': '11.10.4', '@emotion/styled': '11.10.4', '@emotion/cache': '11.10.3', }, - }, - ({ next }) => { - it('should have styling from the css prop', async () => { - const browser = await next.browser('/') + }) - const color = await browser - .elementByCss('#test-element') - .getComputedCss('background-color') - expect(color).toBe('rgb(255, 192, 203)') - }) - } -) + it('should have styling from the css prop', async () => { + const browser = await next.browser('/') + + const color = await browser + .elementByCss('#test-element') + .getComputedCss('background-color') + expect(color).toBe('rgb(255, 192, 203)') + }) +}) diff --git a/test/development/basic/node-builtins.test.ts b/test/development/basic/node-builtins.test.ts index 982fa6c391..96a3ce3cae 100644 --- a/test/development/basic/node-builtins.test.ts +++ b/test/development/basic/node-builtins.test.ts @@ -1,172 +1,170 @@ import { join } from 'path' -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' -createNextDescribe( - 'node builtins', - { +describe('node builtins', () => { + const { next } = nextTestSetup({ files: join(__dirname, 'node-builtins'), - }, - ({ next }) => { - it('should have polyfilled node.js builtins for the browser correctly', async () => { - const browser = await next.browser('/') + }) - await browser.waitForCondition('window.didRender', 15000) + it('should have polyfilled node.js builtins for the browser correctly', async () => { + const browser = await next.browser('/') - const data = await browser - .waitForElementByCss('#node-browser-polyfills') - .text() - const parsedData = JSON.parse(data) + await browser.waitForCondition('window.didRender', 15000) - expect(parsedData.vm).toBe(105) - expect(parsedData.hash).toBe( - 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' - ) - expect(parsedData.buffer).toBe('hello world') - expect(parsedData.stream).toBe(true) - expect(parsedData.assert).toBe(true) - expect(parsedData.constants).toBe(7) - expect(parsedData.domain).toBe(true) - expect(parsedData.http).toBe(true) - expect(parsedData.https).toBe(true) - expect(parsedData.os).toBe('\n') - expect(parsedData.path).toBe('/hello/world/test.txt') - expect(parsedData.process).toBe('browser') - expect(parsedData.querystring).toBe('a=b') - expect(parsedData.stringDecoder).toBe(true) - expect(parsedData.sys).toBe(true) - expect(parsedData.timers).toBe(true) - }) + const data = await browser + .waitForElementByCss('#node-browser-polyfills') + .text() + const parsedData = JSON.parse(data) - it('should have polyfilled node.js builtins for the browser correctly in client component', async () => { - const browser = await next.browser('/client-component') + expect(parsedData.vm).toBe(105) + expect(parsedData.hash).toBe( + 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' + ) + expect(parsedData.buffer).toBe('hello world') + expect(parsedData.stream).toBe(true) + expect(parsedData.assert).toBe(true) + expect(parsedData.constants).toBe(7) + expect(parsedData.domain).toBe(true) + expect(parsedData.http).toBe(true) + expect(parsedData.https).toBe(true) + expect(parsedData.os).toBe('\n') + expect(parsedData.path).toBe('/hello/world/test.txt') + expect(parsedData.process).toBe('browser') + expect(parsedData.querystring).toBe('a=b') + expect(parsedData.stringDecoder).toBe(true) + expect(parsedData.sys).toBe(true) + expect(parsedData.timers).toBe(true) + }) - await browser.waitForCondition('window.didRender', 15000) + it('should have polyfilled node.js builtins for the browser correctly in client component', async () => { + const browser = await next.browser('/client-component') - const data = await browser - .waitForElementByCss('#node-browser-polyfills') - .text() - const parsedData = JSON.parse(data) + await browser.waitForCondition('window.didRender', 15000) - expect(parsedData.vm).toBe(105) - expect(parsedData.hash).toBe( - 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' - ) - expect(parsedData.buffer).toBe('hello world') - expect(parsedData.stream).toBe(true) - expect(parsedData.assert).toBe(true) - expect(parsedData.constants).toBe(7) - expect(parsedData.domain).toBe(true) - expect(parsedData.http).toBe(true) - expect(parsedData.https).toBe(true) - expect(parsedData.os).toBe('\n') - expect(parsedData.path).toBe('/hello/world/test.txt') - expect(parsedData.process).toBe('browser') - expect(parsedData.querystring).toBe('a=b') - expect(parsedData.stringDecoder).toBe(true) - expect(parsedData.sys).toBe(true) - expect(parsedData.timers).toBe(true) - }) + const data = await browser + .waitForElementByCss('#node-browser-polyfills') + .text() + const parsedData = JSON.parse(data) - it('should support node.js builtins', async () => { - const $ = await next.render$('/server') + expect(parsedData.vm).toBe(105) + expect(parsedData.hash).toBe( + 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' + ) + expect(parsedData.buffer).toBe('hello world') + expect(parsedData.stream).toBe(true) + expect(parsedData.assert).toBe(true) + expect(parsedData.constants).toBe(7) + expect(parsedData.domain).toBe(true) + expect(parsedData.http).toBe(true) + expect(parsedData.https).toBe(true) + expect(parsedData.os).toBe('\n') + expect(parsedData.path).toBe('/hello/world/test.txt') + expect(parsedData.process).toBe('browser') + expect(parsedData.querystring).toBe('a=b') + expect(parsedData.stringDecoder).toBe(true) + expect(parsedData.sys).toBe(true) + expect(parsedData.timers).toBe(true) + }) - const data = $('#node-browser-polyfills').text() - const parsedData = JSON.parse(data) + it('should support node.js builtins', async () => { + const $ = await next.render$('/server') - expect(parsedData.vm).toBe(105) - expect(parsedData.hash).toBe( - 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' - ) - expect(parsedData.buffer).toBe('hello world') - expect(parsedData.stream).toBe(true) - expect(parsedData.assert).toBe(true) - expect(parsedData.constants).toBe(7) - expect(parsedData.domain).toBe(true) - expect(parsedData.http).toBe(true) - expect(parsedData.https).toBe(true) - expect(parsedData.os).toBe('\n') - expect(parsedData.path).toBe('/hello/world/test.txt') - expect(parsedData.process).toInclude('next-server') - expect(parsedData.querystring).toBe('a=b') - expect(parsedData.stringDecoder).toBe(true) - expect(parsedData.sys).toBe(true) - expect(parsedData.timers).toBe(true) - }) + const data = $('#node-browser-polyfills').text() + const parsedData = JSON.parse(data) - it('should support node.js builtins prefixed by node:', async () => { - const $ = await next.render$('/server-node-schema') + expect(parsedData.vm).toBe(105) + expect(parsedData.hash).toBe( + 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' + ) + expect(parsedData.buffer).toBe('hello world') + expect(parsedData.stream).toBe(true) + expect(parsedData.assert).toBe(true) + expect(parsedData.constants).toBe(7) + expect(parsedData.domain).toBe(true) + expect(parsedData.http).toBe(true) + expect(parsedData.https).toBe(true) + expect(parsedData.os).toBe('\n') + expect(parsedData.path).toBe('/hello/world/test.txt') + expect(parsedData.process).toInclude('next-server') + expect(parsedData.querystring).toBe('a=b') + expect(parsedData.stringDecoder).toBe(true) + expect(parsedData.sys).toBe(true) + expect(parsedData.timers).toBe(true) + }) - const data = $('#node-browser-polyfills').text() - const parsedData = JSON.parse(data) + it('should support node.js builtins prefixed by node:', async () => { + const $ = await next.render$('/server-node-schema') - expect(parsedData.vm).toBe(105) - expect(parsedData.hash).toBe( - 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' - ) - expect(parsedData.buffer).toBe('hello world') - expect(parsedData.stream).toBe(true) - expect(parsedData.assert).toBe(true) - expect(parsedData.constants).toBe(7) - expect(parsedData.domain).toBe(true) - expect(parsedData.http).toBe(true) - expect(parsedData.https).toBe(true) - expect(parsedData.os).toBe('\n') - expect(parsedData.path).toBe('/hello/world/test.txt') - expect(parsedData.process).toInclude('next-server') - expect(parsedData.querystring).toBe('a=b') - expect(parsedData.stringDecoder).toBe(true) - expect(parsedData.sys).toBe(true) - expect(parsedData.timers).toBe(true) - }) + const data = $('#node-browser-polyfills').text() + const parsedData = JSON.parse(data) - it('should support node.js builtins in server component', async () => { - const $ = await next.render$('/server-component') + expect(parsedData.vm).toBe(105) + expect(parsedData.hash).toBe( + 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' + ) + expect(parsedData.buffer).toBe('hello world') + expect(parsedData.stream).toBe(true) + expect(parsedData.assert).toBe(true) + expect(parsedData.constants).toBe(7) + expect(parsedData.domain).toBe(true) + expect(parsedData.http).toBe(true) + expect(parsedData.https).toBe(true) + expect(parsedData.os).toBe('\n') + expect(parsedData.path).toBe('/hello/world/test.txt') + expect(parsedData.process).toInclude('next-server') + expect(parsedData.querystring).toBe('a=b') + expect(parsedData.stringDecoder).toBe(true) + expect(parsedData.sys).toBe(true) + expect(parsedData.timers).toBe(true) + }) - const data = $('#node-browser-polyfills').text() - const parsedData = JSON.parse(data) + it('should support node.js builtins in server component', async () => { + const $ = await next.render$('/server-component') - expect(parsedData.vm).toBe(105) - expect(parsedData.hash).toBe( - 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' - ) - expect(parsedData.buffer).toBe('hello world') - expect(parsedData.stream).toBe(true) - expect(parsedData.assert).toBe(true) - expect(parsedData.constants).toBe(7) - expect(parsedData.domain).toBe(true) - expect(parsedData.http).toBe(true) - expect(parsedData.https).toBe(true) - expect(parsedData.os).toBe('\n') - expect(parsedData.path).toBe('/hello/world/test.txt') - expect(parsedData.querystring).toBe('a=b') - expect(parsedData.stringDecoder).toBe(true) - expect(parsedData.sys).toBe(true) - expect(parsedData.timers).toBe(true) - }) + const data = $('#node-browser-polyfills').text() + const parsedData = JSON.parse(data) - it('should support node.js builtins prefixed by node: in server component', async () => { - const $ = await next.render$('/server-component/node-schema') + expect(parsedData.vm).toBe(105) + expect(parsedData.hash).toBe( + 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' + ) + expect(parsedData.buffer).toBe('hello world') + expect(parsedData.stream).toBe(true) + expect(parsedData.assert).toBe(true) + expect(parsedData.constants).toBe(7) + expect(parsedData.domain).toBe(true) + expect(parsedData.http).toBe(true) + expect(parsedData.https).toBe(true) + expect(parsedData.os).toBe('\n') + expect(parsedData.path).toBe('/hello/world/test.txt') + expect(parsedData.querystring).toBe('a=b') + expect(parsedData.stringDecoder).toBe(true) + expect(parsedData.sys).toBe(true) + expect(parsedData.timers).toBe(true) + }) - const data = $('#node-browser-polyfills').text() - const parsedData = JSON.parse(data) + it('should support node.js builtins prefixed by node: in server component', async () => { + const $ = await next.render$('/server-component/node-schema') - expect(parsedData.vm).toBe(105) - expect(parsedData.hash).toBe( - 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' - ) - expect(parsedData.buffer).toBe('hello world') - expect(parsedData.stream).toBe(true) - expect(parsedData.assert).toBe(true) - expect(parsedData.constants).toBe(7) - expect(parsedData.domain).toBe(true) - expect(parsedData.http).toBe(true) - expect(parsedData.https).toBe(true) - expect(parsedData.os).toBe('\n') - expect(parsedData.path).toBe('/hello/world/test.txt') - expect(parsedData.querystring).toBe('a=b') - expect(parsedData.stringDecoder).toBe(true) - expect(parsedData.sys).toBe(true) - expect(parsedData.timers).toBe(true) - }) - } -) + const data = $('#node-browser-polyfills').text() + const parsedData = JSON.parse(data) + + expect(parsedData.vm).toBe(105) + expect(parsedData.hash).toBe( + 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' + ) + expect(parsedData.buffer).toBe('hello world') + expect(parsedData.stream).toBe(true) + expect(parsedData.assert).toBe(true) + expect(parsedData.constants).toBe(7) + expect(parsedData.domain).toBe(true) + expect(parsedData.http).toBe(true) + expect(parsedData.https).toBe(true) + expect(parsedData.os).toBe('\n') + expect(parsedData.path).toBe('/hello/world/test.txt') + expect(parsedData.querystring).toBe('a=b') + expect(parsedData.stringDecoder).toBe(true) + expect(parsedData.sys).toBe(true) + expect(parsedData.timers).toBe(true) + }) +}) diff --git a/test/development/basic/styled-components/styled-components.test.ts b/test/development/basic/styled-components/styled-components.test.ts index 32fc2593b1..a74aba499a 100644 --- a/test/development/basic/styled-components/styled-components.test.ts +++ b/test/development/basic/styled-components/styled-components.test.ts @@ -1,88 +1,85 @@ import webdriver from 'next-webdriver' -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils' -createNextDescribe( - 'styled-components SWC transform', - { +describe('styled-components SWC transform', () => { + const { next } = nextTestSetup({ files: __dirname, dependencies: { 'styled-components': '5.3.3', }, - }, - ({ next }) => { - async function matchLogs$(browser) { - let foundLog = false + }) + async function matchLogs$(browser) { + let foundLog = false - const browserLogs = await browser.log('browser') + const browserLogs = await browser.log('browser') - browserLogs.forEach((log) => { - if (log.message.includes('Warning: Prop `%s` did not match.')) { - foundLog = true - } - }) - return foundLog - } - - it('should not have hydration mismatch with styled-components transform enabled', async () => { - let browser - try { - browser = await webdriver(next.url, '/') - - // Compile /_error - await fetchViaHTTP(next.url, '/404') - - // Try 4 times to be sure there is no mismatch - expect(await matchLogs$(browser)).toBe(false) - await browser.refresh() - expect(await matchLogs$(browser)).toBe(false) - await browser.refresh() - expect(await matchLogs$(browser)).toBe(false) - await browser.refresh() - expect(await matchLogs$(browser)).toBe(false) - } finally { - if (browser) { - await browser.close() - } + browserLogs.forEach((log) => { + if (log.message.includes('Warning: Prop `%s` did not match.')) { + foundLog = true } }) - - it('should render the page with correct styles', async () => { - const browser = await webdriver(next.url, '/') - - expect( - await browser.eval( - `window.getComputedStyle(document.querySelector('#btn')).color` - ) - ).toBe('rgb(255, 255, 255)') - expect( - await browser.eval( - `window.getComputedStyle(document.querySelector('#wrap-div')).color` - ) - ).toBe('rgb(0, 0, 0)') - }) - - it('should enable the display name transform by default', async () => { - // make sure the index chunk gets generated - const html = await next.render('/') - expect(html).toContain('pages__Button') - }) - - it('should contain styles in initial HTML', async () => { - const html = await renderViaHTTP(next.url, '/') - expect(html).toContain('background:transparent') - expect(html).toContain('color:white') - }) - - it('should only render once on the server per request', async () => { - const outputs = [] - next.on('stdout', (args) => { - outputs.push(args) - }) - await renderViaHTTP(next.url, '/') - expect( - outputs.filter((output) => output.trim() === '__render__').length - ).toBe(1) - }) + return foundLog } -) + + it('should not have hydration mismatch with styled-components transform enabled', async () => { + let browser + try { + browser = await webdriver(next.url, '/') + + // Compile /_error + await fetchViaHTTP(next.url, '/404') + + // Try 4 times to be sure there is no mismatch + expect(await matchLogs$(browser)).toBe(false) + await browser.refresh() + expect(await matchLogs$(browser)).toBe(false) + await browser.refresh() + expect(await matchLogs$(browser)).toBe(false) + await browser.refresh() + expect(await matchLogs$(browser)).toBe(false) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should render the page with correct styles', async () => { + const browser = await webdriver(next.url, '/') + + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('#btn')).color` + ) + ).toBe('rgb(255, 255, 255)') + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('#wrap-div')).color` + ) + ).toBe('rgb(0, 0, 0)') + }) + + it('should enable the display name transform by default', async () => { + // make sure the index chunk gets generated + const html = await next.render('/') + expect(html).toContain('pages__Button') + }) + + it('should contain styles in initial HTML', async () => { + const html = await renderViaHTTP(next.url, '/') + expect(html).toContain('background:transparent') + expect(html).toContain('color:white') + }) + + it('should only render once on the server per request', async () => { + const outputs = [] + next.on('stdout', (args) => { + outputs.push(args) + }) + await renderViaHTTP(next.url, '/') + expect( + outputs.filter((output) => output.trim() === '__render__').length + ).toBe(1) + }) +}) diff --git a/test/development/basic/theme-ui/theme-ui.test.ts b/test/development/basic/theme-ui/theme-ui.test.ts index 80b66978d7..b5d3ff0150 100644 --- a/test/development/basic/theme-ui/theme-ui.test.ts +++ b/test/development/basic/theme-ui/theme-ui.test.ts @@ -1,19 +1,17 @@ import { join } from 'path' -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' -createNextDescribe( - 'theme-ui SWC option', - { +describe('theme-ui SWC option', () => { + const { next } = nextTestSetup({ files: join(__dirname, 'fixture'), dependencies: { 'theme-ui': '0.12.0', }, - }, - ({ next }) => { - it('should have theme provided styling', async () => { - const browser = await next.browser('/') - const color = await browser.elementByCss('#hello').getComputedCss('color') - expect(color).toBe('rgb(51, 51, 238)') - }) - } -) + }) + + it('should have theme provided styling', async () => { + const browser = await next.browser('/') + const color = await browser.elementByCss('#hello').getComputedCss('color') + expect(color).toBe('rgb(51, 51, 238)') + }) +}) diff --git a/test/development/duplicate-pages/duplicate-pages.test.ts b/test/development/duplicate-pages/duplicate-pages.test.ts index ae1c708bd1..414bc59948 100644 --- a/test/development/duplicate-pages/duplicate-pages.test.ts +++ b/test/development/duplicate-pages/duplicate-pages.test.ts @@ -1,14 +1,11 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' -createNextDescribe( - 'Handles Duplicate Pages', - { +describe('Handles Duplicate Pages', () => { + const { next } = nextTestSetup({ files: __dirname, - }, - ({ next }) => { - it('Shows warning in development', async () => { - await next.render('/hello') - expect(next.cliOutput).toMatch(/Duplicate page detected/) - }) - } -) + }) + it('Shows warning in development', async () => { + await next.render('/hello') + expect(next.cliOutput).toMatch(/Duplicate page detected/) + }) +}) diff --git a/test/development/experimental-https-server/https-server.generated-key.test.ts b/test/development/experimental-https-server/https-server.generated-key.test.ts index 441060b52a..82dc95332f 100644 --- a/test/development/experimental-https-server/https-server.generated-key.test.ts +++ b/test/development/experimental-https-server/https-server.generated-key.test.ts @@ -1,37 +1,36 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import https from 'https' import { renderViaHTTP, shouldRunTurboDevTest } from 'next-test-utils' -createNextDescribe( - 'experimental-https-server (generated certificate)', - { +describe('experimental-https-server (generated certificate)', () => { + const { next, skipped } = nextTestSetup({ files: __dirname, startCommand: `pnpm next ${ shouldRunTurboDevTest() ? 'dev --turbo' : 'dev' } --experimental-https`, skipStart: !process.env.CI, - }, - ({ next }) => { - if (!process.env.CI) { - console.warn('only runs on CI as it requires administrator privileges') - it('only runs on CI as it requires administrator privileges', () => {}) - return - } + }) + if (skipped) return - const agent = new https.Agent({ - rejectUnauthorized: false, - }) - - it('should successfully load the app in app dir', async () => { - expect(next.url).toInclude('https://') - const html = await renderViaHTTP(next.url, '/1', undefined, { agent }) - expect(html).toContain('Hello from App') - }) - - it('should successfully load the app in pages dir', async () => { - expect(next.url).toInclude('https://') - const html = await renderViaHTTP(next.url, '/2', undefined, { agent }) - expect(html).toContain('Hello from Pages') - }) + if (!process.env.CI) { + console.warn('only runs on CI as it requires administrator privileges') + it('only runs on CI as it requires administrator privileges', () => {}) + return } -) + + const agent = new https.Agent({ + rejectUnauthorized: false, + }) + + it('should successfully load the app in app dir', async () => { + expect(next.url).toInclude('https://') + const html = await renderViaHTTP(next.url, '/1', undefined, { agent }) + expect(html).toContain('Hello from App') + }) + + it('should successfully load the app in pages dir', async () => { + expect(next.url).toInclude('https://') + const html = await renderViaHTTP(next.url, '/2', undefined, { agent }) + expect(html).toContain('Hello from Pages') + }) +}) diff --git a/test/development/experimental-https-server/https-server.provided-key.test.ts b/test/development/experimental-https-server/https-server.provided-key.test.ts index ce9312e877..54128046b5 100644 --- a/test/development/experimental-https-server/https-server.provided-key.test.ts +++ b/test/development/experimental-https-server/https-server.provided-key.test.ts @@ -1,30 +1,27 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import https from 'https' import { renderViaHTTP, shouldRunTurboDevTest } from 'next-test-utils' -createNextDescribe( - 'experimental-https-server (provided certificate)', - { +describe('experimental-https-server (provided certificate)', () => { + const { next } = nextTestSetup({ files: __dirname, startCommand: `pnpm next ${ shouldRunTurboDevTest() ? 'dev --turbo' : 'dev' } --experimental-https --experimental-https-key ./certificates/localhost-key.pem --experimental-https-cert ./certificates/localhost.pem`, - }, - ({ next }) => { - const agent = new https.Agent({ - rejectUnauthorized: false, - }) + }) + const agent = new https.Agent({ + rejectUnauthorized: false, + }) - it('should successfully load the app in app dir', async () => { - expect(next.url).toInclude('https://') - const html = await renderViaHTTP(next.url, '/1', undefined, { agent }) - expect(html).toContain('Hello from App') - }) + it('should successfully load the app in app dir', async () => { + expect(next.url).toInclude('https://') + const html = await renderViaHTTP(next.url, '/1', undefined, { agent }) + expect(html).toContain('Hello from App') + }) - it('should successfully load the app in pages dir', async () => { - expect(next.url).toInclude('https://') - const html = await renderViaHTTP(next.url, '/2', undefined, { agent }) - expect(html).toContain('Hello from Pages') - }) - } -) + it('should successfully load the app in pages dir', async () => { + expect(next.url).toInclude('https://') + const html = await renderViaHTTP(next.url, '/2', undefined, { agent }) + expect(html).toContain('Hello from Pages') + }) +}) diff --git a/test/development/middleware-errors/index.test.ts b/test/development/middleware-errors/index.test.ts index 86c4d8b89d..feb4c824f2 100644 --- a/test/development/middleware-errors/index.test.ts +++ b/test/development/middleware-errors/index.test.ts @@ -1,66 +1,64 @@ import { check, getRedboxSource, hasRedbox } from 'next-test-utils' import stripAnsi from 'strip-ansi' -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' -createNextDescribe( - 'middleware - development errors', - { +describe('middleware - development errors', () => { + const { next } = nextTestSetup({ files: __dirname, env: { __NEXT_TEST_WITH_DEVTOOL: '1' }, - }, - ({ next }) => { - beforeEach(async () => { - await next.stop() - }) + }) + beforeEach(async () => { + await next.stop() + }) - describe('when middleware throws synchronously', () => { - beforeEach(async () => { - await next.patchFile( - 'middleware.js', - ` + describe('when middleware throws synchronously', () => { + beforeEach(async () => { + await next.patchFile( + 'middleware.js', + ` export default function () { throw new Error('boom') }` - ) + ) - await next.start() - }) - - it('logs the error correctly', async () => { - await next.fetch('/') - const output = stripAnsi(next.cliOutput) - await check(() => { - if (process.env.TURBOPACK) { - expect(stripAnsi(next.cliOutput)).toMatch( - /middleware.js \(\d+:\d+\) @ Object.__TURBOPACK__default__export__ \[as handler\]/ - ) - } else { - expect(stripAnsi(next.cliOutput)).toMatch( - /middleware.js \(\d+:\d+\) @ Object.default \[as handler\]/ - ) - } - - expect(stripAnsi(next.cliOutput)).toMatch(/boom/) - return 'success' - }, 'success') - expect(output).not.toContain( - 'webpack-internal:///(middleware)/./middleware.js' - ) - }) - - it('renders the error correctly and recovers', async () => { - const browser = await next.browser('/') - expect(await hasRedbox(browser)).toBe(true) - await next.patchFile('middleware.js', `export default function () {}`) - await hasRedbox(browser) - }) + await next.start() }) - describe('when middleware contains an unhandled rejection', () => { - beforeEach(async () => { - await next.patchFile( - 'middleware.js', - ` + it('logs the error correctly', async () => { + await next.fetch('/') + const output = stripAnsi(next.cliOutput) + await check(() => { + if (process.env.TURBOPACK) { + expect(stripAnsi(next.cliOutput)).toMatch( + /middleware.js \(\d+:\d+\) @ Object.__TURBOPACK__default__export__ \[as handler\]/ + ) + } else { + expect(stripAnsi(next.cliOutput)).toMatch( + /middleware.js \(\d+:\d+\) @ Object.default \[as handler\]/ + ) + } + + expect(stripAnsi(next.cliOutput)).toMatch(/boom/) + return 'success' + }, 'success') + expect(output).not.toContain( + 'webpack-internal:///(middleware)/./middleware.js' + ) + }) + + it('renders the error correctly and recovers', async () => { + const browser = await next.browser('/') + expect(await hasRedbox(browser)).toBe(true) + await next.patchFile('middleware.js', `export default function () {}`) + await hasRedbox(browser) + }) + }) + + describe('when middleware contains an unhandled rejection', () => { + beforeEach(async () => { + await next.patchFile( + 'middleware.js', + ` import { NextResponse } from 'next/server' async function throwError() { throw new Error('async boom!') @@ -69,114 +67,114 @@ createNextDescribe( throwError() return NextResponse.next() }` - ) + ) - await next.start() - }) - - it('logs the error correctly', async () => { - await next.fetch('/') - await check( - () => stripAnsi(next.cliOutput), - new RegExp(`unhandledRejection: Error: async boom!`, 'm') - ) - // expect(output).not.toContain( - // 'webpack-internal:///(middleware)/./middleware.js' - // ) - }) - - it('does not render the error', async () => { - const browser = await next.browser('/') - expect(await hasRedbox(browser)).toBe(false) - expect(await browser.elementByCss('#page-title')).toBeTruthy() - }) + await next.start() }) - describe('when running invalid dynamic code with eval', () => { - beforeEach(async () => { - await next.patchFile( - 'middleware.js', - ` + it('logs the error correctly', async () => { + await next.fetch('/') + await check( + () => stripAnsi(next.cliOutput), + new RegExp(`unhandledRejection: Error: async boom!`, 'm') + ) + // expect(output).not.toContain( + // 'webpack-internal:///(middleware)/./middleware.js' + // ) + }) + + it('does not render the error', async () => { + const browser = await next.browser('/') + expect(await hasRedbox(browser)).toBe(false) + expect(await browser.elementByCss('#page-title')).toBeTruthy() + }) + }) + + describe('when running invalid dynamic code with eval', () => { + beforeEach(async () => { + await next.patchFile( + 'middleware.js', + ` import { NextResponse } from 'next/server' export default function () { eval('test') return NextResponse.next() }` - ) + ) - await next.start() - }) - - it('logs the error correctly', async () => { - await next.fetch('/') - const output = stripAnsi(next.cliOutput) - await check(() => { - expect(stripAnsi(next.cliOutput)).toMatch( - /middleware.js \(\d+:\d+\) @ eval/ - ) - expect(stripAnsi(next.cliOutput)).toMatch(/test is not defined/) - return 'success' - }, 'success') - expect(output).not.toContain( - 'webpack-internal:///(middleware)/./middleware.js' - ) - }) - - it('renders the error correctly and recovers', async () => { - const browser = await next.browser('/') - expect(await hasRedbox(browser)).toBe(true) - expect(await getRedboxSource(browser)).toContain(`eval('test')`) - await next.patchFile('middleware.js', `export default function () {}`) - await hasRedbox(browser) - }) + await next.start() }) - describe('when throwing while loading the module', () => { - beforeEach(async () => { - await next.patchFile( - 'middleware.js', - ` + it('logs the error correctly', async () => { + await next.fetch('/') + const output = stripAnsi(next.cliOutput) + await check(() => { + expect(stripAnsi(next.cliOutput)).toMatch( + /middleware.js \(\d+:\d+\) @ eval/ + ) + expect(stripAnsi(next.cliOutput)).toMatch(/test is not defined/) + return 'success' + }, 'success') + expect(output).not.toContain( + 'webpack-internal:///(middleware)/./middleware.js' + ) + }) + + it('renders the error correctly and recovers', async () => { + const browser = await next.browser('/') + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxSource(browser)).toContain(`eval('test')`) + await next.patchFile('middleware.js', `export default function () {}`) + await hasRedbox(browser) + }) + }) + + describe('when throwing while loading the module', () => { + beforeEach(async () => { + await next.patchFile( + 'middleware.js', + ` import { NextResponse } from 'next/server' throw new Error('booooom!') export default function () { return NextResponse.next() }` - ) - await next.start() - }) - - it('logs the error correctly', async () => { - await next.fetch('/') - const output = stripAnsi(next.cliOutput) - await check(() => { - expect(stripAnsi(next.cliOutput)).toMatch( - /middleware.js \(\d+:\d+\) @ / - ) - expect(stripAnsi(next.cliOutput)).toMatch(/booooom!/) - return 'success' - }, 'success') - expect(output).not.toContain( - 'webpack-internal:///(middleware)/./middleware.js' - ) - }) - - it('renders the error correctly and recovers', async () => { - const browser = await next.browser('/') - expect(await hasRedbox(browser)).toBe(true) - const source = await getRedboxSource(browser) - expect(source).toContain(`throw new Error('booooom!')`) - expect(source).toContain('middleware.js') - expect(source).not.toContain('//middleware.js') - await next.patchFile('middleware.js', `export default function () {}`) - await hasRedbox(browser) - }) + ) + await next.start() }) - describe('when there is an unhandled rejection while loading the module', () => { - beforeEach(async () => { - await next.patchFile( - 'middleware.js', - ` + it('logs the error correctly', async () => { + await next.fetch('/') + const output = stripAnsi(next.cliOutput) + await check(() => { + expect(stripAnsi(next.cliOutput)).toMatch( + /middleware.js \(\d+:\d+\) @ / + ) + expect(stripAnsi(next.cliOutput)).toMatch(/booooom!/) + return 'success' + }, 'success') + expect(output).not.toContain( + 'webpack-internal:///(middleware)/./middleware.js' + ) + }) + + it('renders the error correctly and recovers', async () => { + const browser = await next.browser('/') + expect(await hasRedbox(browser)).toBe(true) + const source = await getRedboxSource(browser) + expect(source).toContain(`throw new Error('booooom!')`) + expect(source).toContain('middleware.js') + expect(source).not.toContain('//middleware.js') + await next.patchFile('middleware.js', `export default function () {}`) + await hasRedbox(browser) + }) + }) + + describe('when there is an unhandled rejection while loading the module', () => { + beforeEach(async () => { + await next.patchFile( + 'middleware.js', + ` import { NextResponse } from 'next/server' (async function(){ throw new Error('you shall see me') @@ -185,127 +183,124 @@ createNextDescribe( export default function () { return NextResponse.next() }` - ) + ) - await next.start() - }) - - it('logs the error correctly', async () => { - await next.fetch('/') - await check( - () => stripAnsi(next.cliOutput), - new RegExp(`unhandledRejection: Error: you shall see me`, 'm') - ) - // expect(output).not.toContain( - // 'webpack-internal:///(middleware)/./middleware.js' - // ) - }) - - it('does not render the error', async () => { - const browser = await next.browser('/') - expect(await hasRedbox(browser)).toBe(false) - expect(await browser.elementByCss('#page-title')).toBeTruthy() - }) + await next.start() }) - describe('when there is an unhandled rejection while loading a dependency', () => { - beforeEach(async () => { - await next.patchFile( - 'middleware.js', - ` + it('logs the error correctly', async () => { + await next.fetch('/') + await check( + () => stripAnsi(next.cliOutput), + new RegExp(`unhandledRejection: Error: you shall see me`, 'm') + ) + // expect(output).not.toContain( + // 'webpack-internal:///(middleware)/./middleware.js' + // ) + }) + + it('does not render the error', async () => { + const browser = await next.browser('/') + expect(await hasRedbox(browser)).toBe(false) + expect(await browser.elementByCss('#page-title')).toBeTruthy() + }) + }) + + describe('when there is an unhandled rejection while loading a dependency', () => { + beforeEach(async () => { + await next.patchFile( + 'middleware.js', + ` import { NextResponse } from 'next/server' import './lib/unhandled' export default function () { return NextResponse.next() }` - ) + ) - await next.start() - }) - - it('logs the error correctly', async () => { - await next.fetch('/') - const output = stripAnsi(next.cliOutput) - await check( - () => stripAnsi(next.cliOutput), - new RegExp( - ` uncaughtException: Error: This file asynchronously fails while loading`, - 'm' - ) - ) - expect(output).not.toContain( - 'webpack-internal:///(middleware)/./middleware.js' - ) - }) - - it('does not render the error', async () => { - const browser = await next.browser('/') - expect(await hasRedbox(browser)).toBe(false) - expect(await browser.elementByCss('#page-title')).toBeTruthy() - }) + await next.start() }) - describe('when there is a compilation error from boot', () => { - beforeEach(async () => { - await next.patchFile('middleware.js', `export default function () }`) + it('logs the error correctly', async () => { + await next.fetch('/') + const output = stripAnsi(next.cliOutput) + await check( + () => stripAnsi(next.cliOutput), + new RegExp( + ` uncaughtException: Error: This file asynchronously fails while loading`, + 'm' + ) + ) + expect(output).not.toContain( + 'webpack-internal:///(middleware)/./middleware.js' + ) + }) - await next.start() - }) + it('does not render the error', async () => { + const browser = await next.browser('/') + expect(await hasRedbox(browser)).toBe(false) + expect(await browser.elementByCss('#page-title')).toBeTruthy() + }) + }) - it('logs the error correctly', async () => { - await next.fetch('/') - await check(async () => { - expect(next.cliOutput).toContain(`Expected '{', got '}'`) - expect( - next.cliOutput.split(`Expected '{', got '}'`).length - ).toBeGreaterThanOrEqual(2) + describe('when there is a compilation error from boot', () => { + beforeEach(async () => { + await next.patchFile('middleware.js', `export default function () }`) - return 'success' - }, 'success') - }) + await next.start() + }) - it('renders the error correctly and recovers', async () => { - const browser = await next.browser('/') - expect(await hasRedbox(browser)).toBe(true) + it('logs the error correctly', async () => { + await next.fetch('/') + await check(async () => { + expect(next.cliOutput).toContain(`Expected '{', got '}'`) expect( - await browser.elementByCss('#nextjs__container_errors_desc').text() - ).toEqual('Failed to compile') - await next.patchFile('middleware.js', `export default function () {}`) - await hasRedbox(browser) - expect(await browser.elementByCss('#page-title')).toBeTruthy() - }) + next.cliOutput.split(`Expected '{', got '}'`).length + ).toBeGreaterThanOrEqual(2) + + return 'success' + }, 'success') }) - describe('when there is a compilation error after boot', () => { - beforeEach(async () => { - await next.patchFile('middleware.js', `export default function () {}`) - - await next.start() - }) - - it('logs the error correctly', async () => { - await next.patchFile('middleware.js', `export default function () }`) - await next.fetch('/') - - await check(() => { - expect(next.cliOutput).toContain(`Expected '{', got '}'`) - expect(next.cliOutput.split(`Expected '{', got '}'`).length).toEqual( - 2 - ) - return 'success' - }, 'success') - }) - - it('renders the error correctly and recovers', async () => { - const browser = await next.browser('/') - expect(await hasRedbox(browser)).toBe(false) - await next.patchFile('middleware.js', `export default function () }`) - expect(await hasRedbox(browser)).toBe(true) - await next.patchFile('middleware.js', `export default function () {}`) - expect(await hasRedbox(browser)).toBe(false) - expect(await browser.elementByCss('#page-title')).toBeTruthy() - }) + it('renders the error correctly and recovers', async () => { + const browser = await next.browser('/') + expect(await hasRedbox(browser)).toBe(true) + expect( + await browser.elementByCss('#nextjs__container_errors_desc').text() + ).toEqual('Failed to compile') + await next.patchFile('middleware.js', `export default function () {}`) + await hasRedbox(browser) + expect(await browser.elementByCss('#page-title')).toBeTruthy() }) - } -) + }) + + describe('when there is a compilation error after boot', () => { + beforeEach(async () => { + await next.patchFile('middleware.js', `export default function () {}`) + + await next.start() + }) + + it('logs the error correctly', async () => { + await next.patchFile('middleware.js', `export default function () }`) + await next.fetch('/') + + await check(() => { + expect(next.cliOutput).toContain(`Expected '{', got '}'`) + expect(next.cliOutput.split(`Expected '{', got '}'`).length).toEqual(2) + return 'success' + }, 'success') + }) + + it('renders the error correctly and recovers', async () => { + const browser = await next.browser('/') + expect(await hasRedbox(browser)).toBe(false) + await next.patchFile('middleware.js', `export default function () }`) + expect(await hasRedbox(browser)).toBe(true) + await next.patchFile('middleware.js', `export default function () {}`) + expect(await hasRedbox(browser)).toBe(false) + expect(await browser.elementByCss('#page-title')).toBeTruthy() + }) + }) +}) diff --git a/test/development/next-font/deprecated-package.test.ts b/test/development/next-font/deprecated-package.test.ts index 7b9b3fdb3f..97d176e846 100644 --- a/test/development/next-font/deprecated-package.test.ts +++ b/test/development/next-font/deprecated-package.test.ts @@ -1,10 +1,9 @@ /* eslint-env jest */ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { check } from 'next-test-utils' -createNextDescribe( - 'Deprecated @next/font warning', - { +describe('Deprecated @next/font warning', () => { + const { next, skipped } = nextTestSetup({ files: { 'pages/index.js': '', }, @@ -14,34 +13,34 @@ createNextDescribe( '@next/font': 'canary', }, skipStart: true, - }, - ({ next }) => { - it('should warn if @next/font is in deps', async () => { - await next.start() - await check(() => next.cliOutput, /ready/i) - await check( - () => next.cliOutput, - new RegExp('please use the built-in `next/font` instead') - ) + }) + if (skipped) return - await next.stop() - await next.clean() - }) + it('should warn if @next/font is in deps', async () => { + await next.start() + await check(() => next.cliOutput, /ready/i) + await check( + () => next.cliOutput, + new RegExp('please use the built-in `next/font` instead') + ) - it('should not warn if @next/font is not in deps', async () => { - // Remove @next/font from deps - const packageJson = JSON.parse(await next.readFile('package.json')) - delete packageJson.dependencies['@next/font'] - await next.patchFile('package.json', JSON.stringify(packageJson)) + await next.stop() + await next.clean() + }) - await next.start() - await check(() => next.cliOutput, /ready/i) - expect(next.cliOutput).not.toInclude( - 'please use the built-in `next/font` instead' - ) + it('should not warn if @next/font is not in deps', async () => { + // Remove @next/font from deps + const packageJson = JSON.parse(await next.readFile('package.json')) + delete packageJson.dependencies['@next/font'] + await next.patchFile('package.json', JSON.stringify(packageJson)) - await next.stop() - await next.clean() - }) - } -) + await next.start() + await check(() => next.cliOutput, /ready/i) + expect(next.cliOutput).not.toInclude( + 'please use the built-in `next/font` instead' + ) + + await next.stop() + await next.clean() + }) +}) diff --git a/test/development/pages-dir/client-navigation/index.test.ts b/test/development/pages-dir/client-navigation/index.test.ts index 78a1d041e1..3e4bae1fbe 100644 --- a/test/development/pages-dir/client-navigation/index.test.ts +++ b/test/development/pages-dir/client-navigation/index.test.ts @@ -12,1742 +12,275 @@ import { import webdriver from 'next-webdriver' import path from 'path' import renderingSuite from './rendering' -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' -createNextDescribe( - 'Client Navigation', - { +describe('Client Navigation', () => { + const { next } = nextTestSetup({ files: path.join(__dirname, 'fixture'), - }, - ({ next }) => { - it('should not reload when visiting /_error directly', async () => { - const { status } = await fetchViaHTTP(next.appPort, '/_error') - const browser = await webdriver(next.appPort, '/_error') + }) - await browser.eval('window.hello = true') + it('should not reload when visiting /_error directly', async () => { + const { status } = await fetchViaHTTP(next.appPort, '/_error') + const browser = await webdriver(next.appPort, '/_error') - // wait on-demand-entries timeout since it can trigger - // reloading non-stop - for (let i = 0; i < 15; i++) { - expect(await browser.eval('window.hello')).toBe(true) - await waitFor(1000) - } - const html = await browser.eval('document.documentElement.innerHTML') + await browser.eval('window.hello = true') - expect(status).toBe(404) - expect(html).toContain('This page could not be found') - expect(html).toContain('404') + // wait on-demand-entries timeout since it can trigger + // reloading non-stop + for (let i = 0; i < 15; i++) { + expect(await browser.eval('window.hello')).toBe(true) + await waitFor(1000) + } + const html = await browser.eval('document.documentElement.innerHTML') + + expect(status).toBe(404) + expect(html).toContain('This page could not be found') + expect(html).toContain('404') + }) + + describe('with ', () => { + it('should navigate the page', async () => { + const browser = await webdriver(next.appPort, '/nav') + const text = await browser + .elementByCss('#about-link') + .click() + .waitForElementByCss('.nav-about') + .elementByCss('p') + .text() + + expect(text).toBe('This is the about page.') + await browser.close() }) - describe('with ', () => { - it('should navigate the page', async () => { - const browser = await webdriver(next.appPort, '/nav') - const text = await browser - .elementByCss('#about-link') - .click() - .waitForElementByCss('.nav-about') - .elementByCss('p') - .text() - - expect(text).toBe('This is the about page.') - await browser.close() - }) - - it('should have proper error when no children are provided', async () => { - const browser = await webdriver(next.appPort, '/link-no-child') - expect(await hasRedbox(browser)).toBe(true) - expect(await getRedboxHeader(browser)).toContain( - 'No children were passed to with `href` of `/about` but one child is required' - ) - }) - - it('should not throw error when one number type child is provided', async () => { - const browser = await webdriver(next.appPort, '/link-number-child') - expect(await hasRedbox(browser)).toBe(false) - if (browser) await browser.close() - }) - - it('should navigate back after reload', async () => { - const browser = await webdriver(next.appPort, '/nav') - await browser.elementByCss('#about-link').click() - await browser.waitForElementByCss('.nav-about') - await browser.refresh() - await waitFor(3000) - await browser.back() - await waitFor(3000) - const text = await browser.elementByCss('#about-link').text() - if (browser) await browser.close() - expect(text).toMatch(/About/) - }) - - it('should navigate forwards after reload', async () => { - const browser = await webdriver(next.appPort, '/nav') - await browser.elementByCss('#about-link').click() - await browser.waitForElementByCss('.nav-about') - await browser.back() - await browser.refresh() - await waitFor(3000) - await browser.forward() - await waitFor(3000) - const text = await browser.elementByCss('p').text() - if (browser) await browser.close() - expect(text).toMatch(/this is the about page/i) - }) - - it('should error when calling onClick without event', async () => { - const browser = await webdriver(next.appPort, '/link-invalid-onclick') - expect(await browser.elementByCss('#errors').text()).toBe('0') - await browser.elementByCss('#custom-button').click() - expect(await browser.elementByCss('#errors').text()).toBe('1') - }) - - it('should navigate via the client side', async () => { - const browser = await webdriver(next.appPort, '/nav') - - const counterText = await browser - .elementByCss('#increase') - .click() - .elementByCss('#about-link') - .click() - .waitForElementByCss('.nav-about') - .elementByCss('#home-link') - .click() - .waitForElementByCss('.nav-home') - .elementByCss('#counter') - .text() - - expect(counterText).toBe('Counter: 1') - await browser.close() - }) - - it('should navigate an absolute url', async () => { - const browser = await webdriver( - next.appPort, - `/absolute-url?port=${next.appPort}` - ) - await browser.waitForElementByCss('#absolute-link').click() - await check( - () => browser.eval(() => window.location.origin), - 'https://vercel.com' - ) - }) - - it('should call mouse handlers with an absolute url', async () => { - const browser = await webdriver( - next.appPort, - `/absolute-url?port=${next.appPort}` - ) - - await browser.elementByCss('#absolute-link-mouse-events').moveTo() - - expect( - await browser - .waitForElementByCss('#absolute-link-mouse-events') - .getAttribute('data-hover') - ).toBe('true') - }) - - it('should navigate an absolute local url', async () => { - const browser = await webdriver( - next.appPort, - `/absolute-url?port=${next.appPort}` - ) - // @ts-expect-error _didNotNavigate is set intentionally - await browser.eval(() => (window._didNotNavigate = true)) - await browser.waitForElementByCss('#absolute-local-link').click() - const text = await browser - .waitForElementByCss('.nav-about') - .elementByCss('p') - .text() - - expect(text).toBe('This is the about page.') - // @ts-expect-error _didNotNavigate is set intentionally - expect(await browser.eval(() => window._didNotNavigate)).toBe(true) - }) - - it('should navigate an absolute local url with as', async () => { - const browser = await webdriver( - next.appPort, - `/absolute-url?port=${next.appPort}` - ) - // @ts-expect-error _didNotNavigate is set intentionally - await browser.eval(() => (window._didNotNavigate = true)) - await browser - .waitForElementByCss('#absolute-local-dynamic-link') - .click() - expect(await browser.waitForElementByCss('#dynamic-page').text()).toBe( - 'hello' - ) - // @ts-expect-error _didNotNavigate is set intentionally - expect(await browser.eval(() => window._didNotNavigate)).toBe(true) - }) - }) - - describe('with tag inside the ', () => { - it('should navigate the page', async () => { - const browser = await webdriver(next.appPort, '/nav/about') - const text = await browser - .elementByCss('#home-link') - .click() - .waitForElementByCss('.nav-home') - .elementByCss('p') - .text() - - expect(text).toBe('This is the home.') - await browser.close() - }) - - it('should not navigate if the tag has a target', async () => { - const browser = await webdriver(next.appPort, '/nav') - - await browser - .elementByCss('#increase') - .click() - .elementByCss('#target-link') - .click() - - await waitFor(1000) - - const counterText = await browser.elementByCss('#counter').text() - - expect(counterText).toBe('Counter: 1') - await browser.close() - }) - - it('should not navigate if the click-event is modified', async () => { - const browser = await webdriver(next.appPort, '/nav') - - await browser.elementByCss('#increase').click() - - const key = process.platform === 'darwin' ? 'Meta' : 'Control' - - await browser.keydown(key) - - await browser.elementByCss('#in-svg-link').click() - - await browser.keyup(key) - await waitFor(1000) - - const counterText = await browser.elementByCss('#counter').text() - - expect(counterText).toBe('Counter: 1') - await browser.close() - }) - - it('should not reload when link in svg is clicked', async () => { - const browser = await webdriver(next.appPort, '/nav') - await browser.eval('window.hello = true') - await browser - .elementByCss('#in-svg-link') - .click() - .waitForElementByCss('.nav-about') - - expect(await browser.eval('window.hello')).toBe(true) - await browser.close() - }) - }) - - describe('with unexpected nested tag', () => { - it('should not redirect if passHref prop is not defined in Link', async () => { - const browser = await webdriver(next.appPort, '/nav/pass-href-prop') - const text = await browser - .elementByCss('#without-href') - .click() - .waitForElementByCss('.nav-pass-href-prop') - .elementByCss('p') - .text() - - expect(text).toBe('This is the passHref prop page.') - await browser.close() - }) - - it('should redirect if passHref prop is defined in Link', async () => { - const browser = await webdriver(next.appPort, '/nav/pass-href-prop') - const text = await browser - .elementByCss('#with-href') - .click() - .waitForElementByCss('.nav-home') - .elementByCss('p') - .text() - - expect(text).toBe('This is the home.') - await browser.close() - }) - }) - - describe('with empty getInitialProps()', () => { - it('should render an error', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/nav') - await browser.elementByCss('#empty-props').click() - expect(await hasRedbox(browser)).toBe(true) - expect(await getRedboxHeader(browser)).toMatch( - /should resolve to an object\. But found "null" instead\./ - ) - } finally { - if (browser) { - await browser.close() - } - } - }) - }) - - describe('with the same page but different querystring', () => { - it('should navigate the page', async () => { - const browser = await webdriver(next.appPort, '/nav/querystring?id=1') - const text = await browser - .elementByCss('#next-id-link') - .click() - .waitForElementByCss('.nav-id-2') - .elementByCss('p') - .text() - - expect(text).toBe('2') - await browser.close() - }) - - it('should remove querystring', async () => { - const browser = await webdriver(next.appPort, '/nav/querystring?id=1') - const text = await browser - .elementByCss('#main-page') - .click() - .waitForElementByCss('.nav-id-0') - .elementByCss('p') - .text() - - expect(text).toBe('0') - await browser.close() - }) - }) - - describe('with the current url', () => { - it('should reload the page', async () => { - const browser = await webdriver(next.appPort, '/nav/self-reload') - const defaultCount = await browser.elementByCss('p').text() - expect(defaultCount).toBe('COUNT: 0') - - const countAfterClicked = await browser - .elementByCss('#self-reload-link') - .click() - .elementByCss('p') - .text() - - expect(countAfterClicked).toBe('COUNT: 1') - await browser.close() - }) - - it('should always replace the state', async () => { - const browser = await webdriver(next.appPort, '/nav') - - const countAfterClicked = await browser - .elementByCss('#self-reload-link') - .click() - .waitForElementByCss('#self-reload-page') - .elementByCss('#self-reload-link') - .click() - .elementByCss('#self-reload-link') - .click() - .elementByCss('p') - .text() - - // counts (page change + two clicks) - expect(countAfterClicked).toBe('COUNT: 3') - - // Since we replace the state, back button would simply go us back to /nav - await browser.back().waitForElementByCss('.nav-home') - - await browser.close() - }) - }) - - describe('with onClick action', () => { - it('should reload the page and perform additional action', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/nav/on-click') - const defaultCountQuery = await browser - .elementByCss('#query-count') - .text() - const defaultCountState = await browser - .elementByCss('#state-count') - .text() - expect(defaultCountQuery).toBe('QUERY COUNT: 0') - expect(defaultCountState).toBe('STATE COUNT: 0') - - await browser.elementByCss('#on-click-link').click() - - const countQueryAfterClicked = await browser - .elementByCss('#query-count') - .text() - const countStateAfterClicked = await browser - .elementByCss('#state-count') - .text() - expect(countQueryAfterClicked).toBe('QUERY COUNT: 1') - expect(countStateAfterClicked).toBe('STATE COUNT: 1') - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should not reload if default was prevented', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/nav/on-click') - const defaultCountQuery = await browser - .elementByCss('#query-count') - .text() - const defaultCountState = await browser - .elementByCss('#state-count') - .text() - expect(defaultCountQuery).toBe('QUERY COUNT: 0') - expect(defaultCountState).toBe('STATE COUNT: 0') - - await browser.elementByCss('#on-click-link-prevent-default').click() - - const countQueryAfterClicked = await browser - .elementByCss('#query-count') - .text() - const countStateAfterClicked = await browser - .elementByCss('#state-count') - .text() - expect(countQueryAfterClicked).toBe('QUERY COUNT: 0') - expect(countStateAfterClicked).toBe('STATE COUNT: 1') - - await browser.elementByCss('#on-click-link').click() - - const countQueryAfterClickedAgain = await browser - .elementByCss('#query-count') - .text() - const countStateAfterClickedAgain = await browser - .elementByCss('#state-count') - .text() - expect(countQueryAfterClickedAgain).toBe('QUERY COUNT: 1') - expect(countStateAfterClickedAgain).toBe('STATE COUNT: 2') - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should always replace the state and perform additional action', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/nav') - - await browser - .elementByCss('#on-click-link') - .click() - .waitForElementByCss('#on-click-page') - - const defaultCountQuery = await browser - .elementByCss('#query-count') - .text() - expect(defaultCountQuery).toBe('QUERY COUNT: 1') - - await browser.elementByCss('#on-click-link').click() - const countQueryAfterClicked = await browser - .elementByCss('#query-count') - .text() - const countStateAfterClicked = await browser - .elementByCss('#state-count') - .text() - expect(countQueryAfterClicked).toBe('QUERY COUNT: 2') - expect(countStateAfterClicked).toBe('STATE COUNT: 1') - - // Since we replace the state, back button would simply go us back to /nav - await browser.back().waitForElementByCss('.nav-home') - } finally { - if (browser) { - await browser.close() - } - } - }) - }) - describe('resets scroll at the correct time', () => { - it('should reset scroll before the new page runs its lifecycles ()', async () => { - let browser - try { - browser = await webdriver( - next.appPort, - '/nav/long-page-to-snap-scroll' - ) - - // Scrolls to item 400 on the page - await browser - .waitForElementByCss('#long-page-to-snap-scroll') - .elementByCss('#scroll-to-item-400') - .click() - - const scrollPosition = await browser.eval('window.pageYOffset') - expect(scrollPosition).toBe(7208) - - // Go to snap scroll page - await browser - .elementByCss('#goto-snap-scroll-position') - .click() - .waitForElementByCss('#scroll-pos-y') - - const snappedScrollPosition = await browser.eval( - 'document.getElementById("scroll-pos-y").innerText' - ) - expect(snappedScrollPosition).toBe('0') - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should reset scroll before the new page runs its lifecycles (Router#push)', async () => { - let browser - try { - browser = await webdriver( - next.appPort, - '/nav/long-page-to-snap-scroll' - ) - - // Scrolls to item 400 on the page - await browser - .waitForElementByCss('#long-page-to-snap-scroll') - .elementByCss('#scroll-to-item-400') - .click() - - const scrollPosition = await browser.eval('window.pageYOffset') - expect(scrollPosition).toBe(7208) - - // Go to snap scroll page - await browser - .elementByCss('#goto-snap-scroll-position-imperative') - .click() - .waitForElementByCss('#scroll-pos-y') - - const snappedScrollPosition = await browser.eval( - 'document.getElementById("scroll-pos-y").innerText' - ) - expect(snappedScrollPosition).toBe('0') - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should intentionally not reset scroll before the new page runs its lifecycles (Router#push)', async () => { - let browser - try { - browser = await webdriver( - next.appPort, - '/nav/long-page-to-snap-scroll' - ) - - // Scrolls to item 400 on the page - await browser - .waitForElementByCss('#long-page-to-snap-scroll') - .elementByCss('#scroll-to-item-400') - .click() - - const scrollPosition = await browser.eval('window.pageYOffset') - expect(scrollPosition).toBe(7208) - - // Go to snap scroll page - await browser - .elementByCss('#goto-snap-scroll-position-imperative-noscroll') - .click() - .waitForElementByCss('#scroll-pos-y') - - const snappedScrollPosition = await browser.eval( - 'document.getElementById("scroll-pos-y").innerText' - ) - expect(snappedScrollPosition).not.toBe('0') - expect(Number(snappedScrollPosition)).toBeGreaterThanOrEqual(7208) - } finally { - if (browser) { - await browser.close() - } - } - }) - }) - - describe('with hash changes', () => { - describe('check hydration mis-match', () => { - it('should not have hydration mis-match for hash link', async () => { - const browser = await webdriver(next.appPort, '/nav/hash-changes') - const browserLogs = await browser.log('browser') - let found = false - browserLogs.forEach((log) => { - console.log('log.message', log.message) - if (log.message.includes('Warning: Prop')) { - found = true - } - }) - expect(found).toEqual(false) - }) - }) - - describe('when hash change via Link', () => { - it('should not run getInitialProps', async () => { - const browser = await webdriver(next.appPort, '/nav/hash-changes') - - const counter = await browser - .elementByCss('#via-link') - .click() - .elementByCss('p') - .text() - - expect(counter).toBe('COUNT: 0') - - await browser.close() - }) - - it('should scroll to the specified position on the same page', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/nav/hash-changes') - - // Scrolls to item 400 on the page - await browser.elementByCss('#scroll-to-item-400').click() - - const scrollPositionBeforeEmptyHash = await browser.eval( - 'window.pageYOffset' - ) - - expect(scrollPositionBeforeEmptyHash).toBe(7258) - - // Scrolls back to top when scrolling to `#` with no value. - await browser.elementByCss('#via-empty-hash').click() - - const scrollPositionAfterEmptyHash = await browser.eval( - 'window.pageYOffset' - ) - - expect(scrollPositionAfterEmptyHash).toBe(0) - - // Scrolls to item 400 on the page - await browser.elementByCss('#scroll-to-item-400').click() - - const scrollPositionBeforeTopHash = await browser.eval( - 'window.pageYOffset' - ) - - expect(scrollPositionBeforeTopHash).toBe(7258) - - // Scrolls back to top when clicking link with href `#top`. - await browser.elementByCss('#via-top-hash').click() - - const scrollPositionAfterTopHash = await browser.eval( - 'window.pageYOffset' - ) - - expect(scrollPositionAfterTopHash).toBe(0) - - // Scrolls to cjk anchor on the page - await browser.elementByCss('#scroll-to-cjk-anchor').click() - - const scrollPositionCJKHash = await browser.eval( - 'window.pageYOffset' - ) - - expect(scrollPositionCJKHash).toBe(17436) - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should not scroll to hash when scroll={false} is set', async () => { - const browser = await webdriver(next.appPort, '/nav/hash-changes') - const curScroll = await browser.eval( - 'document.documentElement.scrollTop' - ) - await browser - .elementByCss('#scroll-to-name-item-400-no-scroll') - .click() - expect(curScroll).toBe( - await browser.eval('document.documentElement.scrollTop') - ) - }) - - it('should scroll to the specified position on the same page with a name property', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/nav/hash-changes') - - // Scrolls to item 400 with name="name-item-400" on the page - await browser.elementByCss('#scroll-to-name-item-400').click() - - const scrollPosition = await browser.eval('window.pageYOffset') - - expect(scrollPosition).toBe(16258) - - // Scrolls back to top when scrolling to `#` with no value. - await browser.elementByCss('#via-empty-hash').click() - - const scrollPositionAfterEmptyHash = await browser.eval( - 'window.pageYOffset' - ) - - expect(scrollPositionAfterEmptyHash).toBe(0) - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should scroll to the specified position to a new page', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/nav') - - // Scrolls to item 400 on the page - await browser - .elementByCss('#scroll-to-hash') - .click() - .waitForElementByCss('#hash-changes-page') - - const scrollPosition = await browser.eval('window.pageYOffset') - expect(scrollPosition).toBe(7258) - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should scroll to the specified CJK position to a new page', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/nav') - - // Scrolls to CJK anchor on the page - await browser - .elementByCss('#scroll-to-cjk-hash') - .click() - .waitForElementByCss('#hash-changes-page') - - const scrollPosition = await browser.eval('window.pageYOffset') - expect(scrollPosition).toBe(17436) - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('Should update asPath', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/nav/hash-changes') - - await browser.elementByCss('#via-link').click() - - const asPath = await browser.elementByCss('div#asPath').text() - expect(asPath).toBe('ASPATH: /nav/hash-changes#via-link') - } finally { - if (browser) { - await browser.close() - } - } - }) - }) - - describe('when hash change via A tag', () => { - it('should not run getInitialProps', async () => { - const browser = await webdriver(next.appPort, '/nav/hash-changes') - - const counter = await browser - .elementByCss('#via-a') - .click() - .elementByCss('p') - .text() - - expect(counter).toBe('COUNT: 0') - - await browser.close() - }) - }) - - describe('when hash get removed', () => { - it('should not run getInitialProps', async () => { - const browser = await webdriver(next.appPort, '/nav/hash-changes') - - const counter = await browser - .elementByCss('#via-a') - .click() - .elementByCss('#page-url') - .click() - .elementByCss('p') - .text() - - expect(counter).toBe('COUNT: 1') - - await browser.close() - }) - - it('should not run getInitialProps when removing via back', async () => { - const browser = await webdriver(next.appPort, '/nav/hash-changes') - - const counter = await browser - .elementByCss('#scroll-to-item-400') - .click() - .back() - .elementByCss('p') - .text() - - expect(counter).toBe('COUNT: 0') - await browser.close() - }) - }) - - describe('when hash set to empty', () => { - it('should not run getInitialProps', async () => { - const browser = await webdriver(next.appPort, '/nav/hash-changes') - - const counter = await browser - .elementByCss('#via-a') - .click() - .elementByCss('#via-empty-hash') - .click() - .elementByCss('p') - .text() - - expect(counter).toBe('COUNT: 0') - - await browser.close() - }) - }) - }) - - describe('with hash changes with state', () => { - describe('when passing state via hash change', () => { - it('should increment the history state counter', async () => { - const browser = await webdriver( - next.appPort, - '/nav/hash-changes-with-state#' - ) - - const historyCount = await browser - .elementByCss('#increment-history-count') - .click() - .elementByCss('#increment-history-count') - .click() - .elementByCss('div#history-count') - .text() - - expect(historyCount).toBe('HISTORY COUNT: 2') - - const counter = await browser.elementByCss('p').text() - - // getInitialProps should not be called with only hash changes - expect(counter).toBe('COUNT: 0') - - await browser.close() - }) - - it('should increment the shallow history state counter', async () => { - const browser = await webdriver( - next.appPort, - '/nav/hash-changes-with-state#' - ) - - const historyCount = await browser - .elementByCss('#increment-shallow-history-count') - .click() - .elementByCss('#increment-shallow-history-count') - .click() - .elementByCss('div#shallow-history-count') - .text() - - expect(historyCount).toBe('SHALLOW HISTORY COUNT: 2') - - const counter = await browser.elementByCss('p').text() - - expect(counter).toBe('COUNT: 0') - - await browser.close() - }) - }) - }) - - describe('with shallow routing', () => { - it('should update the url without running getInitialProps', async () => { - const browser = await webdriver(next.appPort, '/nav/shallow-routing') - const counter = await browser - .elementByCss('#increase') - .click() - .elementByCss('#increase') - .click() - .elementByCss('#counter') - .text() - expect(counter).toBe('Counter: 2') - - const getInitialPropsRunCount = await browser - .elementByCss('#get-initial-props-run-count') - .text() - expect(getInitialPropsRunCount).toBe('getInitialProps run count: 1') - - await browser.close() - }) - - it('should handle the back button and should not run getInitialProps', async () => { - const browser = await webdriver(next.appPort, '/nav/shallow-routing') - let counter = await browser - .elementByCss('#increase') - .click() - .elementByCss('#increase') - .click() - .elementByCss('#counter') - .text() - expect(counter).toBe('Counter: 2') - - counter = await browser.back().elementByCss('#counter').text() - expect(counter).toBe('Counter: 1') - - const getInitialPropsRunCount = await browser - .elementByCss('#get-initial-props-run-count') - .text() - expect(getInitialPropsRunCount).toBe('getInitialProps run count: 1') - - await browser.close() - }) - - it('should run getInitialProps always when rending the page to the screen', async () => { - const browser = await webdriver(next.appPort, '/nav/shallow-routing') - - const counter = await browser - .elementByCss('#increase') - .click() - .elementByCss('#increase') - .click() - .elementByCss('#home-link') - .click() - .waitForElementByCss('.nav-home') - .back() - .waitForElementByCss('.shallow-routing') - .elementByCss('#counter') - .text() - expect(counter).toBe('Counter: 2') - - const getInitialPropsRunCount = await browser - .elementByCss('#get-initial-props-run-count') - .text() - expect(getInitialPropsRunCount).toBe('getInitialProps run count: 2') - - await browser.close() - }) - - it('should keep the scroll position on shallow routing', async () => { - const browser = await webdriver(next.appPort, '/nav/shallow-routing') - await browser.eval(() => - document.querySelector('#increase').scrollIntoView() - ) - const scrollPosition = await browser.eval('window.pageYOffset') - - expect(scrollPosition).toBeGreaterThan(3000) - - await browser.elementByCss('#increase').click() - await waitFor(500) - const newScrollPosition = await browser.eval('window.pageYOffset') - - expect(newScrollPosition).toBe(scrollPosition) - - await browser.elementByCss('#increase2').click() - await waitFor(500) - const newScrollPosition2 = await browser.eval('window.pageYOffset') - - expect(newScrollPosition2).toBe(0) - - await browser.eval(() => - document.querySelector('#invalidShallow').scrollIntoView() - ) - const scrollPositionDown = await browser.eval('window.pageYOffset') - - expect(scrollPositionDown).toBeGreaterThan(3000) - - await browser.elementByCss('#invalidShallow').click() - await waitFor(500) - const newScrollPosition3 = await browser.eval('window.pageYOffset') - - expect(newScrollPosition3).toBe(0) - }) - }) - - it('should scroll to top when the scroll option is set to true', async () => { - const browser = await webdriver(next.appPort, '/nav/shallow-routing') - await browser.eval(() => - document.querySelector('#increaseWithScroll').scrollIntoView() + it('should have proper error when no children are provided', async () => { + const browser = await webdriver(next.appPort, '/link-no-child') + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxHeader(browser)).toContain( + 'No children were passed to with `href` of `/about` but one child is required' ) - const scrollPosition = await browser.eval('window.pageYOffset') - - expect(scrollPosition).toBeGreaterThan(3000) - - await browser.elementByCss('#increaseWithScroll').click() - await check(async () => { - const newScrollPosition = await browser.eval('window.pageYOffset') - return newScrollPosition === 0 ? 'success' : 'fail' - }, 'success') }) - describe('with URL objects', () => { - it('should work with ', async () => { - const browser = await webdriver(next.appPort, '/nav') - const text = await browser - .elementByCss('#query-string-link') - .click() - .waitForElementByCss('.nav-querystring') - .elementByCss('p') - .text() - expect(text).toBe('10') + it('should not throw error when one number type child is provided', async () => { + const browser = await webdriver(next.appPort, '/link-number-child') + expect(await hasRedbox(browser)).toBe(false) + if (browser) await browser.close() + }) - expect(await browser.url()).toBe( - `http://localhost:${next.appPort}/nav/querystring/10#10` - ) - await browser.close() - }) + it('should navigate back after reload', async () => { + const browser = await webdriver(next.appPort, '/nav') + await browser.elementByCss('#about-link').click() + await browser.waitForElementByCss('.nav-about') + await browser.refresh() + await waitFor(3000) + await browser.back() + await waitFor(3000) + const text = await browser.elementByCss('#about-link').text() + if (browser) await browser.close() + expect(text).toMatch(/About/) + }) - it('should work with "Router.push"', async () => { - const browser = await webdriver(next.appPort, '/nav') - const text = await browser - .elementByCss('#query-string-button') - .click() - .waitForElementByCss('.nav-querystring') - .elementByCss('p') - .text() - expect(text).toBe('10') + it('should navigate forwards after reload', async () => { + const browser = await webdriver(next.appPort, '/nav') + await browser.elementByCss('#about-link').click() + await browser.waitForElementByCss('.nav-about') + await browser.back() + await browser.refresh() + await waitFor(3000) + await browser.forward() + await waitFor(3000) + const text = await browser.elementByCss('p').text() + if (browser) await browser.close() + expect(text).toMatch(/this is the about page/i) + }) - expect(await browser.url()).toBe( - `http://localhost:${next.appPort}/nav/querystring/10#10` - ) - await browser.close() - }) + it('should error when calling onClick without event', async () => { + const browser = await webdriver(next.appPort, '/link-invalid-onclick') + expect(await browser.elementByCss('#errors').text()).toBe('0') + await browser.elementByCss('#custom-button').click() + expect(await browser.elementByCss('#errors').text()).toBe('1') + }) - it('should work with the "replace" prop', async () => { - const browser = await webdriver(next.appPort, '/nav') + it('should navigate via the client side', async () => { + const browser = await webdriver(next.appPort, '/nav') - let stackLength = await browser.eval('window.history.length') + const counterText = await browser + .elementByCss('#increase') + .click() + .elementByCss('#about-link') + .click() + .waitForElementByCss('.nav-about') + .elementByCss('#home-link') + .click() + .waitForElementByCss('.nav-home') + .elementByCss('#counter') + .text() - expect(stackLength).toBe(2) + expect(counterText).toBe('Counter: 1') + await browser.close() + }) - // Navigation to /about using a replace link should maintain the url stack length - const text = await browser - .elementByCss('#about-replace-link') - .click() - .waitForElementByCss('.nav-about') - .elementByCss('p') - .text() + it('should navigate an absolute url', async () => { + const browser = await webdriver( + next.appPort, + `/absolute-url?port=${next.appPort}` + ) + await browser.waitForElementByCss('#absolute-link').click() + await check( + () => browser.eval(() => window.location.origin), + 'https://vercel.com' + ) + }) - expect(text).toBe('This is the about page.') + it('should call mouse handlers with an absolute url', async () => { + const browser = await webdriver( + next.appPort, + `/absolute-url?port=${next.appPort}` + ) - stackLength = await browser.eval('window.history.length') + await browser.elementByCss('#absolute-link-mouse-events').moveTo() - expect(stackLength).toBe(2) - - // Going back to the home with a regular link will augment the history count + expect( await browser - .elementByCss('#home-link') - .click() - .waitForElementByCss('.nav-home') - - stackLength = await browser.eval('window.history.length') - - expect(stackLength).toBe(3) - - await browser.close() - }) - - it('should handle undefined in router.push', async () => { - const browser = await webdriver(next.appPort, '/nav/query-params') - await browser.elementByCss('#click-me').click() - const query = JSON.parse( - await browser.waitForElementByCss('#query-value').text() - ) - expect(query).toEqual({ - param1: '', - param2: '', - param3: '', - param4: '0', - param5: 'false', - param7: '', - param8: '', - param9: '', - param10: '', - param11: ['', '', '', '0', 'false', '', '', '', '', ''], - }) - }) + .waitForElementByCss('#absolute-link-mouse-events') + .getAttribute('data-hover') + ).toBe('true') }) - describe('with querystring relative urls', () => { - it('should work with Link', async () => { - const browser = await webdriver(next.appPort, '/nav/query-only') - try { - await browser.elementByCss('#link').click() + it('should navigate an absolute local url', async () => { + const browser = await webdriver( + next.appPort, + `/absolute-url?port=${next.appPort}` + ) + // @ts-expect-error _didNotNavigate is set intentionally + await browser.eval(() => (window._didNotNavigate = true)) + await browser.waitForElementByCss('#absolute-local-link').click() + const text = await browser + .waitForElementByCss('.nav-about') + .elementByCss('p') + .text() - await check(() => browser.waitForElementByCss('#prop').text(), 'foo') - } finally { - await browser.close() - } - }) - - it('should work with router.push', async () => { - const browser = await webdriver(next.appPort, '/nav/query-only') - try { - await browser.elementByCss('#router-push').click() - - await check(() => browser.waitForElementByCss('#prop').text(), 'bar') - } finally { - await browser.close() - } - }) - - it('should work with router.replace', async () => { - const browser = await webdriver(next.appPort, '/nav/query-only') - try { - await browser.elementByCss('#router-replace').click() - - await check(() => browser.waitForElementByCss('#prop').text(), 'baz') - } finally { - await browser.close() - } - }) - - it('router.replace with shallow=true shall not throw route cancelled errors', async () => { - const browser = await webdriver(next.appPort, '/nav/query-only-shallow') - try { - await browser.elementByCss('#router-replace').click() - // the error occurs on every replace() after the first one - await browser.elementByCss('#router-replace').click() - - await check( - () => browser.waitForElementByCss('#routeState').text(), - '{"completed":2,"errors":0}' - ) - } finally { - await browser.close() - } - }) + expect(text).toBe('This is the about page.') + // @ts-expect-error _didNotNavigate is set intentionally + expect(await browser.eval(() => window._didNotNavigate)).toBe(true) }) - describe('with getInitialProp redirect', () => { - it('should redirect the page via client side', async () => { - const browser = await webdriver(next.appPort, '/nav') - const text = await browser - .elementByCss('#redirect-link') - .click() - .waitForElementByCss('.nav-about') - .elementByCss('p') - .text() + it('should navigate an absolute local url with as', async () => { + const browser = await webdriver( + next.appPort, + `/absolute-url?port=${next.appPort}` + ) + // @ts-expect-error _didNotNavigate is set intentionally + await browser.eval(() => (window._didNotNavigate = true)) + await browser.waitForElementByCss('#absolute-local-dynamic-link').click() + expect(await browser.waitForElementByCss('#dynamic-page').text()).toBe( + 'hello' + ) + // @ts-expect-error _didNotNavigate is set intentionally + expect(await browser.eval(() => window._didNotNavigate)).toBe(true) + }) + }) - expect(text).toBe('This is the about page.') - await browser.close() - }) + describe('with tag inside the ', () => { + it('should navigate the page', async () => { + const browser = await webdriver(next.appPort, '/nav/about') + const text = await browser + .elementByCss('#home-link') + .click() + .waitForElementByCss('.nav-home') + .elementByCss('p') + .text() - it('should redirect the page when loading', async () => { - const browser = await webdriver(next.appPort, '/nav/redirect') - const text = await browser - .waitForElementByCss('.nav-about') - .elementByCss('p') - .text() - - expect(text).toBe('This is the about page.') - await browser.close() - }) + expect(text).toBe('This is the home.') + await browser.close() }) - describe('with different types of urls', () => { - it('should work with normal page', async () => { - const browser = await webdriver(next.appPort, '/with-cdm') - const text = await browser.elementByCss('p').text() + it('should not navigate if the tag has a target', async () => { + const browser = await webdriver(next.appPort, '/nav') - expect(text).toBe('ComponentDidMount executed on client.') - await browser.close() - }) + await browser + .elementByCss('#increase') + .click() + .elementByCss('#target-link') + .click() - it('should work with dir/ page', async () => { - const browser = await webdriver(next.appPort, '/nested-cdm') - const text = await browser.elementByCss('p').text() + await waitFor(1000) - expect(text).toBe('ComponentDidMount executed on client.') - await browser.close() - }) + const counterText = await browser.elementByCss('#counter').text() - it('should not work with /index page', async () => { - const browser = await webdriver(next.appPort, '/index') - expect(await browser.elementByCss('h1').text()).toBe('404') - expect(await browser.elementByCss('h2').text()).toBe( - 'This page could not be found.' - ) - await browser.close() - }) - - it('should work with / page', async () => { - const browser = await webdriver(next.appPort, '/') - const text = await browser.elementByCss('p').text() - - expect(text).toBe('ComponentDidMount executed on client.') - await browser.close() - }) + expect(counterText).toBe('Counter: 1') + await browser.close() }) - describe('with the HOC based router', () => { - it('should navigate as expected', async () => { - const browser = await webdriver(next.appPort, '/nav/with-hoc') + it('should not navigate if the click-event is modified', async () => { + const browser = await webdriver(next.appPort, '/nav') - const pathname = await browser.elementByCss('#pathname').text() - expect(pathname).toBe('Current path: /nav/with-hoc') + await browser.elementByCss('#increase').click() - const asPath = await browser.elementByCss('#asPath').text() - expect(asPath).toBe('Current asPath: /nav/with-hoc') + const key = process.platform === 'darwin' ? 'Meta' : 'Control' - const text = await browser - .elementByCss('.nav-with-hoc a') - .click() - .waitForElementByCss('.nav-home') - .elementByCss('p') - .text() + await browser.keydown(key) - expect(text).toBe('This is the home.') - await browser.close() - }) + await browser.elementByCss('#in-svg-link').click() + + await browser.keyup(key) + await waitFor(1000) + + const counterText = await browser.elementByCss('#counter').text() + + expect(counterText).toBe('Counter: 1') + await browser.close() }) - describe('with asPath', () => { - describe('inside getInitialProps', () => { - it('should show the correct asPath with a Link with as prop', async () => { - const browser = await webdriver(next.appPort, '/nav') - const asPath = await browser - .elementByCss('#as-path-link') - .click() - .waitForElementByCss('.as-path-content') - .elementByCss('.as-path-content') - .text() + it('should not reload when link in svg is clicked', async () => { + const browser = await webdriver(next.appPort, '/nav') + await browser.eval('window.hello = true') + await browser + .elementByCss('#in-svg-link') + .click() + .waitForElementByCss('.nav-about') - expect(asPath).toBe('/as/path') - await browser.close() - }) + expect(await browser.eval('window.hello')).toBe(true) + await browser.close() + }) + }) - it('should show the correct asPath with a Link without the as prop', async () => { - const browser = await webdriver(next.appPort, '/nav') - const asPath = await browser - .elementByCss('#as-path-link-no-as') - .click() - .waitForElementByCss('.as-path-content') - .elementByCss('.as-path-content') - .text() + describe('with unexpected nested tag', () => { + it('should not redirect if passHref prop is not defined in Link', async () => { + const browser = await webdriver(next.appPort, '/nav/pass-href-prop') + const text = await browser + .elementByCss('#without-href') + .click() + .waitForElementByCss('.nav-pass-href-prop') + .elementByCss('p') + .text() - expect(asPath).toBe('/nav/as-path') - await browser.close() - }) - }) - - describe('with next/router', () => { - it('should show the correct asPath', async () => { - const browser = await webdriver(next.appPort, '/nav') - const asPath = await browser - .elementByCss('#as-path-using-router-link') - .click() - .waitForElementByCss('.as-path-content') - .elementByCss('.as-path-content') - .text() - - expect(asPath).toBe('/nav/as-path-using-router') - await browser.close() - }) - - it('should navigate an absolute url on push', async () => { - const browser = await webdriver( - next.appPort, - `/absolute-url?port=${next.appPort}` - ) - await browser.waitForElementByCss('#router-push').click() - await check( - () => browser.eval(() => window.location.origin), - 'https://vercel.com' - ) - }) - - it('should navigate an absolute url on replace', async () => { - const browser = await webdriver( - next.appPort, - `/absolute-url?port=${next.appPort}` - ) - await browser.waitForElementByCss('#router-replace').click() - await check( - () => browser.eval(() => window.location.origin), - 'https://vercel.com' - ) - }) - - it('should navigate an absolute local url on push', async () => { - const browser = await webdriver( - next.appPort, - `/absolute-url?port=${next.appPort}` - ) - // @ts-expect-error _didNotNavigate is set intentionally - await browser.eval(() => (window._didNotNavigate = true)) - await browser.waitForElementByCss('#router-local-push').click() - const text = await browser - .waitForElementByCss('.nav-about') - .elementByCss('p') - .text() - expect(text).toBe('This is the about page.') - // @ts-expect-error _didNotNavigate is set intentionally - expect(await browser.eval(() => window._didNotNavigate)).toBe(true) - }) - - it('should navigate an absolute local url on replace', async () => { - const browser = await webdriver( - next.appPort, - `/absolute-url?port=${next.appPort}` - ) - // @ts-expect-error _didNotNavigate is set intentionally - await browser.eval(() => (window._didNotNavigate = true)) - await browser.waitForElementByCss('#router-local-replace').click() - const text = await browser - .waitForElementByCss('.nav-about') - .elementByCss('p') - .text() - expect(text).toBe('This is the about page.') - // @ts-expect-error _didNotNavigate is set intentionally - expect(await browser.eval(() => window._didNotNavigate)).toBe(true) - }) - }) - - describe('with next/link', () => { - it('should use pushState with same href and different asPath', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/nav/as-path-pushstate') - await browser - .elementByCss('#hello') - .click() - .waitForElementByCss('#something-hello') - const queryOne = JSON.parse( - await browser.elementByCss('#router-query').text() - ) - expect(queryOne.something).toBe('hello') - await browser - .elementByCss('#same-query') - .click() - .waitForElementByCss('#something-same-query') - const queryTwo = JSON.parse( - await browser.elementByCss('#router-query').text() - ) - expect(queryTwo.something).toBe('hello') - await browser.back().waitForElementByCss('#something-hello') - const queryThree = JSON.parse( - await browser.elementByCss('#router-query').text() - ) - expect(queryThree.something).toBe('hello') - await browser - .elementByCss('#else') - .click() - .waitForElementByCss('#something-else') - await browser - .elementByCss('#hello2') - .click() - .waitForElementByCss('#nav-as-path-pushstate') - await browser.back().waitForElementByCss('#something-else') - const queryFour = JSON.parse( - await browser.elementByCss('#router-query').text() - ) - expect(queryFour.something).toBe(undefined) - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should detect asPath query changes correctly', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/nav/as-path-query') - await browser - .elementByCss('#hello') - .click() - .waitForElementByCss('#something-hello-something-hello') - const queryOne = JSON.parse( - await browser.elementByCss('#router-query').text() - ) - expect(queryOne.something).toBe('hello') - await browser - .elementByCss('#hello2') - .click() - .waitForElementByCss('#something-hello-something-else') - const queryTwo = JSON.parse( - await browser.elementByCss('#router-query').text() - ) - expect(queryTwo.something).toBe('else') - } finally { - if (browser) { - await browser.close() - } - } - }) - }) + expect(text).toBe('This is the passHref prop page.') + await browser.close() }) - describe('runtime errors', () => { - it('should show redbox when a client side error is thrown inside a component', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/error-inside-browser-page') - expect(await hasRedbox(browser)).toBe(true) - const text = await getRedboxSource(browser) - expect(text).toMatch(/An Expected error occurred/) - expect(text).toMatch( - /pages[\\/]error-inside-browser-page\.js \(5:13\)/ - ) - } finally { - if (browser) { - await browser.close() - } - } - }) + it('should redirect if passHref prop is defined in Link', async () => { + const browser = await webdriver(next.appPort, '/nav/pass-href-prop') + const text = await browser + .elementByCss('#with-href') + .click() + .waitForElementByCss('.nav-home') + .elementByCss('p') + .text() - it('should show redbox when a client side error is thrown outside a component', async () => { - let browser - try { - browser = await webdriver( - next.appPort, - '/error-in-the-browser-global-scope' - ) - expect(await hasRedbox(browser)).toBe(true) - const text = await getRedboxSource(browser) - expect(text).toMatch(/An Expected error occurred/) - expect(text).toMatch(/error-in-the-browser-global-scope\.js \(2:9\)/) - } finally { - if (browser) { - await browser.close() - } - } - }) + expect(text).toBe('This is the home.') + await browser.close() }) + }) - describe('with 404 pages', () => { - it('should 404 on not existent page', async () => { - const browser = await webdriver(next.appPort, '/non-existent') - expect(await browser.elementByCss('h1').text()).toBe('404') - expect(await browser.elementByCss('h2').text()).toBe( - 'This page could not be found.' - ) - await browser.close() - }) - - it('should 404 on wrong casing', async () => { - const browser = await webdriver(next.appPort, '/nAv/AbOuT') - expect(await browser.elementByCss('h1').text()).toBe('404') - expect(await browser.elementByCss('h2').text()).toBe( - 'This page could not be found.' - ) - await browser.close() - }) - - it('should get url dynamic param', async () => { - const browser = await webdriver( - next.appPort, - '/dynamic/dynamic-part/route' - ) - expect(await browser.elementByCss('p').text()).toBe('dynamic-part') - await browser.close() - }) - - it('should 404 on wrong casing of url dynamic param', async () => { - const browser = await webdriver( - next.appPort, - '/dynamic/dynamic-part/RoUtE' - ) - expect(await browser.elementByCss('h1').text()).toBe('404') - expect(await browser.elementByCss('h2').text()).toBe( - 'This page could not be found.' - ) - await browser.close() - }) - - it('should not 404 for /', async () => { - const browser = await webdriver(next.appPort, '/nav/about/') - const text = await browser.elementByCss('p').text() - expect(text).toBe('This is the about page.') - await browser.close() - }) - - it('should should not contain a page script in a 404 page', async () => { - const browser = await webdriver(next.appPort, '/non-existent') - const scripts = await browser.elementsByCss('script[src]') - for (const script of scripts) { - const src = await script.getAttribute('src') - expect(src.includes('/non-existent')).toBeFalsy() - } - await browser.close() - }) - }) - - describe('updating head while client routing', () => { - it('should only execute async and defer scripts once', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/head') - - await browser.waitForElementByCss('h1') - await waitFor(2000) - expect( - Number(await browser.eval('window.__test_async_executions')) - ).toBe(1) - expect( - Number(await browser.eval('window.__test_defer_executions')) - ).toBe(1) - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should warn when stylesheets or scripts are in head', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/head') - - await browser.waitForElementByCss('h1') - await waitFor(1000) - const browserLogs = await browser.log('browser') - let foundStyles = false - let foundScripts = false - const logs = [] - browserLogs.forEach(({ message }) => { - if (message.includes('Do not add stylesheets using next/head')) { - foundStyles = true - logs.push(message) - } - if (message.includes('Do not add "'` - ) - - expect(res.status).toBe(500) + function fetchWithPolicy(policy: string | null, reportOnly?: boolean) { + const cspKey = reportOnly + ? 'Content-Security-Policy-Report-Only' + : 'Content-Security-Policy' + return next.fetch('/dashboard', { + headers: policy + ? { + [cspKey]: policy, + } + : {}, }) } -) + + async function renderWithPolicy(policy: string | null, reportOnly?: boolean) { + const res = await fetchWithPolicy(policy, reportOnly) + + expect(res.ok).toBe(true) + + const html = await res.text() + + return cheerio.load(html) + } + + it('does not include nonce when not enabled', async () => { + const policies = [ + `script-src 'nonce-'`, // invalid nonce + 'style-src "nonce-cmFuZG9tCg=="', // no script or default src + '', // empty string + ] + + for (const policy of policies) { + const $ = await renderWithPolicy(policy) + + // Find all the script tags without src attributes and with nonce + // attributes. + const elements = $('script[nonce]:not([src])') + + // Expect there to be none. + expect(elements.length).toBe(0) + } + }) + + it('includes a nonce value with inline scripts when Content-Security-Policy header is defined', async () => { + // A random nonce value, base64 encoded. + const nonce = 'cmFuZG9tCg==' + + // Validate all the cases where we could parse the nonce. + const policies = [ + `script-src 'nonce-${nonce}'`, // base case + ` script-src 'nonce-${nonce}' `, // extra space added around sources and directive + `style-src 'self'; script-src 'nonce-${nonce}'`, // extra directives + `script-src 'self' 'nonce-${nonce}' 'nonce-othernonce'`, // extra nonces + `default-src 'nonce-othernonce'; script-src 'nonce-${nonce}';`, // script and then fallback case + `default-src 'nonce-${nonce}'`, // fallback case + ] + + for (const policy of policies) { + const $ = await renderWithPolicy(policy) + + // Find all the script tags without src attributes. + const elements = $('script:not([src])') + + // Expect there to be at least 1 script tag without a src attribute. + expect(elements.length).toBeGreaterThan(0) + + // Expect all inline scripts to have the nonce value. + elements.each((i, el) => { + expect(el.attribs['nonce']).toBe(nonce) + }) + } + }) + + it('includes a nonce value with inline scripts when Content-Security-Policy-Report-Only header is defined', async () => { + // A random nonce value, base64 encoded. + const nonce = 'cmFuZG9tCg==' + + // Validate all the cases where we could parse the nonce. + const policies = [ + `script-src 'nonce-${nonce}'`, // base case + ` script-src 'nonce-${nonce}' `, // extra space added around sources and directive + `style-src 'self'; script-src 'nonce-${nonce}'`, // extra directives + `script-src 'self' 'nonce-${nonce}' 'nonce-othernonce'`, // extra nonces + `default-src 'nonce-othernonce'; script-src 'nonce-${nonce}';`, // script and then fallback case + `default-src 'nonce-${nonce}'`, // fallback case + ] + + for (const policy of policies) { + const $ = await renderWithPolicy(policy, true) + + // Find all the script tags without src attributes. + const elements = $('script:not([src])') + + // Expect there to be at least 1 script tag without a src attribute. + expect(elements.length).toBeGreaterThan(0) + + // Expect all inline scripts to have the nonce value. + elements.each((i, el) => { + expect(el.attribs['nonce']).toBe(nonce) + }) + } + }) + + it('includes a nonce value with bootstrap scripts when Content-Security-Policy header is defined', async () => { + // A random nonce value, base64 encoded. + const nonce = 'cmFuZG9tCg==' + + // Validate all the cases where we could parse the nonce. + const policies = [ + `script-src 'nonce-${nonce}'`, // base case + ` script-src 'nonce-${nonce}' `, // extra space added around sources and directive + `style-src 'self'; script-src 'nonce-${nonce}'`, // extra directives + `script-src 'self' 'nonce-${nonce}' 'nonce-othernonce'`, // extra nonces + `default-src 'nonce-othernonce'; script-src 'nonce-${nonce}';`, // script and then fallback case + `default-src 'nonce-${nonce}'`, // fallback case + ] + + for (const policy of policies) { + const $ = await renderWithPolicy(policy) + + // Find all the script tags without src attributes. + const elements = $('script[src]') + + // Expect there to be at least 2 script tag with a src attribute. + // The main chunk and the webpack runtime. + expect(elements.length).toBeGreaterThan(1) + + // Expect all inline scripts to have the nonce value. + elements.each((i, el) => { + expect(el.attribs['nonce']).toBe(nonce) + }) + } + }) + + it('includes an integrity attribute on scripts', async () => { + const $ = await next.render$('/dashboard') + // Currently webpack chunks loaded via flight runtime do not get integrity + // hashes. This was previously unobservable in this test because these scripts + // are inserted by the webpack runtime and immediately removed from the document. + // However with the advent of preinitialization of chunks used during SSR there are + // some script tags for flight loaded chunks that will be part of the initial HTML + // but do not have integrity hashes. Flight does not currently support a way to + // provide integrity hashes for these chunks. When this is addressed in React upstream + // we can revisit this tests assertions and start to ensure it actually applies to + // all SSR'd scripts. For now we will look for known entrypoint scripts and assume + // everything else in the is part of flight loaded chunks + + // Collect all the scripts with integrity hashes so we can verify them. + const files: Map = new Map() + + function assertHasIntegrity(el: CheerioElement) { + const integrity = el.attribs['integrity'] + expect(integrity).toBeDefined() + expect(integrity).toStartWith('sha256-') + + const src = el.attribs['src'] + expect(src).toBeDefined() + + files.set(src, integrity) + } + + // scripts are most entrypoint scripts, polyfills, and flight loaded scripts. + // Since we currently cannot assert integrity on flight loaded scripts (they do not have it) + // We have to target specific expected entrypoint/polyfill scripts and assert them directly + const mainScript = $('head script[src^="/_next/static/chunks/main-app"]') + expect(mainScript.length).toBe(1) + assertHasIntegrity(mainScript.get(0)) + + const polyfillsScript = $( + 'head script[src^="/_next/static/chunks/polyfills"]' + ) + expect(polyfillsScript.length).toBe(1) + assertHasIntegrity(polyfillsScript.get(0)) + + // body scripts should include just the bootstrap script. We assert that all body + // scripts have integrity because we don't expect any flight loaded scripts to appear + // here + const bodyScripts = $('body script[src]') + expect(bodyScripts.length).toBeGreaterThan(0) + bodyScripts.each((i, el) => { + assertHasIntegrity(el) + }) + + // For each script tag, ensure that the integrity attribute is the + // correct hash of the script tag. + for (const [src, integrity] of files) { + const res = await next.fetch(src) + expect(res.status).toBe(200) + const content = await res.text() + + const hash = crypto + .createHash('sha256') + .update(content) + .digest() + .toString('base64') + + expect(integrity).toEndWith(hash) + } + }) + + it('throws when escape characters are included in nonce', async () => { + const res = await fetchWithPolicy(`script-src 'nonce-">"'`) + + expect(res.status).toBe(500) + }) +}) diff --git a/test/production/app-dir/symbolic-file-links/symbolic-file-links.test.ts b/test/production/app-dir/symbolic-file-links/symbolic-file-links.test.ts index 9f337b129a..2510d95e8d 100644 --- a/test/production/app-dir/symbolic-file-links/symbolic-file-links.test.ts +++ b/test/production/app-dir/symbolic-file-links/symbolic-file-links.test.ts @@ -1,34 +1,32 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' -createNextDescribe( - 'symbolic-file-links', - { +describe('symbolic-file-links', () => { + const { next } = nextTestSetup({ files: __dirname, - }, - ({ next }) => { - // Recommended for tests that check HTML. Cheerio is a HTML parser that has a jQuery like API. - it('should work using cheerio', async () => { - const $ = await next.render$('/') - expect($('p').text()).toBe('hello world') - }) + }) - // Recommended for tests that need a full browser - it('should work using browser', async () => { - const browser = await next.browser('/') - expect(await browser.elementByCss('p').text()).toBe('hello world') - }) + // Recommended for tests that check HTML. Cheerio is a HTML parser that has a jQuery like API. + it('should work using cheerio', async () => { + const $ = await next.render$('/') + expect($('p').text()).toBe('hello world') + }) - // In case you need the full HTML. Can also use $.html() with cheerio. - it('should work with html', async () => { - const html = await next.render('/') - expect(html).toContain('hello world') - }) + // Recommended for tests that need a full browser + it('should work using browser', async () => { + const browser = await next.browser('/') + expect(await browser.elementByCss('p').text()).toBe('hello world') + }) - // In case you need to test the response object - it('should work with fetch', async () => { - const res = await next.fetch('/') - const html = await res.text() - expect(html).toContain('hello world') - }) - } -) + // In case you need the full HTML. Can also use $.html() with cheerio. + it('should work with html', async () => { + const html = await next.render('/') + expect(html).toContain('hello world') + }) + + // In case you need to test the response object + it('should work with fetch', async () => { + const res = await next.fetch('/') + const html = await res.text() + expect(html).toContain('hello world') + }) +}) diff --git a/test/production/app-dir/unexpected-error/unexpected-error.test.ts b/test/production/app-dir/unexpected-error/unexpected-error.test.ts index 7a293c3aff..eca9f433f0 100644 --- a/test/production/app-dir/unexpected-error/unexpected-error.test.ts +++ b/test/production/app-dir/unexpected-error/unexpected-error.test.ts @@ -1,26 +1,24 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' -createNextDescribe( - 'unexpected-error', - { +describe('unexpected-error', () => { + const { next } = nextTestSetup({ files: __dirname, - }, - ({ next }) => { - it('should set response status to 500 for unexpected errors in ssr app route', async () => { - const res = await next.fetch('/ssr-unexpected-error?error=true') - expect(res.status).toBe(500) - }) + }) - it('cannot change response status when streaming has started', async () => { - const res = await next.fetch( - '/ssr-unexpected-error-after-streaming?error=true' - ) - expect(res.status).toBe(200) - }) + it('should set response status to 500 for unexpected errors in ssr app route', async () => { + const res = await next.fetch('/ssr-unexpected-error?error=true') + expect(res.status).toBe(500) + }) - it('should set response status to 500 for unexpected errors in isr app route', async () => { - const res = await next.fetch('/isr-unexpected-error?error=true') - expect(res.status).toBe(500) - }) - } -) + it('cannot change response status when streaming has started', async () => { + const res = await next.fetch( + '/ssr-unexpected-error-after-streaming?error=true' + ) + expect(res.status).toBe(200) + }) + + it('should set response status to 500 for unexpected errors in isr app route', async () => { + const res = await next.fetch('/isr-unexpected-error?error=true') + expect(res.status).toBe(500) + }) +}) diff --git a/test/production/custom-server/custom-server.test.ts b/test/production/custom-server/custom-server.test.ts index 6b1a6a1f99..92ecb3cec9 100644 --- a/test/production/custom-server/custom-server.test.ts +++ b/test/production/custom-server/custom-server.test.ts @@ -1,54 +1,50 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' -createNextDescribe( - 'custom server', - { +describe('custom server', () => { + const { next } = nextTestSetup({ files: __dirname, startCommand: 'node server.js', dependencies: { 'get-port': '5.1.1', }, - }, - ({ next }) => { - it.each(['a', 'b', 'c'])('can navigate to /%s', async (page) => { - const $ = await next.render$(`/${page}`) - expect($('p').text()).toBe(`Page ${page}`) + }) + + it.each(['a', 'b', 'c'])('can navigate to /%s', async (page) => { + const $ = await next.render$(`/${page}`) + expect($('p').text()).toBe(`Page ${page}`) + }) + + it('should log any error messages when server is started without "quiet" setting', async () => { + await next.render(`/error`) + expect(next.cliOutput).toInclude('Server side error') + }) + + describe('with app dir', () => { + it('should render app with react canary', async () => { + const $ = await next.render$(`/1`) + expect($('body').text()).toMatch(/app: .+-canary/) }) - it('should log any error messages when server is started without "quiet" setting', async () => { - await next.render(`/error`) - expect(next.cliOutput).toInclude('Server side error') + it('should not render pages with react canary', async () => { + const $ = await next.render$(`/2`) + expect($('body').text()).toMatch(/pages:/) + expect($('body').text()).not.toMatch(/canary/) }) + }) +}) - describe('with app dir', () => { - it('should render app with react canary', async () => { - const $ = await next.render$(`/1`) - expect($('body').text()).toMatch(/app: .+-canary/) - }) - - it('should not render pages with react canary', async () => { - const $ = await next.render$(`/2`) - expect($('body').text()).toMatch(/pages:/) - expect($('body').text()).not.toMatch(/canary/) - }) - }) - } -) - -createNextDescribe( - 'custom server with quiet setting', - { +describe('custom server with quiet setting', () => { + const { next } = nextTestSetup({ files: __dirname, startCommand: 'node server.js', env: { USE_QUIET: 'true' }, dependencies: { 'get-port': '5.1.1', }, - }, - ({ next }) => { - it('should not log any error messages when server is started with "quiet" setting', async () => { - await next.render(`/error`) - expect(next.cliOutput).not.toInclude('Server side error') - }) - } -) + }) + + it('should not log any error messages when server is started with "quiet" setting', async () => { + await next.render(`/error`) + expect(next.cliOutput).not.toInclude('Server side error') + }) +}) diff --git a/test/production/error-hydration/error-hydration.test.ts b/test/production/error-hydration/error-hydration.test.ts index 733b29dcee..1ff83ec4bf 100644 --- a/test/production/error-hydration/error-hydration.test.ts +++ b/test/production/error-hydration/error-hydration.test.ts @@ -1,4 +1,4 @@ -import { NextInstance, createNextDescribe } from 'e2e-utils' +import { NextInstance, nextTestSetup } from 'e2e-utils' async function setupErrorHydrationTests( next: NextInstance, @@ -17,79 +17,77 @@ async function setupErrorHydrationTests( return [browser, consoleMessages] as const } -createNextDescribe( - 'error-hydration', - { +describe('error-hydration', () => { + const { next } = nextTestSetup({ files: __dirname, - }, - ({ next }) => { - // Recommended for tests that need a full browser - it('should log no error messages for server-side errors', async () => { - const [, consoleMessages] = await setupErrorHydrationTests( - next, - '/with-error' + }) + + // Recommended for tests that need a full browser + it('should log no error messages for server-side errors', async () => { + const [, consoleMessages] = await setupErrorHydrationTests( + next, + '/with-error' + ) + + expect( + consoleMessages.find((message) => + message.startsWith('A client-side exception has occurred') ) + ).toBeUndefined() - expect( - consoleMessages.find((message) => - message.startsWith('A client-side exception has occurred') - ) - ).toBeUndefined() - - expect( - consoleMessages.find( - (message) => - message === - '{name: Internal Server Error., message: 500 - Internal Server Error., statusCode: 500}' - ) - ).toBeUndefined() - }) - - it('should not invoke the error page getInitialProps client-side for server-side errors', async () => { - const [b] = await setupErrorHydrationTests(next, '/with-error') - - expect( - await b.eval( - () => - (window as any).__ERROR_PAGE_GET_INITIAL_PROPS_INVOKED_CLIENT_SIDE__ - ) - ).toBe(undefined) - }) - - it('should log an message for client-side errors, including the full, custom error', async () => { - const [browser, consoleMessages] = await setupErrorHydrationTests( - next, - '/no-error' + expect( + consoleMessages.find( + (message) => + message === + '{name: Internal Server Error., message: 500 - Internal Server Error., statusCode: 500}' ) + ).toBeUndefined() + }) - const link = await browser.elementByCss('a') - await link.click() + it('should not invoke the error page getInitialProps client-side for server-side errors', async () => { + const [b] = await setupErrorHydrationTests(next, '/with-error') - expect( - consoleMessages.some((m) => m.includes('Error: custom error')) - ).toBe(true) + expect( + await b.eval( + () => + (window as any).__ERROR_PAGE_GET_INITIAL_PROPS_INVOKED_CLIENT_SIDE__ + ) + ).toBe(undefined) + }) - expect( - consoleMessages.some((m) => - m.includes( - 'A client-side exception has occurred, see here for more info' - ) + it('should log an message for client-side errors, including the full, custom error', async () => { + const [browser, consoleMessages] = await setupErrorHydrationTests( + next, + '/no-error' + ) + + const link = await browser.elementByCss('a') + await link.click() + + expect(consoleMessages.some((m) => m.includes('Error: custom error'))).toBe( + true + ) + + expect( + consoleMessages.some((m) => + m.includes( + 'A client-side exception has occurred, see here for more info' ) - ).toBe(true) - }) + ) + ).toBe(true) + }) - it("invokes _error's getInitialProps for client-side errors", async () => { - const [browser] = await setupErrorHydrationTests(next, '/no-error') + it("invokes _error's getInitialProps for client-side errors", async () => { + const [browser] = await setupErrorHydrationTests(next, '/no-error') - const link = await browser.elementByCss('a') - await link.click() + const link = await browser.elementByCss('a') + await link.click() - expect( - await browser.eval( - () => - (window as any).__ERROR_PAGE_GET_INITIAL_PROPS_INVOKED_CLIENT_SIDE__ - ) - ).toBe(true) - }) - } -) + expect( + await browser.eval( + () => + (window as any).__ERROR_PAGE_GET_INITIAL_PROPS_INVOKED_CLIENT_SIDE__ + ) + ).toBe(true) + }) +}) diff --git a/test/production/export/index.test.ts b/test/production/export/index.test.ts index b0562d6838..336de74d77 100644 --- a/test/production/export/index.test.ts +++ b/test/production/export/index.test.ts @@ -1,5 +1,5 @@ import path from 'path' -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { renderViaHTTP, startStaticServer, @@ -10,497 +10,492 @@ import { AddressInfo, Server } from 'net' import cheerio from 'cheerio' import webdriver from 'next-webdriver' -createNextDescribe( - 'static export', - { +describe('static export', () => { + const { next, skipped } = nextTestSetup({ files: __dirname, skipStart: true, - }, - ({ next }) => { - const nextConfigPath = 'next.config.js' - const outdir = 'out' - const outNoTrailSlash = 'outNoTrailSlash' - let server: Server - let port: number - let serverNoTrailSlash: Server - let portNoTrailSlash: number + }) - beforeAll(async () => { + if (skipped) { + return + } + + const nextConfigPath = 'next.config.js' + const outdir = 'out' + const outNoTrailSlash = 'outNoTrailSlash' + let server: Server + let port: number + let serverNoTrailSlash: Server + let portNoTrailSlash: number + + beforeAll(async () => { + const nextConfig = await next.readFile(nextConfigPath) + await next.build() + + await next.patchFile( + nextConfigPath, + nextConfig + .replace(`trailingSlash: true`, `trailingSlash: false`) + .replace(`distDir: 'out'`, `distDir: '${outNoTrailSlash}'`) + ) + await next.build() + await next.patchFile(nextConfigPath, nextConfig) + + server = await startStaticServer(path.join(next.testDir, outdir)) + serverNoTrailSlash = await startStaticServer( + path.join(next.testDir, outNoTrailSlash) + ) + port = (server.address() as AddressInfo).port + portNoTrailSlash = (serverNoTrailSlash.address() as AddressInfo).port + }) + + afterAll(async () => { + await Promise.all([ + new Promise((resolve) => server.close(resolve)), + new Promise((resolve) => serverNoTrailSlash.close(resolve)), + ]) + }) + + it('should delete existing exported files', async () => { + const tmpOutDir = 'tmpOutDir' + const tempfile = path.join(tmpOutDir, 'temp.txt') + await next.patchFile(tempfile, 'test') + const nextConfig = await next.readFile(nextConfigPath) + await next.patchFile( + nextConfigPath, + nextConfig.replace(`distDir: 'out'`, `distDir: '${tmpOutDir}'`) + ) + await next.build() + await next.patchFile(nextConfigPath, nextConfig) + await expect(next.readFile(tempfile)).rejects.toThrow() + }) + + const fileExist = async (file: string) => + await next + .readFile(file) + .then(() => true) + .catch(() => false) + + it('should honor trailingSlash for 404 page', async () => { + expect(await fileExist(path.join(outdir, '404/index.html'))).toBe(true) + + // we still output 404.html for backwards compat + expect(await fileExist(path.join(outdir, '404.html'))).toBe(true) + }) + + it('should handle trailing slash in getStaticPaths', async () => { + expect(await fileExist(path.join(outdir, 'gssp/foo/index.html'))).toBe(true) + + expect(await fileExist(path.join(outNoTrailSlash, 'gssp/foo.html'))).toBe( + true + ) + }) + + it('should only output 404.html without trailingSlash', async () => { + expect(await fileExist(path.join(outNoTrailSlash, '404/index.html'))).toBe( + false + ) + + expect(await fileExist(path.join(outNoTrailSlash, '404.html'))).toBe(true) + }) + + it('should not duplicate /index with trailingSlash', async () => { + expect(await fileExist(path.join(outdir, 'index/index.html'))).toBe(false) + + expect(await fileExist(path.join(outdir, 'index.html'))).toBe(true) + }) + + describe('Dynamic routes export', () => { + it('Should throw error not matched route', async () => { + const outdir = 'outDynamic' const nextConfig = await next.readFile(nextConfigPath) - await next.build() - await next.patchFile( nextConfigPath, nextConfig - .replace(`trailingSlash: true`, `trailingSlash: false`) - .replace(`distDir: 'out'`, `distDir: '${outNoTrailSlash}'`) + .replace('/blog/nextjs/comment/test', '/bad/path') + .replace(`distDir: 'out'`, `distDir: '${outdir}'`) ) - await next.build() + const { cliOutput } = await next.build() await next.patchFile(nextConfigPath, nextConfig) - server = await startStaticServer(path.join(next.testDir, outdir)) - serverNoTrailSlash = await startStaticServer( - path.join(next.testDir, outNoTrailSlash) - ) - port = (server.address() as AddressInfo).port - portNoTrailSlash = (serverNoTrailSlash.address() as AddressInfo).port - }) - - afterAll(async () => { - await Promise.all([ - new Promise((resolve) => server.close(resolve)), - new Promise((resolve) => serverNoTrailSlash.close(resolve)), - ]) - }) - - it('should delete existing exported files', async () => { - const tmpOutDir = 'tmpOutDir' - const tempfile = path.join(tmpOutDir, 'temp.txt') - await next.patchFile(tempfile, 'test') - const nextConfig = await next.readFile(nextConfigPath) - await next.patchFile( - nextConfigPath, - nextConfig.replace(`distDir: 'out'`, `distDir: '${tmpOutDir}'`) - ) - await next.build() - await next.patchFile(nextConfigPath, nextConfig) - await expect(next.readFile(tempfile)).rejects.toThrow() - }) - - const fileExist = async (file: string) => - await next - .readFile(file) - .then(() => true) - .catch(() => false) - - it('should honor trailingSlash for 404 page', async () => { - expect(await fileExist(path.join(outdir, '404/index.html'))).toBe(true) - - // we still output 404.html for backwards compat - expect(await fileExist(path.join(outdir, '404.html'))).toBe(true) - }) - - it('should handle trailing slash in getStaticPaths', async () => { - expect(await fileExist(path.join(outdir, 'gssp/foo/index.html'))).toBe( - true - ) - - expect(await fileExist(path.join(outNoTrailSlash, 'gssp/foo.html'))).toBe( - true + expect(cliOutput).toContain( + 'https://nextjs.org/docs/messages/export-path-mismatch' ) }) + }) - it('should only output 404.html without trailingSlash', async () => { - expect( - await fileExist(path.join(outNoTrailSlash, '404/index.html')) - ).toBe(false) + describe('Render via browser', () => { + it('should render the home page', async () => { + const browser = await webdriver(port, '/') + const text = await browser.elementByCss('#home-page p').text() - expect(await fileExist(path.join(outNoTrailSlash, '404.html'))).toBe(true) + expect(text).toBe('This is the home page') + await browser.close() }) - it('should not duplicate /index with trailingSlash', async () => { - expect(await fileExist(path.join(outdir, 'index/index.html'))).toBe(false) + it('should add trailing slash on Link', async () => { + const browser = await webdriver(port, '/') + const link = await browser + .elementByCss('#about-via-link') + .getAttribute('href') - expect(await fileExist(path.join(outdir, 'index.html'))).toBe(true) + expect(link.slice(-1)).toBe('/') }) - describe('Dynamic routes export', () => { - it('Should throw error not matched route', async () => { - const outdir = 'outDynamic' - const nextConfig = await next.readFile(nextConfigPath) - await next.patchFile( - nextConfigPath, - nextConfig - .replace('/blog/nextjs/comment/test', '/bad/path') - .replace(`distDir: 'out'`, `distDir: '${outdir}'`) - ) - const { cliOutput } = await next.build() - await next.patchFile(nextConfigPath, nextConfig) + it('should not add any slash on hash Link', async () => { + const browser = await webdriver(port, '/hash-link') + const link = await browser.elementByCss('#hash-link').getAttribute('href') - expect(cliOutput).toContain( - 'https://nextjs.org/docs/messages/export-path-mismatch' - ) - }) + expect(link).toMatch(/\/hash-link\/#hash$/) }) - describe('Render via browser', () => { + it('should preserve hash symbol on empty hash Link', async () => { + const browser = await webdriver(port, '/empty-hash-link') + const link = await browser + .elementByCss('#empty-hash-link') + .getAttribute('href') + + expect(link).toMatch(/\/hello\/#$/) + }) + + it('should preserve question mark on empty query Link', async () => { + const browser = await webdriver(port, '/empty-query-link') + const link = await browser + .elementByCss('#empty-query-link') + .getAttribute('href') + + expect(link).toMatch(/\/hello\/\?$/) + }) + + it('should not add trailing slash on Link when disabled', async () => { + const browser = await webdriver(portNoTrailSlash, '/') + const link = await browser + .elementByCss('#about-via-link') + .getAttribute('href') + + expect(link.slice(-1)).not.toBe('/') + }) + + it('should do navigations via Link', async () => { + const browser = await webdriver(port, '/') + const text = await browser + .elementByCss('#about-via-link') + .click() + .waitForElementByCss('#about-page') + .elementByCss('#about-page p') + .text() + + expect(text).toBe('This is the About page foo') + await browser.close() + }) + + it('should do navigations via Router', async () => { + const browser = await webdriver(port, '/') + const text = await browser + .elementByCss('#about-via-router') + .click() + .waitForElementByCss('#about-page') + .elementByCss('#about-page p') + .text() + + expect(text).toBe('This is the About page foo') + await browser.close() + }) + + it('should do run client side javascript', async () => { + const browser = await webdriver(port, '/') + const text = await browser + .elementByCss('#counter') + .click() + .waitForElementByCss('#counter-page') + .elementByCss('#counter-increase') + .click() + .elementByCss('#counter-increase') + .click() + .elementByCss('#counter-page p') + .text() + + expect(text).toBe('Counter: 2') + await browser.close() + }) + + it('should render pages using getInitialProps', async () => { + const browser = await webdriver(port, '/') + const text = await browser + .elementByCss('#get-initial-props') + .click() + .waitForElementByCss('#dynamic-page') + .elementByCss('#dynamic-page p') + .text() + + expect(text).toBe('cool dynamic text') + await browser.close() + }) + + it('should render dynamic pages with custom urls', async () => { + const browser = await webdriver(port, '/') + const text = await browser + .elementByCss('#dynamic-1') + .click() + .waitForElementByCss('#dynamic-page') + .elementByCss('#dynamic-page p') + .text() + + expect(text).toBe('next export is nice') + await browser.close() + }) + + it('should support client side navigation', async () => { + const browser = await webdriver(port, '/') + const text = await browser + .elementByCss('#counter') + .click() + .waitForElementByCss('#counter-page') + .elementByCss('#counter-increase') + .click() + .elementByCss('#counter-increase') + .click() + .elementByCss('#counter-page p') + .text() + + expect(text).toBe('Counter: 2') + + // let's go back and come again to this page: + const textNow = await browser + .elementByCss('#go-back') + .click() + .waitForElementByCss('#home-page') + .elementByCss('#counter') + .click() + .waitForElementByCss('#counter-page') + .elementByCss('#counter-page p') + .text() + + expect(textNow).toBe('Counter: 2') + + await browser.close() + }) + + it('should render dynamic import components in the client', async () => { + const browser = await webdriver(port, '/') + await browser + .elementByCss('#dynamic-imports-link') + .click() + .waitForElementByCss('#dynamic-imports-page') + + await check( + () => getBrowserBodyText(browser), + /Welcome to dynamic imports/ + ) + + await browser.close() + }) + + it('should render pages with url hash correctly', async () => { + let browser + try { + browser = await webdriver(port, '/') + + // Check for the query string content + const text = await browser + .elementByCss('#with-hash') + .click() + .waitForElementByCss('#dynamic-page') + .elementByCss('#dynamic-page p') + .text() + + expect(text).toBe('Vercel is awesome') + + await check(() => browser.elementByCss('#hash').text(), /cool/) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should render 404 when visiting a page that returns notFound from gsp', async () => { + let browser + try { + browser = await webdriver(port, '/') + + const text = await browser + .elementByCss('#gsp-notfound-link') + .click() + .waitForElementByCss('pre') + .elementByCss('pre') + .text() + + expect(text).toBe('Cannot GET /gsp-notfound/') + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should navigate even if used a button inside ', async () => { + const browser = await webdriver(port, '/button-link') + + const text = await browser + .elementByCss('button') + .click() + .waitForElementByCss('#home-page') + .elementByCss('#home-page p') + .text() + + expect(text).toBe('This is the home page') + await browser.close() + }) + + it('should update query after mount', async () => { + const browser = await webdriver(port, '/query-update?hello=world') + const query = await browser.elementByCss('#query').text() + expect(JSON.parse(query)).toEqual({ hello: 'world', a: 'blue' }) + await browser.close() + }) + + describe('pages in the nested level: level1', () => { it('should render the home page', async () => { const browser = await webdriver(port, '/') - const text = await browser.elementByCss('#home-page p').text() - expect(text).toBe('This is the home page') - await browser.close() - }) - - it('should add trailing slash on Link', async () => { - const browser = await webdriver(port, '/') - const link = await browser - .elementByCss('#about-via-link') - .getAttribute('href') - - expect(link.slice(-1)).toBe('/') - }) - - it('should not add any slash on hash Link', async () => { - const browser = await webdriver(port, '/hash-link') - const link = await browser - .elementByCss('#hash-link') - .getAttribute('href') - - expect(link).toMatch(/\/hash-link\/#hash$/) - }) - - it('should preserve hash symbol on empty hash Link', async () => { - const browser = await webdriver(port, '/empty-hash-link') - const link = await browser - .elementByCss('#empty-hash-link') - .getAttribute('href') - - expect(link).toMatch(/\/hello\/#$/) - }) - - it('should preserve question mark on empty query Link', async () => { - const browser = await webdriver(port, '/empty-query-link') - const link = await browser - .elementByCss('#empty-query-link') - .getAttribute('href') - - expect(link).toMatch(/\/hello\/\?$/) - }) - - it('should not add trailing slash on Link when disabled', async () => { - const browser = await webdriver(portNoTrailSlash, '/') - const link = await browser - .elementByCss('#about-via-link') - .getAttribute('href') - - expect(link.slice(-1)).not.toBe('/') - }) - - it('should do navigations via Link', async () => { - const browser = await webdriver(port, '/') - const text = await browser - .elementByCss('#about-via-link') - .click() - .waitForElementByCss('#about-page') - .elementByCss('#about-page p') - .text() - - expect(text).toBe('This is the About page foo') - await browser.close() - }) - - it('should do navigations via Router', async () => { - const browser = await webdriver(port, '/') - const text = await browser - .elementByCss('#about-via-router') - .click() - .waitForElementByCss('#about-page') - .elementByCss('#about-page p') - .text() - - expect(text).toBe('This is the About page foo') - await browser.close() - }) - - it('should do run client side javascript', async () => { - const browser = await webdriver(port, '/') - const text = await browser - .elementByCss('#counter') - .click() - .waitForElementByCss('#counter-page') - .elementByCss('#counter-increase') - .click() - .elementByCss('#counter-increase') - .click() - .elementByCss('#counter-page p') - .text() - - expect(text).toBe('Counter: 2') - await browser.close() - }) - - it('should render pages using getInitialProps', async () => { - const browser = await webdriver(port, '/') - const text = await browser - .elementByCss('#get-initial-props') - .click() - .waitForElementByCss('#dynamic-page') - .elementByCss('#dynamic-page p') - .text() - - expect(text).toBe('cool dynamic text') - await browser.close() - }) - - it('should render dynamic pages with custom urls', async () => { - const browser = await webdriver(port, '/') - const text = await browser - .elementByCss('#dynamic-1') - .click() - .waitForElementByCss('#dynamic-page') - .elementByCss('#dynamic-page p') - .text() - - expect(text).toBe('next export is nice') - await browser.close() - }) - - it('should support client side navigation', async () => { - const browser = await webdriver(port, '/') - const text = await browser - .elementByCss('#counter') - .click() - .waitForElementByCss('#counter-page') - .elementByCss('#counter-increase') - .click() - .elementByCss('#counter-increase') - .click() - .elementByCss('#counter-page p') - .text() - - expect(text).toBe('Counter: 2') - - // let's go back and come again to this page: - const textNow = await browser - .elementByCss('#go-back') - .click() - .waitForElementByCss('#home-page') - .elementByCss('#counter') - .click() - .waitForElementByCss('#counter-page') - .elementByCss('#counter-page p') - .text() - - expect(textNow).toBe('Counter: 2') - - await browser.close() - }) - - it('should render dynamic import components in the client', async () => { - const browser = await webdriver(port, '/') - await browser - .elementByCss('#dynamic-imports-link') - .click() - .waitForElementByCss('#dynamic-imports-page') + await browser.eval( + 'document.getElementById("level1-home-page").click()' + ) await check( () => getBrowserBodyText(browser), - /Welcome to dynamic imports/ + /This is the Level1 home page/ ) await browser.close() }) - it('should render pages with url hash correctly', async () => { - let browser - try { - browser = await webdriver(port, '/') - - // Check for the query string content - const text = await browser - .elementByCss('#with-hash') - .click() - .waitForElementByCss('#dynamic-page') - .elementByCss('#dynamic-page p') - .text() - - expect(text).toBe('Vercel is awesome') - - await check(() => browser.elementByCss('#hash').text(), /cool/) - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should render 404 when visiting a page that returns notFound from gsp', async () => { - let browser - try { - browser = await webdriver(port, '/') - - const text = await browser - .elementByCss('#gsp-notfound-link') - .click() - .waitForElementByCss('pre') - .elementByCss('pre') - .text() - - expect(text).toBe('Cannot GET /gsp-notfound/') - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should navigate even if used a button inside ', async () => { - const browser = await webdriver(port, '/button-link') - - const text = await browser - .elementByCss('button') - .click() - .waitForElementByCss('#home-page') - .elementByCss('#home-page p') - .text() - - expect(text).toBe('This is the home page') - await browser.close() - }) - - it('should update query after mount', async () => { - const browser = await webdriver(port, '/query-update?hello=world') - const query = await browser.elementByCss('#query').text() - expect(JSON.parse(query)).toEqual({ hello: 'world', a: 'blue' }) - await browser.close() - }) - - describe('pages in the nested level: level1', () => { - it('should render the home page', async () => { - const browser = await webdriver(port, '/') - - await browser.eval( - 'document.getElementById("level1-home-page").click()' - ) - - await check( - () => getBrowserBodyText(browser), - /This is the Level1 home page/ - ) - - await browser.close() - }) - - it('should render the about page', async () => { - const browser = await webdriver(port, '/') - - await browser.eval( - 'document.getElementById("level1-about-page").click()' - ) - - await check( - () => getBrowserBodyText(browser), - /This is the Level1 about page/ - ) - - await browser.close() - }) - }) - }) - - describe('Render via SSR', () => { - it('should render the home page', async () => { - const html = await renderViaHTTP(port, '/') - expect(html).toMatch(/This is the home page/) - }) - it('should render the about page', async () => { - const html = await renderViaHTTP(port, '/about') - expect(html).toMatch(/This is the About page foobar/) - }) + const browser = await webdriver(port, '/') - it('should render links correctly', async () => { - const html = await renderViaHTTP(port, '/') - const $ = cheerio.load(html) - const dynamicLink = $('#dynamic-1').prop('href') - const filePathLink = $('#path-with-extension').prop('href') - expect(dynamicLink).toEqual('/dynamic/one/') - expect(filePathLink).toEqual('/file-name.md') - }) - - it('should render a page with getInitialProps', async () => { - const html = await renderViaHTTP(port, '/dynamic') - expect(html).toMatch(/cool dynamic text/) - }) - - it('should render a dynamically rendered custom url page', async () => { - const html = await renderViaHTTP(port, '/dynamic/one') - expect(html).toMatch(/next export is nice/) - }) - - it('should render pages with dynamic imports', async () => { - const html = await renderViaHTTP(port, '/dynamic-imports') - expect(html).toMatch(/Welcome to dynamic imports/) - }) - - it('should render paths with extensions', async () => { - const html = await renderViaHTTP(port, '/file-name.md') - expect(html).toMatch(/this file has an extension/) - }) - - it('should give empty object for query if there is no query', async () => { - const html = await renderViaHTTP( - port, - '/get-initial-props-with-no-query' + await browser.eval( + 'document.getElementById("level1-about-page").click()' ) - expect(html).toMatch(/Query is: {}/) - }) - it('should render _error on 404.html even if not provided in exportPathMap', async () => { - const html = await renderViaHTTP(port, '/404.html') - // The default error page from the test server - // contains "404", so need to be specific here - expect(html).toMatch(/404.*page.*not.*found/i) - }) + await check( + () => getBrowserBodyText(browser), + /This is the Level1 about page/ + ) - // since exportTrailingSlash is enabled we should allow this - it('should render _error on /404/index.html', async () => { - const html = await renderViaHTTP(port, '/404/index.html') - // The default error page from the test server - // contains "404", so need to be specific here - expect(html).toMatch(/404.*page.*not.*found/i) - }) - - it('Should serve static files', async () => { - const data = await renderViaHTTP(port, '/static/data/item.txt') - expect(data).toBe('item') - }) - - it('Should serve public files', async () => { - const html = await renderViaHTTP(port, '/about') - const data = await renderViaHTTP(port, '/about/data.txt') - expect(html).toMatch(/This is the About page foobar/) - expect(data).toBe('data') - }) - - it('Should render dynamic files with query', async () => { - const html = await renderViaHTTP(port, '/blog/nextjs/comment/test') - expect(html).toMatch(/Blog post nextjs comment test/) + await browser.close() }) }) + }) - describe('API routes export', () => { - it('Should throw if a route is matched', async () => { - const outdir = 'outApi' - const nextConfig = await next.readFile(nextConfigPath) - await next.patchFile( - nextConfigPath, - nextConfig - .replace('// API route', `'/data': { page: '/api/data' },`) - .replace(`distDir: 'out'`, `distDir: '${outdir}'`) - ) - const { cliOutput } = await next.build() - await next.patchFile(nextConfigPath, nextConfig) - - expect(cliOutput).toContain( - 'https://nextjs.org/docs/messages/api-routes-static-export' - ) - }) + describe('Render via SSR', () => { + it('should render the home page', async () => { + const html = await renderViaHTTP(port, '/') + expect(html).toMatch(/This is the home page/) }) - it('exportTrailingSlash is not ignored', async () => { + it('should render the about page', async () => { + const html = await renderViaHTTP(port, '/about') + expect(html).toMatch(/This is the About page foobar/) + }) + + it('should render links correctly', async () => { + const html = await renderViaHTTP(port, '/') + const $ = cheerio.load(html) + const dynamicLink = $('#dynamic-1').prop('href') + const filePathLink = $('#path-with-extension').prop('href') + expect(dynamicLink).toEqual('/dynamic/one/') + expect(filePathLink).toEqual('/file-name.md') + }) + + it('should render a page with getInitialProps', async () => { + const html = await renderViaHTTP(port, '/dynamic') + expect(html).toMatch(/cool dynamic text/) + }) + + it('should render a dynamically rendered custom url page', async () => { + const html = await renderViaHTTP(port, '/dynamic/one') + expect(html).toMatch(/next export is nice/) + }) + + it('should render pages with dynamic imports', async () => { + const html = await renderViaHTTP(port, '/dynamic-imports') + expect(html).toMatch(/Welcome to dynamic imports/) + }) + + it('should render paths with extensions', async () => { + const html = await renderViaHTTP(port, '/file-name.md') + expect(html).toMatch(/this file has an extension/) + }) + + it('should give empty object for query if there is no query', async () => { + const html = await renderViaHTTP(port, '/get-initial-props-with-no-query') + expect(html).toMatch(/Query is: {}/) + }) + + it('should render _error on 404.html even if not provided in exportPathMap', async () => { + const html = await renderViaHTTP(port, '/404.html') + // The default error page from the test server + // contains "404", so need to be specific here + expect(html).toMatch(/404.*page.*not.*found/i) + }) + + // since exportTrailingSlash is enabled we should allow this + it('should render _error on /404/index.html', async () => { + const html = await renderViaHTTP(port, '/404/index.html') + // The default error page from the test server + // contains "404", so need to be specific here + expect(html).toMatch(/404.*page.*not.*found/i) + }) + + it('Should serve static files', async () => { + const data = await renderViaHTTP(port, '/static/data/item.txt') + expect(data).toBe('item') + }) + + it('Should serve public files', async () => { + const html = await renderViaHTTP(port, '/about') + const data = await renderViaHTTP(port, '/about/data.txt') + expect(html).toMatch(/This is the About page foobar/) + expect(data).toBe('data') + }) + + it('Should render dynamic files with query', async () => { + const html = await renderViaHTTP(port, '/blog/nextjs/comment/test') + expect(html).toMatch(/Blog post nextjs comment test/) + }) + }) + + describe('API routes export', () => { + it('Should throw if a route is matched', async () => { + const outdir = 'outApi' const nextConfig = await next.readFile(nextConfigPath) - const tmpOutdir = 'exportTrailingSlash-out' await next.patchFile( nextConfigPath, nextConfig - .replace(`trailingSlash: true`, `exportTrailingSlash: true`) - .replace(`distDir: 'out'`, `distDir: '${tmpOutdir}'`) + .replace('// API route', `'/data': { page: '/api/data' },`) + .replace(`distDir: 'out'`, `distDir: '${outdir}'`) ) - await next.build() + const { cliOutput } = await next.build() await next.patchFile(nextConfigPath, nextConfig) - expect(await fileExist(path.join(tmpOutdir, '404/index.html'))).toBeTrue() + + expect(cliOutput).toContain( + 'https://nextjs.org/docs/messages/api-routes-static-export' + ) }) - } -) + }) + + it('exportTrailingSlash is not ignored', async () => { + const nextConfig = await next.readFile(nextConfigPath) + const tmpOutdir = 'exportTrailingSlash-out' + await next.patchFile( + nextConfigPath, + nextConfig + .replace(`trailingSlash: true`, `exportTrailingSlash: true`) + .replace(`distDir: 'out'`, `distDir: '${tmpOutdir}'`) + ) + await next.build() + await next.patchFile(nextConfigPath, nextConfig) + expect(await fileExist(path.join(tmpOutdir, '404/index.html'))).toBeTrue() + }) +}) diff --git a/test/production/handle-already-sent-response/handle-already-sent-response.test.ts b/test/production/handle-already-sent-response/handle-already-sent-response.test.ts index fe984cebee..45f64d5df2 100644 --- a/test/production/handle-already-sent-response/handle-already-sent-response.test.ts +++ b/test/production/handle-already-sent-response/handle-already-sent-response.test.ts @@ -1,39 +1,37 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' -createNextDescribe( - 'handle already sent response', - { +describe('handle already sent response', () => { + const { next } = nextTestSetup({ files: __dirname, - }, - ({ next }) => { - it('should work with fetch', async () => { - expect(next.cliOutput).toContain('▲ Next.js') - expect(next.cliOutput).toContain('- Local:') + }) - let res = await next.fetch('/') - let text = await res.text() - expect(text).toEqual('already sent') + it('should work with fetch', async () => { + expect(next.cliOutput).toContain('▲ Next.js') + expect(next.cliOutput).toContain('- Local:') - res = await next.fetch('/') - text = await res.text() - expect(text).toEqual('already sent') + let res = await next.fetch('/') + let text = await res.text() + expect(text).toEqual('already sent') - // Check to see that there's two instances of 'getServerSideProps' in - // the output. If there's only one, then the page was not re-rendered. - let i - for (i = 0; i < 3; i++) { - if ((next.cliOutput.match(/getServerSideProps/g) || []).length >= 2) { - break - } - await new Promise((resolve) => setTimeout(resolve, 1000)) - } - if (i === 3) { - throw new Error('Timed out waiting for logs to show') + res = await next.fetch('/') + text = await res.text() + expect(text).toEqual('already sent') + + // Check to see that there's two instances of 'getServerSideProps' in + // the output. If there's only one, then the page was not re-rendered. + let i + for (i = 0; i < 3; i++) { + if ((next.cliOutput.match(/getServerSideProps/g) || []).length >= 2) { + break } + await new Promise((resolve) => setTimeout(resolve, 1000)) + } + if (i === 3) { + throw new Error('Timed out waiting for logs to show') + } - // Headers should not be added after response is sent via - // `getServerSideProps`. If they were, we would see this error. - expect(next.cliOutput).not.toContain('ERR_HTTP_HEADERS_SENT') - }) - } -) + // Headers should not be added after response is sent via + // `getServerSideProps`. If they were, we would see this error. + expect(next.cliOutput).not.toContain('ERR_HTTP_HEADERS_SENT') + }) +}) diff --git a/test/production/ipc-forbidden-headers/ipc-forbidden-headers.test.ts b/test/production/ipc-forbidden-headers/ipc-forbidden-headers.test.ts index b773b09742..4fa88ce915 100644 --- a/test/production/ipc-forbidden-headers/ipc-forbidden-headers.test.ts +++ b/test/production/ipc-forbidden-headers/ipc-forbidden-headers.test.ts @@ -1,49 +1,45 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' -createNextDescribe( - 'ipc-forbidden-headers', - { +describe('ipc-forbidden-headers', () => { + const { next } = nextTestSetup({ files: __dirname, - }, - ({ next }) => { - it('should not error if expect header is included', async () => { - let res = await next.fetch('/api/pages-api', { - method: 'POST', - headers: { expect: '100-continue' }, - }) - let text = await res.text() + }) - expect(text).toEqual('Hello, Next.js!') + it('should not error if expect header is included', async () => { + let res = await next.fetch('/api/pages-api', { + method: 'POST', + headers: { expect: '100-continue' }, + }) + let text = await res.text() - res = await next.fetch('/api/app-api', { - method: 'POST', - headers: { - expect: '100-continue', - }, - }) - text = await res.text() + expect(text).toEqual('Hello, Next.js!') - expect(text).toEqual('Hello, Next.js!') - expect(next.cliOutput).not.toContain('UND_ERR_NOT_SUPPORTED') + res = await next.fetch('/api/app-api', { + method: 'POST', + headers: { + expect: '100-continue', + }, + }) + text = await res.text() + + expect(text).toEqual('Hello, Next.js!') + expect(next.cliOutput).not.toContain('UND_ERR_NOT_SUPPORTED') + }) + + it("should not error on content-length: 0 if request shouldn't contain a payload", async () => { + let res = await next.fetch('/api/pages-api', { + method: 'DELETE', + headers: { 'content-length': '0' }, }) - it("should not error on content-length: 0 if request shouldn't contain a payload", async () => { - let res = await next.fetch('/api/pages-api', { - method: 'DELETE', - headers: { 'content-length': '0' }, - }) + expect(res.status).toBe(200) - expect(res.status).toBe(200) - - res = await next.fetch('/api/app-api', { - method: 'DELETE', - headers: { 'content-length': '0' }, - }) - - expect(res.status).toBe(200) - expect(next.cliOutput).not.toContain( - 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' - ) + res = await next.fetch('/api/app-api', { + method: 'DELETE', + headers: { 'content-length': '0' }, }) - } -) + + expect(res.status).toBe(200) + expect(next.cliOutput).not.toContain('UND_ERR_REQ_CONTENT_LENGTH_MISMATCH') + }) +}) diff --git a/test/production/middleware-typescript/test/index.test.ts b/test/production/middleware-typescript/test/index.test.ts index be6235e32d..ea735b20b1 100644 --- a/test/production/middleware-typescript/test/index.test.ts +++ b/test/production/middleware-typescript/test/index.test.ts @@ -1,16 +1,14 @@ /* eslint-env jest */ import { join } from 'path' -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' -createNextDescribe( - 'middleware-typescript', - { +describe('middleware-typescript', () => { + const { next } = nextTestSetup({ files: join(__dirname, '../app'), - }, - ({ next }) => { - it('should have built and started', async () => { - const response = await next.fetch('/static') - expect(response.headers.get('data')).toEqual('hello from middleware') - }) - } -) + }) + + it('should have built and started', async () => { + const response = await next.fetch('/static') + expect(response.headers.get('data')).toEqual('hello from middleware') + }) +}) diff --git a/test/production/pages-dir/production/test/index.test.ts b/test/production/pages-dir/production/test/index.test.ts index c5c636dc86..5b6e60035e 100644 --- a/test/production/pages-dir/production/test/index.test.ts +++ b/test/production/pages-dir/production/test/index.test.ts @@ -21,7 +21,7 @@ import dynamicImportTests from './dynamic' import processEnv from './process-env' import security from './security' import { promisify } from 'util' -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' const glob = promisify(globOriginal) @@ -29,1261 +29,1246 @@ if (process.env.TEST_WASM) { jest.setTimeout(120 * 1000) } -createNextDescribe( - 'Production Usage', - { +describe('Production Usage', () => { + const { next } = nextTestSetup({ files: path.join(__dirname, '../fixture'), dependencies: { nanoid: '3.1.30', 'es5-ext': '0.10.53', }, - }, - ({ next }) => { - it('should navigate through history after query update', async () => { - const browser = await webdriver(next.appPort, '/') - await browser.eval('window.next.router.push("/about?a=b")') - await browser.waitForElementByCss('.about-page') - await browser.waitForCondition(`!!window.next.router.isReady`) + }) - await browser.refresh() - await browser.waitForCondition(`!!window.next.router.isReady`) - await browser.back() - await browser.waitForElementByCss('.index-page') - await browser.forward() - await browser.waitForElementByCss('.about-page') - await browser.back() - await browser.waitForElementByCss('.index-page') - await browser.refresh() - await browser.waitForCondition(`!!window.next.router.isReady`) - await browser.forward() - await browser.waitForElementByCss('.about-page') + it('should navigate through history after query update', async () => { + const browser = await webdriver(next.appPort, '/') + await browser.eval('window.next.router.push("/about?a=b")') + await browser.waitForElementByCss('.about-page') + await browser.waitForCondition(`!!window.next.router.isReady`) + + await browser.refresh() + await browser.waitForCondition(`!!window.next.router.isReady`) + await browser.back() + await browser.waitForElementByCss('.index-page') + await browser.forward() + await browser.waitForElementByCss('.about-page') + await browser.back() + await browser.waitForElementByCss('.index-page') + await browser.refresh() + await browser.waitForCondition(`!!window.next.router.isReady`) + await browser.forward() + await browser.waitForElementByCss('.about-page') + }) + + if (process.env.BROWSER_NAME !== 'safari') { + it.each([ + { hash: '#hello?' }, + { hash: '#?' }, + { hash: '##' }, + { hash: '##?' }, + { hash: '##hello?' }, + { hash: '##hello' }, + { hash: '#hello?world' }, + { search: '?hello=world', hash: '#a', query: { hello: 'world' } }, + { search: '?hello', hash: '#a', query: { hello: '' } }, + { search: '?hello=', hash: '#a', query: { hello: '' } }, + ])( + 'should handle query/hash correctly during query updating $hash $search', + async ({ hash, search, query }) => { + const browser = await webdriver( + next.appPort, + `/${search || ''}${hash || ''}` + ) + + await check( + () => + browser.eval('window.next.router.isReady ? "ready" : "not ready"'), + 'ready' + ) + expect(await browser.eval('window.location.pathname')).toBe('/') + expect(await browser.eval('window.location.hash')).toBe(hash || '') + expect(await browser.eval('window.location.search')).toBe(search || '') + expect(await browser.eval('next.router.pathname')).toBe('/') + expect( + JSON.parse(await browser.eval('JSON.stringify(next.router.query)')) + ).toEqual(query || {}) + } + ) + } + + it('should not show target deprecation warning', () => { + expect(next.cliOutput).not.toContain( + 'The `target` config is deprecated and will be removed in a future version' + ) + }) + + it('should respond with 405 for POST to static page', async () => { + const res = await fetchViaHTTP(next.appPort, '/about', undefined, { + method: 'POST', }) + expect(res.status).toBe(405) + expect(await res.text()).toContain('Method Not Allowed') + }) - if (process.env.BROWSER_NAME !== 'safari') { - it.each([ - { hash: '#hello?' }, - { hash: '#?' }, - { hash: '##' }, - { hash: '##?' }, - { hash: '##hello?' }, - { hash: '##hello' }, - { hash: '#hello?world' }, - { search: '?hello=world', hash: '#a', query: { hello: 'world' } }, - { search: '?hello', hash: '#a', query: { hello: '' } }, - { search: '?hello=', hash: '#a', query: { hello: '' } }, - ])( - 'should handle query/hash correctly during query updating $hash $search', - async ({ hash, search, query }) => { - const browser = await webdriver( - next.appPort, - `/${search || ''}${hash || ''}` - ) + it('should contain generated page count in output', async () => { + const pageCount = 37 + expect(next.cliOutput).toContain(`Generating static pages (0/${pageCount})`) + expect(next.cliOutput).toContain( + `Generating static pages (${pageCount}/${pageCount})` + ) + // we should only have 4 segments and the initial message logged out + expect(next.cliOutput.match(/Generating static pages/g).length).toBe(5) + }) - await check( - () => - browser.eval( - 'window.next.router.isReady ? "ready" : "not ready"' - ), - 'ready' - ) - expect(await browser.eval('window.location.pathname')).toBe('/') - expect(await browser.eval('window.location.hash')).toBe(hash || '') - expect(await browser.eval('window.location.search')).toBe( - search || '' - ) - expect(await browser.eval('next.router.pathname')).toBe('/') - expect( - JSON.parse(await browser.eval('JSON.stringify(next.router.query)')) - ).toEqual(query || {}) - } + it('should output traces', async () => { + const serverTrace = await next.readJSON('.next/next-server.js.nft.json') + + expect(serverTrace.version).toBe(1) + expect( + serverTrace.files.some((file) => + file.includes('next/dist/server/send-payload.js') ) + ).toBe(true) + expect( + serverTrace.files.some((file) => + file.includes('next/dist/server/lib/route-resolver.js') + ) + ).toBe(false) + const repoRoot = join(next.testDir, '../../../../') + expect( + serverTrace.files.some((file) => { + const fullPath = join(next.testDir, '.next', file) + if (!fullPath.startsWith(repoRoot)) { + console.error(`Found next-server trace file outside repo root`, { + repoRoot, + fullPath, + file, + }) + return true + } + return false + }) + ).toBe(false) + expect( + serverTrace.files.some((file) => + file.includes('next/dist/shared/lib/page-path/normalize-page-path.js') + ) + ).toBe(true) + expect( + serverTrace.files.some((file) => + file.includes('next/dist/server/render.js') + ) + ).toBe(true) + expect( + serverTrace.files.some((file) => + file.includes('next/dist/server/load-components.js') + ) + ).toBe(true) + + if (process.platform !== 'win32') { + expect( + serverTrace.files.some((file) => + file.includes('next/dist/compiled/webpack/bundle5.js') + ) + ).toBe(false) + expect( + serverTrace.files.some((file) => file.includes('node_modules/sharp')) + ).toBe(true) } - it('should not show target deprecation warning', () => { - expect(next.cliOutput).not.toContain( - 'The `target` config is deprecated and will be removed in a future version' + const checks = [ + { + page: '/_app', + tests: [ + /webpack-runtime\.js/, + /node_modules\/react\/index\.js/, + /node_modules\/react\/package\.json/, + /node_modules\/react\/cjs\/react\.production\.min\.js/, + ], + notTests: [/\0/, /\?/, /!/], + }, + { + page: '/client-error', + tests: [ + /webpack-runtime\.js/, + /chunks\/.*?\.js/, + /node_modules\/react\/index\.js/, + /node_modules\/react\/package\.json/, + /node_modules\/react\/cjs\/react\.production\.min\.js/, + /node_modules\/next/, + ], + notTests: [/\0/, /\?/, /!/], + }, + { + page: '/index', + tests: [ + /webpack-runtime\.js/, + /chunks\/.*?\.js/, + /node_modules\/react\/index\.js/, + /node_modules\/react\/package\.json/, + /node_modules\/react\/cjs\/react\.production\.min\.js/, + /node_modules\/next/, + /node_modules\/nanoid\/index\.js/, + /node_modules\/nanoid\/url-alphabet\/index\.js/, + /node_modules\/es5-ext\/array\/#\/clear\.js/, + ], + notTests: [/next\/dist\/pages\/_error\.js/, /\0/, /\?/, /!/], + }, + { + page: '/next-import', + tests: [ + /webpack-runtime\.js/, + /chunks\/.*?\.js/, + /node_modules\/react\/index\.js/, + /node_modules\/react\/package\.json/, + /node_modules\/react\/cjs\/react\.production\.min\.js/, + /node_modules\/next/, + ], + notTests: [ + /next\/dist\/server\/next\.js/, + /next\/dist\/bin/, + /\0/, + /\?/, + /!/, + ], + }, + { + page: '/api', + tests: [/webpack-runtime\.js/, /\/logo\.module\.css/], + notTests: [ + /next\/dist\/server\/next\.js/, + /next\/dist\/bin/, + /\0/, + /\?/, + /!/, + ], + }, + { + page: '/api/readfile-dirname', + tests: [/webpack-api-runtime\.js/, /static\/data\/item\.txt/], + notTests: [ + /next\/dist\/server\/next\.js/, + /next\/dist\/bin/, + /\0/, + /\?/, + /!/, + ], + }, + { + page: '/api/readfile-processcwd', + tests: [/webpack-api-runtime\.js/, /static\/data\/item\.txt/], + notTests: [ + /next\/dist\/server\/next\.js/, + /next\/dist\/bin/, + /\0/, + /\?/, + /!/, + ], + }, + ] + + for (const check of checks) { + require('console').log('checking', check.page) + const { version, files } = await next.readJSON( + join('.next/server/pages/', check.page + '.js.nft.json') ) - }) + expect(version).toBe(1) + expect([...new Set(files)].length).toBe(files.length) - it('should respond with 405 for POST to static page', async () => { - const res = await fetchViaHTTP(next.appPort, '/about', undefined, { - method: 'POST', - }) - expect(res.status).toBe(405) - expect(await res.text()).toContain('Method Not Allowed') - }) - - it('should contain generated page count in output', async () => { - const pageCount = 37 - expect(next.cliOutput).toContain( - `Generating static pages (0/${pageCount})` - ) - expect(next.cliOutput).toContain( - `Generating static pages (${pageCount}/${pageCount})` - ) - // we should only have 4 segments and the initial message logged out - expect(next.cliOutput.match(/Generating static pages/g).length).toBe(5) - }) - - it('should output traces', async () => { - const serverTrace = await next.readJSON('.next/next-server.js.nft.json') - - expect(serverTrace.version).toBe(1) expect( - serverTrace.files.some((file) => - file.includes('next/dist/server/send-payload.js') - ) - ).toBe(true) - expect( - serverTrace.files.some((file) => - file.includes('next/dist/server/lib/route-resolver.js') - ) - ).toBe(false) - const repoRoot = join(next.testDir, '../../../../') - expect( - serverTrace.files.some((file) => { - const fullPath = join(next.testDir, '.next', file) - if (!fullPath.startsWith(repoRoot)) { - console.error(`Found next-server trace file outside repo root`, { - repoRoot, - fullPath, - file, - }) + check.tests.every((item) => { + if (files.some((file) => item.test(file))) { return true } + console.error( + `Failed to find ${item} for page ${check.page} in`, + files + ) return false }) - ).toBe(false) - expect( - serverTrace.files.some((file) => - file.includes('next/dist/shared/lib/page-path/normalize-page-path.js') - ) - ).toBe(true) - expect( - serverTrace.files.some((file) => - file.includes('next/dist/server/render.js') - ) - ).toBe(true) - expect( - serverTrace.files.some((file) => - file.includes('next/dist/server/load-components.js') - ) ).toBe(true) - if (process.platform !== 'win32') { + if (sep === '/') { expect( - serverTrace.files.some((file) => - file.includes('next/dist/compiled/webpack/bundle5.js') - ) - ).toBe(false) - expect( - serverTrace.files.some((file) => file.includes('node_modules/sharp')) - ).toBe(true) - } - - const checks = [ - { - page: '/_app', - tests: [ - /webpack-runtime\.js/, - /node_modules\/react\/index\.js/, - /node_modules\/react\/package\.json/, - /node_modules\/react\/cjs\/react\.production\.min\.js/, - ], - notTests: [/\0/, /\?/, /!/], - }, - { - page: '/client-error', - tests: [ - /webpack-runtime\.js/, - /chunks\/.*?\.js/, - /node_modules\/react\/index\.js/, - /node_modules\/react\/package\.json/, - /node_modules\/react\/cjs\/react\.production\.min\.js/, - /node_modules\/next/, - ], - notTests: [/\0/, /\?/, /!/], - }, - { - page: '/index', - tests: [ - /webpack-runtime\.js/, - /chunks\/.*?\.js/, - /node_modules\/react\/index\.js/, - /node_modules\/react\/package\.json/, - /node_modules\/react\/cjs\/react\.production\.min\.js/, - /node_modules\/next/, - /node_modules\/nanoid\/index\.js/, - /node_modules\/nanoid\/url-alphabet\/index\.js/, - /node_modules\/es5-ext\/array\/#\/clear\.js/, - ], - notTests: [/next\/dist\/pages\/_error\.js/, /\0/, /\?/, /!/], - }, - { - page: '/next-import', - tests: [ - /webpack-runtime\.js/, - /chunks\/.*?\.js/, - /node_modules\/react\/index\.js/, - /node_modules\/react\/package\.json/, - /node_modules\/react\/cjs\/react\.production\.min\.js/, - /node_modules\/next/, - ], - notTests: [ - /next\/dist\/server\/next\.js/, - /next\/dist\/bin/, - /\0/, - /\?/, - /!/, - ], - }, - { - page: '/api', - tests: [/webpack-runtime\.js/, /\/logo\.module\.css/], - notTests: [ - /next\/dist\/server\/next\.js/, - /next\/dist\/bin/, - /\0/, - /\?/, - /!/, - ], - }, - { - page: '/api/readfile-dirname', - tests: [/webpack-api-runtime\.js/, /static\/data\/item\.txt/], - notTests: [ - /next\/dist\/server\/next\.js/, - /next\/dist\/bin/, - /\0/, - /\?/, - /!/, - ], - }, - { - page: '/api/readfile-processcwd', - tests: [/webpack-api-runtime\.js/, /static\/data\/item\.txt/], - notTests: [ - /next\/dist\/server\/next\.js/, - /next\/dist\/bin/, - /\0/, - /\?/, - /!/, - ], - }, - ] - - for (const check of checks) { - require('console').log('checking', check.page) - const { version, files } = await next.readJSON( - join('.next/server/pages/', check.page + '.js.nft.json') - ) - expect(version).toBe(1) - expect([...new Set(files)].length).toBe(files.length) - - expect( - check.tests.every((item) => { + check.notTests.some((item) => { if (files.some((file) => item.test(file))) { + console.error(`Found unexpected ${item} in`, files) return true } - console.error( - `Failed to find ${item} for page ${check.page} in`, - files - ) return false }) - ).toBe(true) + ).toBe(false) + } + } + }) - if (sep === '/') { - expect( - check.notTests.some((item) => { - if (files.some((file) => item.test(file))) { - console.error(`Found unexpected ${item} in`, files) - return true - } - return false - }) - ).toBe(false) + it('should not contain currentScript usage for publicPath', async () => { + const globResult = await glob('webpack-*.js', { + cwd: join(next.testDir, '.next/static/chunks'), + }) + + if (!globResult || globResult.length !== 1) { + throw new Error('could not find webpack-hash.js chunk') + } + + const content = await next.readFile( + join('.next/static/chunks', globResult[0]) + ) + + expect(content).not.toContain('.currentScript') + }) + + it('should not contain amp, rsc APIs in main chunk', async () => { + const globResult = await glob('main-*.js', { + cwd: join(next.testDir, '.next/static/chunks'), + }) + + if (!globResult || globResult.length !== 1) { + throw new Error('could not find main js chunk') + } + + const content = await fs.readFile( + join(next.testDir, '.next/static/chunks', globResult[0]), + 'utf8' + ) + + expect(content).not.toContain('useAmp') + expect(content).not.toContain('useRefreshRoot') + }) + + describe('With basic usage', () => { + it('should render the page', async () => { + const html = await renderViaHTTP(next.appPort, '/') + expect(html).toMatch(/Hello World/) + }) + + if (global.browserName === 'internet explorer') { + it('should handle bad Promise polyfill', async () => { + const browser = await webdriver(next.appPort, '/bad-promise') + expect(await browser.eval('window.didRender')).toBe(true) + }) + + it('should polyfill RegExp successfully', async () => { + const browser = await webdriver(next.appPort, '/regexp-polyfill') + expect(await browser.eval('window.didRender')).toBe(true) + // wait a second for the script to be loaded + await waitFor(1000) + + expect(await browser.eval('window.isSticky')).toBe(true) + expect(await browser.eval('window.isMatch1')).toBe(true) + expect(await browser.eval('window.isMatch2')).toBe(false) + }) + } + + it('should polyfill Node.js modules', async () => { + const browser = await webdriver(next.appPort, '/node-browser-polyfills') + await browser.waitForCondition('window.didRender') + + const data = await browser + .waitForElementByCss('#node-browser-polyfills') + .text() + const parsedData = JSON.parse(data) + + expect(parsedData.vm).toBe(105) + expect(parsedData.hash).toBe( + 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' + ) + expect(parsedData.path).toBe('/hello/world/test.txt') + expect(parsedData.buffer).toBe('hello world') + expect(parsedData.stream).toBe(true) + }) + + it('should allow etag header support', async () => { + const url = `http://localhost:${next.appPort}` + const etag = (await fetchViaHTTP(url, '/')).headers.get('ETag') + + const headers = { 'If-None-Match': etag } + const res2 = await fetchViaHTTP(url, '/', undefined, { headers }) + expect(res2.status).toBe(304) + }) + + it('should allow etag header support with getStaticProps', async () => { + const url = `http://localhost:${next.appPort}` + const etag = (await fetchViaHTTP(url, '/fully-static')).headers.get( + 'ETag' + ) + + const headers = { 'If-None-Match': etag } + const res2 = await fetchViaHTTP(url, '/fully-static', undefined, { + headers, + }) + expect(res2.status).toBe(304) + }) + + // TODO: should we generate weak etags for streaming getServerSideProps? + // this is currently not expected to work with react-18 + it.skip('should allow etag header support with getServerSideProps', async () => { + const url = `http://localhost:${next.appPort}` + const etag = (await fetchViaHTTP(url, '/fully-dynamic')).headers.get( + 'ETag' + ) + + const headers = { 'If-None-Match': etag } + const res2 = await fetchViaHTTP(url, '/fully-dynamic', undefined, { + headers, + }) + expect(res2.status).toBe(304) + }) + + it('should have X-Powered-By header support', async () => { + const url = `http://localhost:${next.appPort}` + const header = (await fetchViaHTTP(url, '/')).headers.get('X-Powered-By') + + expect(header).toBe('Next.js') + }) + + it('should render 404 for routes that do not exist', async () => { + const url = `http://localhost:${next.appPort}` + const res = await fetchViaHTTP(url, '/abcdefghijklmno') + const text = await res.text() + const $html = cheerio.load(text) + expect($html('html').text()).toMatch(/404/) + expect(text).toMatch(/"statusCode":404/) + expect(res.status).toBe(404) + }) + + it('should render 404 for /_next/static route', async () => { + const html = await renderViaHTTP(next.appPort, '/_next/static') + expect(html).toMatch(/This page could not be found/) + }) + + it('should render 200 for POST on page', async () => { + const res = await fetchViaHTTP( + `http://localhost:${next.appPort}`, + '/fully-dynamic', + undefined, + { + method: 'POST', } - } - }) - - it('should not contain currentScript usage for publicPath', async () => { - const globResult = await glob('webpack-*.js', { - cwd: join(next.testDir, '.next/static/chunks'), - }) - - if (!globResult || globResult.length !== 1) { - throw new Error('could not find webpack-hash.js chunk') - } - - const content = await next.readFile( - join('.next/static/chunks', globResult[0]) ) - - expect(content).not.toContain('.currentScript') + expect(res.status).toBe(200) }) - it('should not contain amp, rsc APIs in main chunk', async () => { - const globResult = await glob('main-*.js', { - cwd: join(next.testDir, '.next/static/chunks'), - }) - - if (!globResult || globResult.length !== 1) { - throw new Error('could not find main js chunk') - } - - const content = await fs.readFile( - join(next.testDir, '.next/static/chunks', globResult[0]), - 'utf8' + it('should render 404 for POST on missing page', async () => { + const res = await fetchViaHTTP( + `http://localhost:${next.appPort}`, + '/fake-page', + undefined, + { + method: 'POST', + } ) - - expect(content).not.toContain('useAmp') - expect(content).not.toContain('useRefreshRoot') + expect(res.status).toBe(404) }) - describe('With basic usage', () => { - it('should render the page', async () => { - const html = await renderViaHTTP(next.appPort, '/') - expect(html).toMatch(/Hello World/) - }) + it('should render 404 for _next routes that do not exist', async () => { + const url = `http://localhost:${next.appPort}` + const res = await fetchViaHTTP(url, '/_next/abcdef') + expect(res.status).toBe(404) + }) - if (global.browserName === 'internet explorer') { - it('should handle bad Promise polyfill', async () => { - const browser = await webdriver(next.appPort, '/bad-promise') - expect(await browser.eval('window.didRender')).toBe(true) + it('should render 404 even if the HTTP method is not GET or HEAD', async () => { + const url = `http://localhost:${next.appPort}` + const methods = ['POST', 'PUT', 'DELETE'] + for (const method of methods) { + const res = await fetchViaHTTP(url, '/_next/abcdef', undefined, { + method, }) - - it('should polyfill RegExp successfully', async () => { - const browser = await webdriver(next.appPort, '/regexp-polyfill') - expect(await browser.eval('window.didRender')).toBe(true) - // wait a second for the script to be loaded - await waitFor(1000) - - expect(await browser.eval('window.isSticky')).toBe(true) - expect(await browser.eval('window.isMatch1')).toBe(true) - expect(await browser.eval('window.isMatch2')).toBe(false) - }) - } - - it('should polyfill Node.js modules', async () => { - const browser = await webdriver(next.appPort, '/node-browser-polyfills') - await browser.waitForCondition('window.didRender') - - const data = await browser - .waitForElementByCss('#node-browser-polyfills') - .text() - const parsedData = JSON.parse(data) - - expect(parsedData.vm).toBe(105) - expect(parsedData.hash).toBe( - 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' - ) - expect(parsedData.path).toBe('/hello/world/test.txt') - expect(parsedData.buffer).toBe('hello world') - expect(parsedData.stream).toBe(true) - }) - - it('should allow etag header support', async () => { - const url = `http://localhost:${next.appPort}` - const etag = (await fetchViaHTTP(url, '/')).headers.get('ETag') - - const headers = { 'If-None-Match': etag } - const res2 = await fetchViaHTTP(url, '/', undefined, { headers }) - expect(res2.status).toBe(304) - }) - - it('should allow etag header support with getStaticProps', async () => { - const url = `http://localhost:${next.appPort}` - const etag = (await fetchViaHTTP(url, '/fully-static')).headers.get( - 'ETag' - ) - - const headers = { 'If-None-Match': etag } - const res2 = await fetchViaHTTP(url, '/fully-static', undefined, { - headers, - }) - expect(res2.status).toBe(304) - }) - - // TODO: should we generate weak etags for streaming getServerSideProps? - // this is currently not expected to work with react-18 - it.skip('should allow etag header support with getServerSideProps', async () => { - const url = `http://localhost:${next.appPort}` - const etag = (await fetchViaHTTP(url, '/fully-dynamic')).headers.get( - 'ETag' - ) - - const headers = { 'If-None-Match': etag } - const res2 = await fetchViaHTTP(url, '/fully-dynamic', undefined, { - headers, - }) - expect(res2.status).toBe(304) - }) - - it('should have X-Powered-By header support', async () => { - const url = `http://localhost:${next.appPort}` - const header = (await fetchViaHTTP(url, '/')).headers.get( - 'X-Powered-By' - ) - - expect(header).toBe('Next.js') - }) - - it('should render 404 for routes that do not exist', async () => { - const url = `http://localhost:${next.appPort}` - const res = await fetchViaHTTP(url, '/abcdefghijklmno') - const text = await res.text() - const $html = cheerio.load(text) - expect($html('html').text()).toMatch(/404/) - expect(text).toMatch(/"statusCode":404/) expect(res.status).toBe(404) - }) + } + }) - it('should render 404 for /_next/static route', async () => { - const html = await renderViaHTTP(next.appPort, '/_next/static') - expect(html).toMatch(/This page could not be found/) - }) + it('should render 404 for dotfiles in /static', async () => { + const url = `http://localhost:${next.appPort}` + const res = await fetchViaHTTP(url, '/static/.env') + expect(res.status).toBe(404) + }) - it('should render 200 for POST on page', async () => { + it('should return 405 method on static then GET and HEAD', async () => { + const res = await fetchViaHTTP( + `http://localhost:${next.appPort}`, + '/static/data/item.txt', + undefined, + { + method: 'POST', + } + ) + expect(res.headers.get('allow').includes('GET')).toBe(true) + expect(res.status).toBe(405) + }) + + it('should return 412 on static file when If-Unmodified-Since is provided and file is modified', async () => { + const buildManifest = await next.readJSON('.next/build-manifest.json') + + const files = buildManifest.pages['/'] + + for (const file of files) { const res = await fetchViaHTTP( `http://localhost:${next.appPort}`, - '/fully-dynamic', + `/_next/${file}`, undefined, { - method: 'POST', + method: 'GET', + headers: { + 'if-unmodified-since': 'Fri, 12 Jul 2019 20:00:13 GMT', + }, + } + ) + expect(res.status).toBe(412) + } + }) + + it('should return 200 on static file if If-Unmodified-Since is invalid date', async () => { + const buildManifest = await next.readJSON('.next/build-manifest.json') + + const files = buildManifest.pages['/'] + + for (const file of files) { + const res = await fetchViaHTTP( + `http://localhost:${next.appPort}`, + `/_next/${file}`, + undefined, + { + method: 'GET', + headers: { 'if-unmodified-since': 'nextjs' }, } ) expect(res.status).toBe(200) - }) - - it('should render 404 for POST on missing page', async () => { - const res = await fetchViaHTTP( - `http://localhost:${next.appPort}`, - '/fake-page', - undefined, - { - method: 'POST', - } - ) - expect(res.status).toBe(404) - }) - - it('should render 404 for _next routes that do not exist', async () => { - const url = `http://localhost:${next.appPort}` - const res = await fetchViaHTTP(url, '/_next/abcdef') - expect(res.status).toBe(404) - }) - - it('should render 404 even if the HTTP method is not GET or HEAD', async () => { - const url = `http://localhost:${next.appPort}` - const methods = ['POST', 'PUT', 'DELETE'] - for (const method of methods) { - const res = await fetchViaHTTP(url, '/_next/abcdef', undefined, { - method, - }) - expect(res.status).toBe(404) - } - }) - - it('should render 404 for dotfiles in /static', async () => { - const url = `http://localhost:${next.appPort}` - const res = await fetchViaHTTP(url, '/static/.env') - expect(res.status).toBe(404) - }) - - it('should return 405 method on static then GET and HEAD', async () => { - const res = await fetchViaHTTP( - `http://localhost:${next.appPort}`, - '/static/data/item.txt', - undefined, - { - method: 'POST', - } - ) - expect(res.headers.get('allow').includes('GET')).toBe(true) - expect(res.status).toBe(405) - }) - - it('should return 412 on static file when If-Unmodified-Since is provided and file is modified', async () => { - const buildManifest = await next.readJSON('.next/build-manifest.json') - - const files = buildManifest.pages['/'] - - for (const file of files) { - const res = await fetchViaHTTP( - `http://localhost:${next.appPort}`, - `/_next/${file}`, - undefined, - { - method: 'GET', - headers: { - 'if-unmodified-since': 'Fri, 12 Jul 2019 20:00:13 GMT', - }, - } - ) - expect(res.status).toBe(412) - } - }) - - it('should return 200 on static file if If-Unmodified-Since is invalid date', async () => { - const buildManifest = await next.readJSON('.next/build-manifest.json') - - const files = buildManifest.pages['/'] - - for (const file of files) { - const res = await fetchViaHTTP( - `http://localhost:${next.appPort}`, - `/_next/${file}`, - undefined, - { - method: 'GET', - headers: { 'if-unmodified-since': 'nextjs' }, - } - ) - expect(res.status).toBe(200) - } - }) - - it('should set Content-Length header', async () => { - const url = `http://localhost:${next.appPort}` - const res = await fetchViaHTTP(url, '/') - expect(res.headers.get('Content-Length')).toBeDefined() - }) - - it('should set Cache-Control header', async () => { - const buildManifest = await next.readJSON(`.next/${BUILD_MANIFEST}`) - const reactLoadableManifest = await next.readJSON( - join('./.next', REACT_LOADABLE_MANIFEST) - ) - const url = `http://localhost:${next.appPort}` - - const resources: Set = new Set() - - const manifestKey = Object.keys(reactLoadableManifest).find((item) => { - return item - .replace(/\\/g, '/') - .endsWith('dynamic/css.js -> ../../components/dynamic-css/with-css') - }) - - // test dynamic chunk - reactLoadableManifest[manifestKey].files.forEach((f) => { - resources.add('/' + f) - }) - - // test main.js runtime etc - for (const item of buildManifest.pages['/']) { - resources.add('/' + item) - } - - const cssStaticAssets = await recursiveReadDir( - join(next.testDir, '.next', 'static'), - { pathnameFilter: (f) => /\.css$/.test(f) } - ) - expect(cssStaticAssets.length).toBeGreaterThanOrEqual(1) - expect(cssStaticAssets[0]).toMatch(/[\\/]css[\\/]/) - const mediaStaticAssets = await recursiveReadDir( - join(next.testDir, '.next', 'static'), - { pathnameFilter: (f) => /\.svg$/.test(f) } - ) - expect(mediaStaticAssets.length).toBeGreaterThanOrEqual(1) - expect(mediaStaticAssets[0]).toMatch(/[\\/]media[\\/]/) - ;[...cssStaticAssets, ...mediaStaticAssets].forEach((asset) => { - resources.add(`/static${asset.replace(/\\+/g, '/')}`) - }) - - const responses = await Promise.all( - [...resources].map((resource) => - fetchViaHTTP(url, join('/_next', resource)) - ) - ) - - responses.forEach((res) => { - try { - expect(res.headers.get('Cache-Control')).toBe( - 'public, max-age=31536000, immutable' - ) - } catch (err) { - err.message = res.url + ' ' + err.message - throw err - } - }) - }) - - it('should set correct Cache-Control header for static 404s', async () => { - // this is to fix where 404 headers are set to 'public, max-age=31536000, immutable' - const res = await fetchViaHTTP( - `http://localhost:${next.appPort}`, - `/_next//static/common/bad-static.js` - ) - - expect(res.status).toBe(404) - expect(res.headers.get('Cache-Control')).toBe( - 'no-cache, no-store, max-age=0, must-revalidate' - ) - }) - - it('should block special pages', async () => { - const urls = ['/_document', '/_app'] - for (const url of urls) { - const html = await renderViaHTTP(next.appPort, url) - expect(html).toMatch(/404/) - } - }) - - it('should not contain customServer in NEXT_DATA', async () => { - const html = await renderViaHTTP(next.appPort, '/') - const $ = cheerio.load(html) - expect('customServer' in JSON.parse($('#__NEXT_DATA__').text())).toBe( - false - ) - }) - }) - - describe('API routes', () => { - it('should work with pages/api/index.js', async () => { - const url = `http://localhost:${next.appPort}` - const res = await fetchViaHTTP(url, `/api`) - const body = await res.text() - expect(body).toEqual('API index works') - }) - - it('should work with pages/api/hello.js', async () => { - const url = `http://localhost:${next.appPort}` - const res = await fetchViaHTTP(url, `/api/hello`) - const body = await res.text() - expect(body).toEqual('API hello works') - }) - - // Today, `__dirname` usage fails because Next.js moves the source file - // to .next/server/pages/api but it doesn't move the asset file. - // In the future, it would be nice to make `__dirname` work too. - it('does not work with pages/api/readfile-dirname.js', async () => { - const url = `http://localhost:${next.appPort}` - const res = await fetchViaHTTP(url, `/api/readfile-dirname`) - expect(res.status).toBe(500) - }) - - it('should work with pages/api/readfile-processcwd.js', async () => { - const url = `http://localhost:${next.appPort}` - const res = await fetchViaHTTP(url, `/api/readfile-processcwd`) - const body = await res.text() - expect(body).toBe('item') - }) - - it('should work with dynamic params and search string', async () => { - const url = `http://localhost:${next.appPort}` - const res = await fetchViaHTTP(url, `/api/post-1?val=1`) - const body = await res.json() - - expect(body).toEqual({ val: '1', post: 'post-1' }) - }) - }) - - describe('With navigation', () => { - it('should navigate via client side', async () => { - const browser = await webdriver(next.appPort, '/') - const text = await browser - .elementByCss('a') - .click() - .waitForElementByCss('.about-page') - .elementByCss('.about-page') - .text() - - expect(text).toBe('About Page') - await browser.close() - }) - - it('should navigate to nested index via client side', async () => { - const browser = await webdriver(next.appPort, '/another') - await browser.eval('window.beforeNav = 1') - - const text = await browser - .elementByCss('a') - .click() - .waitForElementByCss('.index-page') - .elementByCss('p') - .text() - - expect(text).toBe('Hello World') - expect(await browser.eval('window.beforeNav')).toBe(1) - await browser.close() - }) - - it('should reload page successfully (on bad link)', async () => { - const browser = await webdriver(next.appPort, '/to-nonexistent') - await browser.eval(function setup() { - // @ts-expect-error Exists on window - window.__DATA_BE_GONE = 'true' - }) - await browser - .waitForElementByCss('#to-nonexistent-page') - .click('#to-nonexistent-page') - await browser.waitForElementByCss('.about-page') - - const oldData = await browser.eval(`window.__DATA_BE_GONE`) - expect(oldData).toBeFalsy() - }) - - it('should reload page successfully (on bad data fetch)', async () => { - const browser = await webdriver(next.appPort, '/to-shadowed-page') - await browser.eval(function setup() { - // @ts-expect-error Exists on window - window.__DATA_BE_GONE = 'true' - }) - await browser.waitForElementByCss('#to-shadowed-page').click() - await browser.waitForElementByCss('.about-page') - - const oldData = await browser.eval(`window.__DATA_BE_GONE`) - expect(oldData).toBeFalsy() - }) - }) - - it('should navigate to external site and back', async () => { - const browser = await webdriver(next.appPort, '/external-and-back') - const initialText = await browser.elementByCss('p').text() - expect(initialText).toBe('server') - - await browser - .elementByCss('a') - .click() - .waitForElementByCss('input') - .back() - .waitForElementByCss('p') - - await waitFor(1000) - const newText = await browser.elementByCss('p').text() - expect(newText).toBe('server') - }) - - it('should navigate to page with CSS and back', async () => { - const browser = await webdriver(next.appPort, '/css-and-back') - const initialText = await browser.elementByCss('p').text() - expect(initialText).toBe('server') - - await browser - .elementByCss('a') - .click() - .waitForElementByCss('input') - .back() - .waitForElementByCss('p') - - await waitFor(1000) - const newText = await browser.elementByCss('p').text() - expect(newText).toBe('client') - }) - - it('should navigate to external site and back (with query)', async () => { - const browser = await webdriver( - next.appPort, - '/external-and-back?hello=world' - ) - const initialText = await browser.elementByCss('p').text() - expect(initialText).toBe('server') - - await browser - .elementByCss('a') - .click() - .waitForElementByCss('input') - .back() - .waitForElementByCss('p') - - await waitFor(1000) - const newText = await browser.elementByCss('p').text() - expect(newText).toBe('server') - }) - - it('should change query correctly', async () => { - const browser = await webdriver(next.appPort, '/query?id=0') - let id = await browser.elementByCss('#q0').text() - expect(id).toBe('0') - - await browser.elementByCss('#first').click().waitForElementByCss('#q1') - - id = await browser.elementByCss('#q1').text() - expect(id).toBe('1') - - await browser.elementByCss('#second').click().waitForElementByCss('#q2') - - id = await browser.elementByCss('#q2').text() - expect(id).toBe('2') - }) - - describe('Runtime errors', () => { - it('should render a server side error on the client side', async () => { - const browser = await webdriver(next.appPort, '/error-in-ssr-render') - await waitFor(2000) - const text = await browser.elementByCss('body').text() - // this makes sure we don't leak the actual error to the client side in production - expect(text).toMatch(/Internal Server Error\./) - const headingText = await browser.elementByCss('h1').text() - // This makes sure we render statusCode on the client side correctly - expect(headingText).toBe('500') - await browser.close() - }) - - it('should render a client side component error', async () => { - const browser = await webdriver( - next.appPort, - '/error-in-browser-render' - ) - await waitFor(2000) - const text = await browser.elementByCss('body').text() - expect(text).toMatch( - /Application error: a client-side exception has occurred/ - ) - await browser.close() - }) - - it('should call getInitialProps on _error page during a client side component error', async () => { - const browser = await webdriver( - next.appPort, - '/error-in-browser-render-status-code' - ) - await waitFor(2000) - const text = await browser.elementByCss('body').text() - expect(text).toMatch(/This page could not be found\./) - await browser.close() - }) - }) - - describe('Misc', () => { - it('should handle already finished responses', async () => { - const html = await renderViaHTTP(next.appPort, '/finish-response') - expect(html).toBe('hi') - }) - - it('should allow to access /static/ and /_next/', async () => { - // This is a test case which prevent the following issue happening again. - // See: https://github.com/vercel/next.js/issues/2617 - await renderViaHTTP(next.appPort, '/_next/') - await renderViaHTTP(next.appPort, '/static/') - const data = await renderViaHTTP(next.appPort, '/static/data/item.txt') - expect(data).toBe('item') - }) - - it('Should allow access to public files', async () => { - const data = await renderViaHTTP(next.appPort, '/data/data.txt') - const file = await renderViaHTTP(next.appPort, '/file') - const legacy = await renderViaHTTP(next.appPort, '/static/legacy.txt') - expect(data).toBe('data') - expect(file).toBe('test') - expect(legacy).toMatch(`new static folder`) - }) - - // TODO: do we want to normalize this for firefox? It seems in - // the latest version of firefox the window state is not reset - // when navigating back from a hard navigation. This might be - // a bug as other browsers do not behave this way. - if (global.browserName !== 'firefox') { - it('should reload the page on page script error', async () => { - const browser = await webdriver(next.appPort, '/counter') - const counter = await browser - .elementByCss('#increase') - .click() - .click() - .elementByCss('#counter') - .text() - expect(counter).toBe('Counter: 2') - - // When we go to the 404 page, it'll do a hard reload. - // So, it's possible for the front proxy to load a page from another zone. - // Since the page is reloaded, when we go back to the counter page again, - // previous counter value should be gone. - const counterAfter404Page = await browser - .elementByCss('#no-such-page') - .click() - .waitForElementByCss('h1') - .back() - .waitForElementByCss('#counter-page') - .elementByCss('#counter') - .text() - expect(counterAfter404Page).toBe('Counter: 0') - - await browser.close() - }) - } - - it('should have default runtime values when not defined', async () => { - const html = await renderViaHTTP(next.appPort, '/runtime-config') - expect(html).toMatch(/found public config/) - expect(html).toMatch(/found server config/) - }) - - it('should not have runtimeConfig in __NEXT_DATA__', async () => { - const html = await renderViaHTTP(next.appPort, '/runtime-config') - const $ = cheerio.load(html) - const script = $('#__NEXT_DATA__').html() - expect(script).not.toMatch(/runtimeConfig/) - }) - - it('should add autoExport for auto pre-rendered pages', async () => { - for (const page of ['/about']) { - const html = await renderViaHTTP(next.appPort, page) - const $ = cheerio.load(html) - const data = JSON.parse($('#__NEXT_DATA__').html()) - expect(data.autoExport).toBe(true) - } - }) - - it('should not add autoExport for non pre-rendered pages', async () => { - for (const page of ['/query']) { - const html = await renderViaHTTP(next.appPort, page) - const $ = cheerio.load(html) - const data = JSON.parse($('#__NEXT_DATA__').html()) - expect(!!data.autoExport).toBe(false) - } - }) - - it('should add prefetch tags when Link prefetch prop is used', async () => { - const browser = await webdriver(next.appPort, '/prefetch') - - if (global.browserName === 'internet explorer') { - // IntersectionObserver isn't present so we need to trigger manually - await waitFor(1000) - await browser.eval(`(function() { - window.next.router.prefetch('/') - window.next.router.prefetch('/process-env') - window.next.router.prefetch('/counter') - window.next.router.prefetch('/about') - })()`) - } - - await waitFor(2000) - - if (global.browserName === 'safari') { - const elements = await browser.elementsByCss('link[rel=preload]') - // optimized preloading uses defer instead of preloading and prefetches - // aren't generated client-side since safari does not support prefetch - expect(elements.length).toBe(0) - } else { - const elements = await browser.elementsByCss('link[rel=prefetch]') - expect(elements.length).toBe(4) - - for (const element of elements) { - const rel = await element.getAttribute('rel') - const as = await element.getAttribute('as') - expect(rel).toBe('prefetch') - expect(as).toBe('script') - } - } - await browser.close() - }) - - // This is a workaround to fix https://github.com/vercel/next.js/issues/5860 - // TODO: remove this workaround when https://bugs.webkit.org/show_bug.cgi?id=187726 is fixed. - it('It does not add a timestamp to link tags with prefetch attribute', async () => { - const browser = await webdriver(next.appPort, '/prefetch') - const links = await browser.elementsByCss('link[rel=prefetch]') - - for (const element of links) { - const href = await element.getAttribute('href') - expect(href).not.toMatch(/\?ts=/) - } - const scripts = await browser.elementsByCss('script[src]') - - for (const element of scripts) { - const src = await element.getAttribute('src') - expect(src).not.toMatch(/\?ts=/) - } - await browser.close() - }) - - if (global.browserName === 'chrome') { - it('should reload the page on page script error with prefetch', async () => { - const browser = await webdriver(next.appPort, '/counter') - if (global.browserName !== 'chrome') return - const counter = await browser - .elementByCss('#increase') - .click() - .click() - .elementByCss('#counter') - .text() - expect(counter).toBe('Counter: 2') - - // Let the browser to prefetch the page and error it on the console. - await waitFor(3000) - - // When we go to the 404 page, it'll do a hard reload. - // So, it's possible for the front proxy to load a page from another zone. - // Since the page is reloaded, when we go back to the counter page again, - // previous counter value should be gone. - const counterAfter404Page = await browser - .elementByCss('#no-such-page-prefetch') - .click() - .waitForElementByCss('h1') - .back() - .waitForElementByCss('#counter-page') - .elementByCss('#counter') - .text() - expect(counterAfter404Page).toBe('Counter: 0') - - await browser.close() - }) } }) - it('should not expose the compiled page file in development', async () => { + it('should set Content-Length header', async () => { const url = `http://localhost:${next.appPort}` - await fetchViaHTTP(`${url}`, `/stateless`) // make sure the stateless page is built - const clientSideJsRes = await fetchViaHTTP( - `${url}`, - '/_next/development/static/development/pages/stateless.js' + const res = await fetchViaHTTP(url, '/') + expect(res.headers.get('Content-Length')).toBeDefined() + }) + + it('should set Cache-Control header', async () => { + const buildManifest = await next.readJSON(`.next/${BUILD_MANIFEST}`) + const reactLoadableManifest = await next.readJSON( + join('./.next', REACT_LOADABLE_MANIFEST) ) - expect(clientSideJsRes.status).toBe(404) - const clientSideJsBody = await clientSideJsRes.text() - expect(clientSideJsBody).toMatch(/404/) + const url = `http://localhost:${next.appPort}` - const serverSideJsRes = await fetchViaHTTP( - `${url}`, - '/_next/development/server/static/development/pages/stateless.js' + const resources: Set = new Set() + + const manifestKey = Object.keys(reactLoadableManifest).find((item) => { + return item + .replace(/\\/g, '/') + .endsWith('dynamic/css.js -> ../../components/dynamic-css/with-css') + }) + + // test dynamic chunk + reactLoadableManifest[manifestKey].files.forEach((f) => { + resources.add('/' + f) + }) + + // test main.js runtime etc + for (const item of buildManifest.pages['/']) { + resources.add('/' + item) + } + + const cssStaticAssets = await recursiveReadDir( + join(next.testDir, '.next', 'static'), + { pathnameFilter: (f) => /\.css$/.test(f) } ) - expect(serverSideJsRes.status).toBe(404) - const serverSideJsBody = await serverSideJsRes.text() - expect(serverSideJsBody).toMatch(/404/) - }) - - it('should not put backslashes in pages-manifest.json', () => { - // Whatever platform you build on, pages-manifest.json should use forward slash (/) - // See: https://github.com/vercel/next.js/issues/4920 - const pagesManifest = require(join( - next.testDir, - '.next', - 'server', - PAGES_MANIFEST - )) - - for (let key of Object.keys(pagesManifest)) { - expect(key).not.toMatch(/\\/) - expect(pagesManifest[key]).not.toMatch(/\\/) - } - }) - - it('should handle failed param decoding', async () => { - const html = await renderViaHTTP( - next.appPort, - '/invalid-param/%DE~%C7%1fY/' + expect(cssStaticAssets.length).toBeGreaterThanOrEqual(1) + expect(cssStaticAssets[0]).toMatch(/[\\/]css[\\/]/) + const mediaStaticAssets = await recursiveReadDir( + join(next.testDir, '.next', 'static'), + { pathnameFilter: (f) => /\.svg$/.test(f) } ) - expect(html).toMatch(/400/) - expect(html).toMatch(/Bad Request/) - }) + expect(mediaStaticAssets.length).toBeGreaterThanOrEqual(1) + expect(mediaStaticAssets[0]).toMatch(/[\\/]media[\\/]/) + ;[...cssStaticAssets, ...mediaStaticAssets].forEach((asset) => { + resources.add(`/static${asset.replace(/\\+/g, '/')}`) + }) - it('should replace static pages with HTML files', async () => { - const pages = ['/about', '/another', '/counter', '/dynamic', '/prefetch'] - for (const page of pages) { - const file = getPageFileFromPagesManifest(next.testDir, page) + const responses = await Promise.all( + [...resources].map((resource) => + fetchViaHTTP(url, join('/_next', resource)) + ) + ) - expect(file.endsWith('.html')).toBe(true) - } - }) - - it('should not replace non-static pages with HTML files', async () => { - const pages = ['/api', '/external-and-back', '/finish-response'] - - for (const page of pages) { - const file = getPageFileFromPagesManifest(next.testDir, page) - - expect(file.endsWith('.js')).toBe(true) - } - }) - - it('should handle AMP correctly in IE', async () => { - const browser = await webdriver(next.appPort, '/some-amp') - const text = await browser.elementByCss('p').text() - expect(text).toBe('Not AMP') - }) - - it('should warn when prefetch is true', async () => { - if (global.browserName !== 'chrome') return - let browser - try { - browser = await webdriver(next.appPort, '/development-logs') - const browserLogs = await browser.log('browser') - let found = false - browserLogs.forEach((log) => { - if (log.message.includes('Next.js auto-prefetches automatically')) { - found = true - } - }) - expect(found).toBe(false) - } finally { - if (browser) { - await browser.close() + responses.forEach((res) => { + try { + expect(res.headers.get('Cache-Control')).toBe( + 'public, max-age=31536000, immutable' + ) + } catch (err) { + err.message = res.url + ' ' + err.message + throw err } + }) + }) + + it('should set correct Cache-Control header for static 404s', async () => { + // this is to fix where 404 headers are set to 'public, max-age=31536000, immutable' + const res = await fetchViaHTTP( + `http://localhost:${next.appPort}`, + `/_next//static/common/bad-static.js` + ) + + expect(res.status).toBe(404) + expect(res.headers.get('Cache-Control')).toBe( + 'no-cache, no-store, max-age=0, must-revalidate' + ) + }) + + it('should block special pages', async () => { + const urls = ['/_document', '/_app'] + for (const url of urls) { + const html = await renderViaHTTP(next.appPort, url) + expect(html).toMatch(/404/) } }) - it('should not emit stats', async () => { - expect(existsSync(join(next.testDir, '.next', 'next-stats.json'))).toBe( + it('should not contain customServer in NEXT_DATA', async () => { + const html = await renderViaHTTP(next.appPort, '/') + const $ = cheerio.load(html) + expect('customServer' in JSON.parse($('#__NEXT_DATA__').text())).toBe( false ) }) + }) - it('should contain the Next.js version in window export', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/about') - const version = await browser.eval('window.next.version') - expect(version).toBeTruthy() - expect(version).toBe( - (await next.readJSON('node_modules/next/package.json')).version - ) - } finally { - if (browser) { - await browser.close() - } - } + describe('API routes', () => { + it('should work with pages/api/index.js', async () => { + const url = `http://localhost:${next.appPort}` + const res = await fetchViaHTTP(url, `/api`) + const body = await res.text() + expect(body).toEqual('API index works') }) - it('should clear all core performance marks', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/fully-dynamic') - - const currentPerfMarks = await browser.eval( - `window.performance.getEntriesByType('mark')` - ) - const allPerfMarks = [ - 'beforeRender', - 'afterHydrate', - 'afterRender', - 'routeChange', - ] - - allPerfMarks.forEach((name) => - expect(currentPerfMarks).not.toContainEqual( - expect.objectContaining({ name }) - ) - ) - } finally { - if (browser) { - await browser.close() - } - } + it('should work with pages/api/hello.js', async () => { + const url = `http://localhost:${next.appPort}` + const res = await fetchViaHTTP(url, `/api/hello`) + const body = await res.text() + expect(body).toEqual('API hello works') }) - it('should not clear custom performance marks', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/mark-in-head') - - const customMarkFound = await browser.eval( - `window.performance.getEntriesByType('mark').filter(function(e) { - return e.name === 'custom-mark' - }).length === 1` - ) - expect(customMarkFound).toBe(true) - } finally { - if (browser) { - await browser.close() - } - } + // Today, `__dirname` usage fails because Next.js moves the source file + // to .next/server/pages/api but it doesn't move the asset file. + // In the future, it would be nice to make `__dirname` work too. + it('does not work with pages/api/readfile-dirname.js', async () => { + const url = `http://localhost:${next.appPort}` + const res = await fetchViaHTTP(url, `/api/readfile-dirname`) + expect(res.status).toBe(500) }) - it('should have defer on all script tags', async () => { - const html = await renderViaHTTP(next.appPort, '/') - const $ = cheerio.load(html) - let missing = false - - for (const script of $('script').toArray()) { - // application/json doesn't need async - if ( - script.attribs.type === 'application/json' || - script.attribs.src.includes('polyfills') - ) { - continue - } - - if (script.attribs.defer !== '' || script.attribs.async === '') { - missing = true - } - } - expect(missing).toBe(false) + it('should work with pages/api/readfile-processcwd.js', async () => { + const url = `http://localhost:${next.appPort}` + const res = await fetchViaHTTP(url, `/api/readfile-processcwd`) + const body = await res.text() + expect(body).toBe('item') }) - it('should only have one DOCTYPE', async () => { - const html = await renderViaHTTP(next.appPort, '/') - expect(html).toMatch(/^ { + const url = `http://localhost:${next.appPort}` + const res = await fetchViaHTTP(url, `/api/post-1?val=1`) + const body = await res.json() + + expect(body).toEqual({ val: '1', post: 'post-1' }) + }) + }) + + describe('With navigation', () => { + it('should navigate via client side', async () => { + const browser = await webdriver(next.appPort, '/') + const text = await browser + .elementByCss('a') + .click() + .waitForElementByCss('.about-page') + .elementByCss('.about-page') + .text() + + expect(text).toBe('About Page') + await browser.close() }) - if (global.browserName !== 'internet explorer') { - it('should preserve query when hard navigating from page 404', async () => { - const browser = await webdriver(next.appPort, '/') - await browser.eval(`(function() { - window.beforeNav = 1 - window.next.router.push({ - pathname: '/non-existent', - query: { hello: 'world' } - }) - })()`) + it('should navigate to nested index via client side', async () => { + const browser = await webdriver(next.appPort, '/another') + await browser.eval('window.beforeNav = 1') - await check( - () => browser.eval('document.documentElement.innerHTML'), - /page could not be found/ - ) + const text = await browser + .elementByCss('a') + .click() + .waitForElementByCss('.index-page') + .elementByCss('p') + .text() - expect(await browser.eval('window.beforeNav')).toBeFalsy() - expect(await browser.eval('window.location.hash')).toBe('') - expect(await browser.eval('window.location.search')).toBe( - '?hello=world' - ) - expect(await browser.eval('window.location.pathname')).toBe( - '/non-existent' - ) + expect(text).toBe('Hello World') + expect(await browser.eval('window.beforeNav')).toBe(1) + await browser.close() + }) + + it('should reload page successfully (on bad link)', async () => { + const browser = await webdriver(next.appPort, '/to-nonexistent') + await browser.eval(function setup() { + // @ts-expect-error Exists on window + window.__DATA_BE_GONE = 'true' + }) + await browser + .waitForElementByCss('#to-nonexistent-page') + .click('#to-nonexistent-page') + await browser.waitForElementByCss('.about-page') + + const oldData = await browser.eval(`window.__DATA_BE_GONE`) + expect(oldData).toBeFalsy() + }) + + it('should reload page successfully (on bad data fetch)', async () => { + const browser = await webdriver(next.appPort, '/to-shadowed-page') + await browser.eval(function setup() { + // @ts-expect-error Exists on window + window.__DATA_BE_GONE = 'true' + }) + await browser.waitForElementByCss('#to-shadowed-page').click() + await browser.waitForElementByCss('.about-page') + + const oldData = await browser.eval(`window.__DATA_BE_GONE`) + expect(oldData).toBeFalsy() + }) + }) + + it('should navigate to external site and back', async () => { + const browser = await webdriver(next.appPort, '/external-and-back') + const initialText = await browser.elementByCss('p').text() + expect(initialText).toBe('server') + + await browser + .elementByCss('a') + .click() + .waitForElementByCss('input') + .back() + .waitForElementByCss('p') + + await waitFor(1000) + const newText = await browser.elementByCss('p').text() + expect(newText).toBe('server') + }) + + it('should navigate to page with CSS and back', async () => { + const browser = await webdriver(next.appPort, '/css-and-back') + const initialText = await browser.elementByCss('p').text() + expect(initialText).toBe('server') + + await browser + .elementByCss('a') + .click() + .waitForElementByCss('input') + .back() + .waitForElementByCss('p') + + await waitFor(1000) + const newText = await browser.elementByCss('p').text() + expect(newText).toBe('client') + }) + + it('should navigate to external site and back (with query)', async () => { + const browser = await webdriver( + next.appPort, + '/external-and-back?hello=world' + ) + const initialText = await browser.elementByCss('p').text() + expect(initialText).toBe('server') + + await browser + .elementByCss('a') + .click() + .waitForElementByCss('input') + .back() + .waitForElementByCss('p') + + await waitFor(1000) + const newText = await browser.elementByCss('p').text() + expect(newText).toBe('server') + }) + + it('should change query correctly', async () => { + const browser = await webdriver(next.appPort, '/query?id=0') + let id = await browser.elementByCss('#q0').text() + expect(id).toBe('0') + + await browser.elementByCss('#first').click().waitForElementByCss('#q1') + + id = await browser.elementByCss('#q1').text() + expect(id).toBe('1') + + await browser.elementByCss('#second').click().waitForElementByCss('#q2') + + id = await browser.elementByCss('#q2').text() + expect(id).toBe('2') + }) + + describe('Runtime errors', () => { + it('should render a server side error on the client side', async () => { + const browser = await webdriver(next.appPort, '/error-in-ssr-render') + await waitFor(2000) + const text = await browser.elementByCss('body').text() + // this makes sure we don't leak the actual error to the client side in production + expect(text).toMatch(/Internal Server Error\./) + const headingText = await browser.elementByCss('h1').text() + // This makes sure we render statusCode on the client side correctly + expect(headingText).toBe('500') + await browser.close() + }) + + it('should render a client side component error', async () => { + const browser = await webdriver(next.appPort, '/error-in-browser-render') + await waitFor(2000) + const text = await browser.elementByCss('body').text() + expect(text).toMatch( + /Application error: a client-side exception has occurred/ + ) + await browser.close() + }) + + it('should call getInitialProps on _error page during a client side component error', async () => { + const browser = await webdriver( + next.appPort, + '/error-in-browser-render-status-code' + ) + await waitFor(2000) + const text = await browser.elementByCss('body').text() + expect(text).toMatch(/This page could not be found\./) + await browser.close() + }) + }) + + describe('Misc', () => { + it('should handle already finished responses', async () => { + const html = await renderViaHTTP(next.appPort, '/finish-response') + expect(html).toBe('hi') + }) + + it('should allow to access /static/ and /_next/', async () => { + // This is a test case which prevent the following issue happening again. + // See: https://github.com/vercel/next.js/issues/2617 + await renderViaHTTP(next.appPort, '/_next/') + await renderViaHTTP(next.appPort, '/static/') + const data = await renderViaHTTP(next.appPort, '/static/data/item.txt') + expect(data).toBe('item') + }) + + it('Should allow access to public files', async () => { + const data = await renderViaHTTP(next.appPort, '/data/data.txt') + const file = await renderViaHTTP(next.appPort, '/file') + const legacy = await renderViaHTTP(next.appPort, '/static/legacy.txt') + expect(data).toBe('data') + expect(file).toBe('test') + expect(legacy).toMatch(`new static folder`) + }) + + // TODO: do we want to normalize this for firefox? It seems in + // the latest version of firefox the window state is not reset + // when navigating back from a hard navigation. This might be + // a bug as other browsers do not behave this way. + if (global.browserName !== 'firefox') { + it('should reload the page on page script error', async () => { + const browser = await webdriver(next.appPort, '/counter') + const counter = await browser + .elementByCss('#increase') + .click() + .click() + .elementByCss('#counter') + .text() + expect(counter).toBe('Counter: 2') + + // When we go to the 404 page, it'll do a hard reload. + // So, it's possible for the front proxy to load a page from another zone. + // Since the page is reloaded, when we go back to the counter page again, + // previous counter value should be gone. + const counterAfter404Page = await browser + .elementByCss('#no-such-page') + .click() + .waitForElementByCss('h1') + .back() + .waitForElementByCss('#counter-page') + .elementByCss('#counter') + .text() + expect(counterAfter404Page).toBe('Counter: 0') + + await browser.close() }) } - it('should remove placeholder for next/image correctly', async () => { - const browser = await webdriver(next.appPort, '/') + it('should have default runtime values when not defined', async () => { + const html = await renderViaHTTP(next.appPort, '/runtime-config') + expect(html).toMatch(/found public config/) + expect(html).toMatch(/found server config/) + }) - await browser.eval(`(function() { - window.beforeNav = 1 - window.next.router.push('/static-image') - })()`) - await browser.waitForElementByCss('#static-image') + it('should not have runtimeConfig in __NEXT_DATA__', async () => { + const html = await renderViaHTTP(next.appPort, '/runtime-config') + const $ = cheerio.load(html) + const script = $('#__NEXT_DATA__').html() + expect(script).not.toMatch(/runtimeConfig/) + }) - expect(await browser.eval('window.beforeNav')).toBe(1) - - await check( - () => browser.elementByCss('img').getComputedCss('background-image'), - 'none' - ) - - await browser.eval(`(function() { - window.beforeNav = 1 - window.next.router.push('/') - })()`) - await browser.waitForElementByCss('.index-page') - await waitFor(1000) - - await browser.eval(`(function() { - window.beforeNav = 1 - window.next.router.push('/static-image') - })()`) - await browser.waitForElementByCss('#static-image') - - expect(await browser.eval('window.beforeNav')).toBe(1) - - await check( - () => - browser - .elementByCss('#static-image') - .getComputedCss('background-image'), - 'none' - ) - - for (let i = 0; i < 5; i++) { - expect( - await browser - .elementByCss('#static-image') - .getComputedCss('background-image') - ).toBe('none') - await waitFor(500) + it('should add autoExport for auto pre-rendered pages', async () => { + for (const page of ['/about']) { + const html = await renderViaHTTP(next.appPort, page) + const $ = cheerio.load(html) + const data = JSON.parse($('#__NEXT_DATA__').html()) + expect(data.autoExport).toBe(true) } }) - dynamicImportTests(next, (p, q) => renderViaHTTP(next.appPort, p, q)) + it('should not add autoExport for non pre-rendered pages', async () => { + for (const page of ['/query']) { + const html = await renderViaHTTP(next.appPort, page) + const $ = cheerio.load(html) + const data = JSON.parse($('#__NEXT_DATA__').html()) + expect(!!data.autoExport).toBe(false) + } + }) - processEnv(next) - if (global.browserName !== 'safari') security(next) + it('should add prefetch tags when Link prefetch prop is used', async () => { + const browser = await webdriver(next.appPort, '/prefetch') + + if (global.browserName === 'internet explorer') { + // IntersectionObserver isn't present so we need to trigger manually + await waitFor(1000) + await browser.eval(`(function() { + window.next.router.prefetch('/') + window.next.router.prefetch('/process-env') + window.next.router.prefetch('/counter') + window.next.router.prefetch('/about') + })()`) + } + + await waitFor(2000) + + if (global.browserName === 'safari') { + const elements = await browser.elementsByCss('link[rel=preload]') + // optimized preloading uses defer instead of preloading and prefetches + // aren't generated client-side since safari does not support prefetch + expect(elements.length).toBe(0) + } else { + const elements = await browser.elementsByCss('link[rel=prefetch]') + expect(elements.length).toBe(4) + + for (const element of elements) { + const rel = await element.getAttribute('rel') + const as = await element.getAttribute('as') + expect(rel).toBe('prefetch') + expect(as).toBe('script') + } + } + await browser.close() + }) + + // This is a workaround to fix https://github.com/vercel/next.js/issues/5860 + // TODO: remove this workaround when https://bugs.webkit.org/show_bug.cgi?id=187726 is fixed. + it('It does not add a timestamp to link tags with prefetch attribute', async () => { + const browser = await webdriver(next.appPort, '/prefetch') + const links = await browser.elementsByCss('link[rel=prefetch]') + + for (const element of links) { + const href = await element.getAttribute('href') + expect(href).not.toMatch(/\?ts=/) + } + const scripts = await browser.elementsByCss('script[src]') + + for (const element of scripts) { + const src = await element.getAttribute('src') + expect(src).not.toMatch(/\?ts=/) + } + await browser.close() + }) + + if (global.browserName === 'chrome') { + it('should reload the page on page script error with prefetch', async () => { + const browser = await webdriver(next.appPort, '/counter') + if (global.browserName !== 'chrome') return + const counter = await browser + .elementByCss('#increase') + .click() + .click() + .elementByCss('#counter') + .text() + expect(counter).toBe('Counter: 2') + + // Let the browser to prefetch the page and error it on the console. + await waitFor(3000) + + // When we go to the 404 page, it'll do a hard reload. + // So, it's possible for the front proxy to load a page from another zone. + // Since the page is reloaded, when we go back to the counter page again, + // previous counter value should be gone. + const counterAfter404Page = await browser + .elementByCss('#no-such-page-prefetch') + .click() + .waitForElementByCss('h1') + .back() + .waitForElementByCss('#counter-page') + .elementByCss('#counter') + .text() + expect(counterAfter404Page).toBe('Counter: 0') + + await browser.close() + }) + } + }) + + it('should not expose the compiled page file in development', async () => { + const url = `http://localhost:${next.appPort}` + await fetchViaHTTP(`${url}`, `/stateless`) // make sure the stateless page is built + const clientSideJsRes = await fetchViaHTTP( + `${url}`, + '/_next/development/static/development/pages/stateless.js' + ) + expect(clientSideJsRes.status).toBe(404) + const clientSideJsBody = await clientSideJsRes.text() + expect(clientSideJsBody).toMatch(/404/) + + const serverSideJsRes = await fetchViaHTTP( + `${url}`, + '/_next/development/server/static/development/pages/stateless.js' + ) + expect(serverSideJsRes.status).toBe(404) + const serverSideJsBody = await serverSideJsRes.text() + expect(serverSideJsBody).toMatch(/404/) + }) + + it('should not put backslashes in pages-manifest.json', () => { + // Whatever platform you build on, pages-manifest.json should use forward slash (/) + // See: https://github.com/vercel/next.js/issues/4920 + const pagesManifest = require(join( + next.testDir, + '.next', + 'server', + PAGES_MANIFEST + )) + + for (let key of Object.keys(pagesManifest)) { + expect(key).not.toMatch(/\\/) + expect(pagesManifest[key]).not.toMatch(/\\/) + } + }) + + it('should handle failed param decoding', async () => { + const html = await renderViaHTTP( + next.appPort, + '/invalid-param/%DE~%C7%1fY/' + ) + expect(html).toMatch(/400/) + expect(html).toMatch(/Bad Request/) + }) + + it('should replace static pages with HTML files', async () => { + const pages = ['/about', '/another', '/counter', '/dynamic', '/prefetch'] + for (const page of pages) { + const file = getPageFileFromPagesManifest(next.testDir, page) + + expect(file.endsWith('.html')).toBe(true) + } + }) + + it('should not replace non-static pages with HTML files', async () => { + const pages = ['/api', '/external-and-back', '/finish-response'] + + for (const page of pages) { + const file = getPageFileFromPagesManifest(next.testDir, page) + + expect(file.endsWith('.js')).toBe(true) + } + }) + + it('should handle AMP correctly in IE', async () => { + const browser = await webdriver(next.appPort, '/some-amp') + const text = await browser.elementByCss('p').text() + expect(text).toBe('Not AMP') + }) + + it('should warn when prefetch is true', async () => { + if (global.browserName !== 'chrome') return + let browser + try { + browser = await webdriver(next.appPort, '/development-logs') + const browserLogs = await browser.log('browser') + let found = false + browserLogs.forEach((log) => { + if (log.message.includes('Next.js auto-prefetches automatically')) { + found = true + } + }) + expect(found).toBe(false) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should not emit stats', async () => { + expect(existsSync(join(next.testDir, '.next', 'next-stats.json'))).toBe( + false + ) + }) + + it('should contain the Next.js version in window export', async () => { + let browser + try { + browser = await webdriver(next.appPort, '/about') + const version = await browser.eval('window.next.version') + expect(version).toBeTruthy() + expect(version).toBe( + (await next.readJSON('node_modules/next/package.json')).version + ) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should clear all core performance marks', async () => { + let browser + try { + browser = await webdriver(next.appPort, '/fully-dynamic') + + const currentPerfMarks = await browser.eval( + `window.performance.getEntriesByType('mark')` + ) + const allPerfMarks = [ + 'beforeRender', + 'afterHydrate', + 'afterRender', + 'routeChange', + ] + + allPerfMarks.forEach((name) => + expect(currentPerfMarks).not.toContainEqual( + expect.objectContaining({ name }) + ) + ) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should not clear custom performance marks', async () => { + let browser + try { + browser = await webdriver(next.appPort, '/mark-in-head') + + const customMarkFound = await browser.eval( + `window.performance.getEntriesByType('mark').filter(function(e) { + return e.name === 'custom-mark' + }).length === 1` + ) + expect(customMarkFound).toBe(true) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should have defer on all script tags', async () => { + const html = await renderViaHTTP(next.appPort, '/') + const $ = cheerio.load(html) + let missing = false + + for (const script of $('script').toArray()) { + // application/json doesn't need async + if ( + script.attribs.type === 'application/json' || + script.attribs.src.includes('polyfills') + ) { + continue + } + + if (script.attribs.defer !== '' || script.attribs.async === '') { + missing = true + } + } + expect(missing).toBe(false) + }) + + it('should only have one DOCTYPE', async () => { + const html = await renderViaHTTP(next.appPort, '/') + expect(html).toMatch(/^ { + const browser = await webdriver(next.appPort, '/') + await browser.eval(`(function() { + window.beforeNav = 1 + window.next.router.push({ + pathname: '/non-existent', + query: { hello: 'world' } + }) + })()`) + + await check( + () => browser.eval('document.documentElement.innerHTML'), + /page could not be found/ + ) + + expect(await browser.eval('window.beforeNav')).toBeFalsy() + expect(await browser.eval('window.location.hash')).toBe('') + expect(await browser.eval('window.location.search')).toBe('?hello=world') + expect(await browser.eval('window.location.pathname')).toBe( + '/non-existent' + ) + }) } -) + + it('should remove placeholder for next/image correctly', async () => { + const browser = await webdriver(next.appPort, '/') + + await browser.eval(`(function() { + window.beforeNav = 1 + window.next.router.push('/static-image') + })()`) + await browser.waitForElementByCss('#static-image') + + expect(await browser.eval('window.beforeNav')).toBe(1) + + await check( + () => browser.elementByCss('img').getComputedCss('background-image'), + 'none' + ) + + await browser.eval(`(function() { + window.beforeNav = 1 + window.next.router.push('/') + })()`) + await browser.waitForElementByCss('.index-page') + await waitFor(1000) + + await browser.eval(`(function() { + window.beforeNav = 1 + window.next.router.push('/static-image') + })()`) + await browser.waitForElementByCss('#static-image') + + expect(await browser.eval('window.beforeNav')).toBe(1) + + await check( + () => + browser + .elementByCss('#static-image') + .getComputedCss('background-image'), + 'none' + ) + + for (let i = 0; i < 5; i++) { + expect( + await browser + .elementByCss('#static-image') + .getComputedCss('background-image') + ).toBe('none') + await waitFor(500) + } + }) + + dynamicImportTests(next, (p, q) => renderViaHTTP(next.appPort, p, q)) + + processEnv(next) + if (global.browserName !== 'safari') security(next) +}) diff --git a/test/production/sharp-basic/sharp-basic.test.ts b/test/production/sharp-basic/sharp-basic.test.ts index c8057c306f..beac13b0a4 100644 --- a/test/production/sharp-basic/sharp-basic.test.ts +++ b/test/production/sharp-basic/sharp-basic.test.ts @@ -1,8 +1,7 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' -createNextDescribe( - 'sharp support with hasNextSupport', - { +describe('sharp support with hasNextSupport', () => { + const { next } = nextTestSetup({ files: __dirname, dependencies: { sharp: 'latest', @@ -10,13 +9,12 @@ createNextDescribe( env: { NOW_BUILDER: '1', }, - }, - ({ next }) => { - // we're mainly checking if build/start were successful so - // we have a basic assertion here - it('should work using cheerio', async () => { - const $ = await next.render$('/') - expect($('p').text()).toBe('hello world') - }) - } -) + }) + + // we're mainly checking if build/start were successful so + // we have a basic assertion here + it('should work using cheerio', async () => { + const $ = await next.render$('/') + expect($('p').text()).toBe('hello world') + }) +}) diff --git a/test/production/standalone-mode/basic/index.test.ts b/test/production/standalone-mode/basic/index.test.ts index f738240f7e..dbccf9c6a7 100644 --- a/test/production/standalone-mode/basic/index.test.ts +++ b/test/production/standalone-mode/basic/index.test.ts @@ -1,44 +1,42 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' -createNextDescribe( - 'standalone mode - metadata routes', - { +describe('standalone mode - metadata routes', () => { + const { next } = nextTestSetup({ files: __dirname, dependencies: { swr: 'latest', }, - }, - ({ next }) => { - beforeAll(async () => { - // Hide source files to make sure route.js can read files from source - // in order to hit the prerender cache - await next.renameFolder('app', 'app_hidden') - }) + }) - it('should handle metadata icons correctly', async () => { - const faviconRes = await next.fetch('/favicon.ico') - const iconRes = await next.fetch('/icon.svg') - expect(faviconRes.status).toBe(200) - expect(iconRes.status).toBe(200) - }) + beforeAll(async () => { + // Hide source files to make sure route.js can read files from source + // in order to hit the prerender cache + await next.renameFolder('app', 'app_hidden') + }) - it('should handle correctly not-found.js', async () => { - const res = await next.fetch('/not-found/does-not-exist') - expect(res.status).toBe(404) - const html = await res.text() - expect(html).toContain('app-not-found') - }) + it('should handle metadata icons correctly', async () => { + const faviconRes = await next.fetch('/favicon.ico') + const iconRes = await next.fetch('/icon.svg') + expect(faviconRes.status).toBe(200) + expect(iconRes.status).toBe(200) + }) - it('should handle private _next unmatched route correctly', async () => { - const res = await next.fetch('/_next/does-not-exist') - expect(res.status).toBe(404) - const html = await res.text() - expect(html).toContain('app-not-found') - }) + it('should handle correctly not-found.js', async () => { + const res = await next.fetch('/not-found/does-not-exist') + expect(res.status).toBe(404) + const html = await res.text() + expect(html).toContain('app-not-found') + }) - it('should handle pages rendering correctly', async () => { - const browser = await next.browser('/hello') - expect(await browser.elementByCss('#content').text()).toBe('hello-bar') - }) - } -) + it('should handle private _next unmatched route correctly', async () => { + const res = await next.fetch('/_next/does-not-exist') + expect(res.status).toBe(404) + const html = await res.text() + expect(html).toContain('app-not-found') + }) + + it('should handle pages rendering correctly', async () => { + const browser = await next.browser('/hello') + expect(await browser.elementByCss('#content').text()).toBe('hello-bar') + }) +}) diff --git a/test/production/standalone-mode/no-app-routes/index.test.ts b/test/production/standalone-mode/no-app-routes/index.test.ts index 7a6884bf40..aeebbc247d 100644 --- a/test/production/standalone-mode/no-app-routes/index.test.ts +++ b/test/production/standalone-mode/no-app-routes/index.test.ts @@ -1,14 +1,12 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' -createNextDescribe( - 'standalone mode - no app routes', - { +describe('standalone mode - no app routes', () => { + const { next } = nextTestSetup({ files: __dirname, - }, - ({ next }) => { - it('should handle pages rendering correctly', async () => { - const browser = await next.browser('/hello') - expect(await browser.elementByCss('#index').text()).toBe('index-page') - }) - } -) + }) + + it('should handle pages rendering correctly', async () => { + const browser = await next.browser('/hello') + expect(await browser.elementByCss('#index').text()).toBe('index-page') + }) +}) diff --git a/test/production/supports-module-resolution-nodenext/supports-moduleresolution-nodenext.test.ts b/test/production/supports-module-resolution-nodenext/supports-moduleresolution-nodenext.test.ts index 8a669dcafd..0690397b8b 100644 --- a/test/production/supports-module-resolution-nodenext/supports-moduleresolution-nodenext.test.ts +++ b/test/production/supports-module-resolution-nodenext/supports-moduleresolution-nodenext.test.ts @@ -1,10 +1,9 @@ -import { createNextDescribe, FileRef } from 'e2e-utils' +import { nextTestSetup, FileRef } from 'e2e-utils' import { join } from 'path' // regression test suite for https://github.com/vercel/next.js/issues/38854 -createNextDescribe( - 'Does not override tsconfig moduleResolution field during build', - { +describe('Does not override tsconfig moduleResolution field during build', () => { + const { next } = nextTestSetup({ packageJson: { type: 'module' }, files: { 'tsconfig.json': new FileRef(join(__dirname, 'tsconfig.json')), @@ -17,10 +16,9 @@ createNextDescribe( '@types/node': 'latest', pkg: './pkg', }, - }, - ({ next }) => { - it('boots and renders without throwing an error', async () => { - await next.render$('/') - }) - } -) + }) + + it('boots and renders without throwing an error', async () => { + await next.render$('/') + }) +}) diff --git a/test/production/terser-class-static-blocks/terser-class-static-blocks.test.ts b/test/production/terser-class-static-blocks/terser-class-static-blocks.test.ts index 66ad066e55..afd11457c9 100644 --- a/test/production/terser-class-static-blocks/terser-class-static-blocks.test.ts +++ b/test/production/terser-class-static-blocks/terser-class-static-blocks.test.ts @@ -1,17 +1,15 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' -createNextDescribe( - 'terser-class-static-blocks', - { +describe('terser-class-static-blocks', () => { + const { next } = nextTestSetup({ files: __dirname, nextConfig: { swcMinify: false, }, - }, - ({ next }) => { - it('should work using cheerio', async () => { - const $ = await next.render$('/') - expect($('p').text()).toBe('hello world') - }) - } -) + }) + + it('should work using cheerio', async () => { + const $ = await next.render$('/') + expect($('p').text()).toBe('hello world') + }) +}) diff --git a/test/production/transpile-packages/transpile-packages.test.ts b/test/production/transpile-packages/transpile-packages.test.ts index ca830b755f..2fc40b5cb7 100644 --- a/test/production/transpile-packages/transpile-packages.test.ts +++ b/test/production/transpile-packages/transpile-packages.test.ts @@ -1,25 +1,23 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' -createNextDescribe( - 'app fetch build cache', - { +describe('app fetch build cache', () => { + const { next } = nextTestSetup({ files: __dirname, dependencies: { '@aws-sdk/client-s3': 'latest', lodash: 'latest', 'fast-xml-parser': '4.2.5', // https://github.com/aws/aws-sdk-js-v3/issues/5866#issuecomment-1984616572 }, - }, - ({ next }) => { - it('should render page with dependencies', async () => { - const $ = await next.render$('/') - expect($('#key').text()).toBe('Key: key1') - expect($('#isObject').text()).toBe('isObject: true') - }) + }) - it('should bundle @aws-sdk/client-s3 as a transpiled package', async () => { - const output = await next.readFile('.next/server/app/page.js') - expect(output).not.toContain('require("@aws-sdk/client-s3")') - }) - } -) + it('should render page with dependencies', async () => { + const $ = await next.render$('/') + expect($('#key').text()).toBe('Key: key1') + expect($('#isObject').text()).toBe('isObject: true') + }) + + it('should bundle @aws-sdk/client-s3 as a transpiled package', async () => { + const output = await next.readFile('.next/server/app/page.js') + expect(output).not.toContain('require("@aws-sdk/client-s3")') + }) +})