From 7a2ec4160104215b710e67622d270181adf3ef37 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Mon, 19 Dec 2022 10:20:01 +0100 Subject: [PATCH] Refactor more tests to createNextDescribe (#44104) --- .../acceptance-app/ReactRefresh.test.ts | 5 - .../ReactRefreshLogBox-builtins.test.ts | 5 - .../ReactRefreshLogBox-scss.test.ts | 5 - .../acceptance-app/ReactRefreshLogBox.test.ts | 5 - .../ReactRefreshLogBoxMisc.test.ts | 5 - .../acceptance-app/ReactRefreshModule.test.ts | 5 - .../ReactRefreshRegression.test.ts | 5 - .../acceptance-app/server-components.test.ts | 5 - test/development/basic-basepath/hmr.test.ts | 6 +- test/development/basic/hmr.test.ts | 6 +- test/e2e/app-dir/app-alias.test.ts | 87 +- test/e2e/app-dir/app-edge-global.test.ts | 49 +- test/e2e/app-dir/app-edge.test.ts | 132 +- test/e2e/app-dir/app-external.test.ts | 289 ++-- test/e2e/app-dir/app-middleware.test.ts | 276 ++-- test/e2e/app-dir/app-static.test.ts | 1207 ++++++++--------- test/e2e/app-dir/asset-prefix.test.ts | 92 +- .../app-dir/async-component-preload.test.ts | 41 +- .../app-dir/back-button-download-bug.test.ts | 58 +- test/e2e/app-dir/create-root-layout.test.ts | 13 +- test/e2e/app-dir/dynamic-href.test.ts | 120 +- test/e2e/app-dir/global-error.test.ts | 54 +- test/e2e/app-dir/import.test.ts | 46 +- test/e2e/app-dir/index.test.ts | 603 +++----- test/e2e/app-dir/layout-params.test.ts | 138 +- test/e2e/app-dir/next-font.test.ts | 679 +++++----- test/e2e/app-dir/next-image.test.ts | 557 ++++---- test/e2e/app-dir/prefetching.test.ts | 106 +- test/e2e/app-dir/rendering.test.ts | 225 ++- test/e2e/app-dir/root-layout.test.ts | 362 +++-- test/e2e/app-dir/rsc-basic.test.ts | 183 ++- test/e2e/app-dir/rsc-errors.test.ts | 196 ++- test/e2e/app-dir/standalone.test.ts | 172 ++- test/e2e/app-dir/trailingslash.test.ts | 92 +- test/e2e/app-dir/vercel-analytics.test.ts | 3 +- test/e2e/app-dir/with-babel.test.ts | 41 +- test/e2e/transpile-packages/index.test.ts | 3 +- test/lib/e2e-utils.ts | 3 +- test/lib/next-modes/base.ts | 3 + 39 files changed, 2658 insertions(+), 3224 deletions(-) diff --git a/test/development/acceptance-app/ReactRefresh.test.ts b/test/development/acceptance-app/ReactRefresh.test.ts index 2060f3a657e2db4..fafd38c09b6bb1e 100644 --- a/test/development/acceptance-app/ReactRefresh.test.ts +++ b/test/development/acceptance-app/ReactRefresh.test.ts @@ -5,11 +5,6 @@ import { NextInstance } from 'test/lib/next-modes/base' import path from 'path' describe('ReactRefresh app', () => { - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } - let next: NextInstance beforeAll(async () => { diff --git a/test/development/acceptance-app/ReactRefreshLogBox-builtins.test.ts b/test/development/acceptance-app/ReactRefreshLogBox-builtins.test.ts index 26c403611a4e1b2..7d3a39e2a779742 100644 --- a/test/development/acceptance-app/ReactRefreshLogBox-builtins.test.ts +++ b/test/development/acceptance-app/ReactRefreshLogBox-builtins.test.ts @@ -5,11 +5,6 @@ import path from 'path' // TODO-APP: Investigate snapshot mismatch describe('ReactRefreshLogBox app', () => { - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } - let next: NextInstance beforeAll(async () => { diff --git a/test/development/acceptance-app/ReactRefreshLogBox-scss.test.ts b/test/development/acceptance-app/ReactRefreshLogBox-scss.test.ts index 14fdad092b47c8f..d5c4253706ab1a6 100644 --- a/test/development/acceptance-app/ReactRefreshLogBox-scss.test.ts +++ b/test/development/acceptance-app/ReactRefreshLogBox-scss.test.ts @@ -7,11 +7,6 @@ import path from 'path' // TODO: figure out why snapshots mismatch on GitHub actions // specifically but work in docker and locally describe.skip('ReactRefreshLogBox app', () => { - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } - let next: NextInstance beforeAll(async () => { diff --git a/test/development/acceptance-app/ReactRefreshLogBox.test.ts b/test/development/acceptance-app/ReactRefreshLogBox.test.ts index b11f6b5fa285a50..eb4aba76183e176 100644 --- a/test/development/acceptance-app/ReactRefreshLogBox.test.ts +++ b/test/development/acceptance-app/ReactRefreshLogBox.test.ts @@ -6,11 +6,6 @@ import { check } from 'next-test-utils' import path from 'path' describe('ReactRefreshLogBox app', () => { - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } - let next: NextInstance beforeAll(async () => { diff --git a/test/development/acceptance-app/ReactRefreshLogBoxMisc.test.ts b/test/development/acceptance-app/ReactRefreshLogBoxMisc.test.ts index f4d6e512c322c33..83768772c3e9a86 100644 --- a/test/development/acceptance-app/ReactRefreshLogBoxMisc.test.ts +++ b/test/development/acceptance-app/ReactRefreshLogBoxMisc.test.ts @@ -6,11 +6,6 @@ import path from 'path' // TODO: re-enable these tests after figuring out what is causing // them to be so unreliable in CI describe.skip('ReactRefreshLogBox app', () => { - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } - let next: NextInstance beforeAll(async () => { diff --git a/test/development/acceptance-app/ReactRefreshModule.test.ts b/test/development/acceptance-app/ReactRefreshModule.test.ts index 3346ca4d3846f72..614148ace38e6c6 100644 --- a/test/development/acceptance-app/ReactRefreshModule.test.ts +++ b/test/development/acceptance-app/ReactRefreshModule.test.ts @@ -4,11 +4,6 @@ import { NextInstance } from 'test/lib/next-modes/base' import { sandbox } from './helpers' describe('ReactRefreshModule app', () => { - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } - let next: NextInstance beforeAll(async () => { diff --git a/test/development/acceptance-app/ReactRefreshRegression.test.ts b/test/development/acceptance-app/ReactRefreshRegression.test.ts index 88c19bf0cd53b4f..9e485359636c5e3 100644 --- a/test/development/acceptance-app/ReactRefreshRegression.test.ts +++ b/test/development/acceptance-app/ReactRefreshRegression.test.ts @@ -5,11 +5,6 @@ import { NextInstance } from 'test/lib/next-modes/base' import path from 'path' describe('ReactRefreshRegression app', () => { - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } - let next: NextInstance beforeAll(async () => { diff --git a/test/development/acceptance-app/server-components.test.ts b/test/development/acceptance-app/server-components.test.ts index cdace90cd7b8d05..3e086ee44af7f85 100644 --- a/test/development/acceptance-app/server-components.test.ts +++ b/test/development/acceptance-app/server-components.test.ts @@ -6,11 +6,6 @@ import path from 'path' import { check } from 'next-test-utils' describe('Error Overlay for server components', () => { - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } - let next: NextInstance beforeAll(async () => { diff --git a/test/development/basic-basepath/hmr.test.ts b/test/development/basic-basepath/hmr.test.ts index 8f80697a64d68b3..00299dc267bc562 100644 --- a/test/development/basic-basepath/hmr.test.ts +++ b/test/development/basic-basepath/hmr.test.ts @@ -532,14 +532,10 @@ describe('basic HMR', () => { ) ) - const isReact17 = process.env.NEXT_TEST_REACT_VERSION === '^17' - expect(await hasRedbox(browser)).toBe(true) // TODO: Replace this when webpack 5 is the default expect(await getRedboxHeader(browser)).toMatch( - `Objects are not valid as a React child (found: ${ - isReact17 ? '/search/' : '[object RegExp]' - }). If you meant to render a collection of children, use an array instead.` + `Objects are not valid as a React child (found: [object RegExp]). If you meant to render a collection of children, use an array instead.` ) await next.patchFile(aboutPage, aboutContent) diff --git a/test/development/basic/hmr.test.ts b/test/development/basic/hmr.test.ts index 13982bd06e9c1f1..d83008bcdc3d004 100644 --- a/test/development/basic/hmr.test.ts +++ b/test/development/basic/hmr.test.ts @@ -597,14 +597,10 @@ describe('basic HMR', () => { ) ) - const isReact17 = process.env.NEXT_TEST_REACT_VERSION === '^17' - expect(await hasRedbox(browser)).toBe(true) // TODO: Replace this when webpack 5 is the default expect(await getRedboxHeader(browser)).toMatch( - `Objects are not valid as a React child (found: ${ - isReact17 ? '/search/' : '[object RegExp]' - }). If you meant to render a collection of children, use an array instead.` + `Objects are not valid as a React child (found: [object RegExp]). If you meant to render a collection of children, use an array instead.` ) await next.patchFile(aboutPage, aboutContent) diff --git a/test/e2e/app-dir/app-alias.test.ts b/test/e2e/app-dir/app-alias.test.ts index e857c483236ba4c..1c8583af624e36f 100644 --- a/test/e2e/app-dir/app-alias.test.ts +++ b/test/e2e/app-dir/app-alias.test.ts @@ -1,56 +1,45 @@ -import { createNext, FileRef } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' -import { renderViaHTTP } from 'next-test-utils' -import webdriver from 'next-webdriver' +import { createNextDescribe } from 'e2e-utils' import path from 'path' -import { readJSON } from 'fs-extra' -describe('app-dir alias handling', () => { - if ((global as any).isNextDeploy) { - it('should skip next deploy for now', () => {}) - return - } - - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: new FileRef(path.join(__dirname, 'app-alias')), - dependencies: { - react: 'latest', - 'react-dom': 'latest', - typescript: 'latest', - '@types/react': 'latest', - '@types/node': 'latest', - }, - packageJson: { - type: 'module', - }, +createNextDescribe( + 'app-dir alias handling', + { + files: path.join(__dirname, 'app-alias'), + dependencies: { + react: 'latest', + 'react-dom': 'latest', + typescript: 'latest', + '@types/react': 'latest', + '@types/node': 'latest', + }, + packageJson: { + type: 'module', + }, + skipDeployment: true, + }, + ({ next, isNextDev }) => { + it('should handle typescript paths alias correctly', async () => { + const html = await next.render('/button') + expect(html).toContain('click') }) - }) - afterAll(() => next.destroy()) - it('should handle typescript paths alias correctly', async () => { - const html = await renderViaHTTP(next.url, '/button') - expect(html).toContain('click') - }) - - it('should resolve css imports from outside with src folder presented', async () => { - const browser = await webdriver(next.url, '/button') - const fontSize = await browser - .elementByCss('button') - .getComputedCss('font-size') - expect(fontSize).toBe('50px') - }) + it('should resolve css imports from outside with src folder presented', async () => { + const browser = await next.browser('/button') + const fontSize = await browser + .elementByCss('button') + .getComputedCss('font-size') + expect(fontSize).toBe('50px') + }) - if (!(global as any).isNextDev) { - it('should generate app-build-manifest correctly', async () => { - // Remove other page CSS files: - const manifest = await readJSON( - path.join(next.testDir, '.next', 'app-build-manifest.json') - ) + if (!isNextDev) { + it('should generate app-build-manifest correctly', async () => { + // Remove other page CSS files: + const manifest = await next.readJSON( + path.join('.next', 'app-build-manifest.json') + ) - expect(manifest.pages).not.toBeEmptyObject() - }) + expect(manifest.pages).not.toBeEmptyObject() + }) + } } -}) +) diff --git a/test/e2e/app-dir/app-edge-global.test.ts b/test/e2e/app-dir/app-edge-global.test.ts index 8b0b6c00c2d2251..9f0fbf66cbcfcc8 100644 --- a/test/e2e/app-dir/app-edge-global.test.ts +++ b/test/e2e/app-dir/app-edge-global.test.ts @@ -1,32 +1,23 @@ -import { createNext, FileRef } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' -import { renderViaHTTP } from 'next-test-utils' +import { createNextDescribe } from 'e2e-utils' import path from 'path' -describe('app-dir global edge configuration', () => { - if ((global as any).isNextDeploy) { - it('should skip next deploy for now', () => {}) - return - } - - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: new FileRef(path.join(__dirname, 'app-edge-global')), - dependencies: { - react: 'latest', - 'react-dom': 'latest', - typescript: 'latest', - '@types/react': 'latest', - '@types/node': 'latest', - }, +createNextDescribe( + 'app-dir global edge configuration', + { + files: path.join(__dirname, 'app-edge-global'), + dependencies: { + react: 'latest', + 'react-dom': 'latest', + typescript: 'latest', + '@types/react': 'latest', + '@types/node': 'latest', + }, + skipDeployment: true, + }, + ({ next }) => { + it('should handle edge only routes', async () => { + const html = await next.render('/app-edge') + expect(html).toContain('

Edge!

') }) - }) - afterAll(() => next.destroy()) - - it('should handle edge only routes', async () => { - const appHtml = await renderViaHTTP(next.url, '/app-edge') - expect(appHtml).toContain('

Edge!

') - }) -}) + } +) diff --git a/test/e2e/app-dir/app-edge.test.ts b/test/e2e/app-dir/app-edge.test.ts index f5d4dc2591c08e6..6c098b83aa4ba64 100644 --- a/test/e2e/app-dir/app-edge.test.ts +++ b/test/e2e/app-dir/app-edge.test.ts @@ -1,80 +1,72 @@ -import { createNext, FileRef } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' -import { check, renderViaHTTP } from 'next-test-utils' +import { createNextDescribe } from 'e2e-utils' +import { check } from 'next-test-utils' import path from 'path' -describe('app-dir edge SSR', () => { - if ((global as any).isNextDeploy) { - it('should skip next deploy for now', () => {}) - return - } - - let next: NextInstance +createNextDescribe( + 'app-dir edge SSR', + { + files: path.join(__dirname, 'app-edge'), + dependencies: { + react: 'latest', + 'react-dom': 'latest', + typescript: 'latest', + '@types/react': 'latest', + '@types/node': 'latest', + }, + skipDeployment: true, + }, + ({ next }) => { + it('should handle edge only routes', async () => { + const appHtml = await next.render('/app-edge') + expect(appHtml).toContain('

Edge!

') - beforeAll(async () => { - next = await createNext({ - files: new FileRef(path.join(__dirname, 'app-edge')), - dependencies: { - react: 'latest', - 'react-dom': 'latest', - typescript: 'latest', - '@types/react': 'latest', - '@types/node': 'latest', - }, + const pageHtml = await next.render('/pages-edge') + expect(pageHtml).toContain('

pages-edge-ssr

') }) - }) - afterAll(() => next.destroy()) - - it('should handle edge only routes', async () => { - const appHtml = await renderViaHTTP(next.url, '/app-edge') - expect(appHtml).toContain('

Edge!

') - const pageHtml = await renderViaHTTP(next.url, '/pages-edge') - expect(pageHtml).toContain('

pages-edge-ssr

') - }) - - if ((globalThis as any).isNextDev) { - it('should resolve module without error in edge runtime', async () => { - const logs = [] - next.on('stderr', (log) => { - logs.push(log) + if ((globalThis as any).isNextDev) { + it('should resolve module without error in edge runtime', async () => { + const logs = [] + next.on('stderr', (log) => { + logs.push(log) + }) + await next.render('app-edge') + expect( + logs.some((log) => log.includes(`Attempted import error:`)) + ).toBe(false) }) - await renderViaHTTP(next.url, 'app-edge') - expect(logs.some((log) => log.includes(`Attempted import error:`))).toBe( - false - ) - }) - it('should handle edge rsc hmr', async () => { - const pageFile = 'app/app-edge/page.tsx' - const content = await next.readFile(pageFile) + it('should handle edge rsc hmr', async () => { + const pageFile = 'app/app-edge/page.tsx' + const content = await next.readFile(pageFile) - // Update rendered content - const updatedContent = content.replace('Edge!', 'edge-hmr') - await next.patchFile(pageFile, updatedContent) - await check(async () => { - const html = await renderViaHTTP(next.url, '/app-edge') - return html - }, /edge-hmr/) + // Update rendered content + const updatedContent = content.replace('Edge!', 'edge-hmr') + await next.patchFile(pageFile, updatedContent) + await check(async () => { + const html = await next.render('/app-edge') + return html + }, /edge-hmr/) - // Revert - await next.patchFile(pageFile, content) - await check(async () => { - const html = await renderViaHTTP(next.url, '/app-edge') - return html - }, /Edge!/) - }) - } else { - // Production tests - it('should generate matchers correctly in middleware manifest', async () => { - const manifest = JSON.parse( - await next.readFile('.next/server/middleware-manifest.json') - ) - expect(manifest.functions['/(group)/group/page'].matchers).toEqual([ - { - regexp: '^/group$', - }, - ]) - }) + // Revert + await next.patchFile(pageFile, content) + await check(async () => { + const html = await next.render('/app-edge') + return html + }, /Edge!/) + }) + } else { + // Production tests + it('should generate matchers correctly in middleware manifest', async () => { + const manifest = JSON.parse( + await next.readFile('.next/server/middleware-manifest.json') + ) + expect(manifest.functions['/(group)/group/page'].matchers).toEqual([ + { + regexp: '^/group$', + }, + ]) + }) + } } -}) +) diff --git a/test/e2e/app-dir/app-external.test.ts b/test/e2e/app-dir/app-external.test.ts index a30ffc420ba08f0..f2b1300fbfb8ec5 100644 --- a/test/e2e/app-dir/app-external.test.ts +++ b/test/e2e/app-dir/app-external.test.ts @@ -1,8 +1,5 @@ import path from 'path' -import { renderViaHTTP, fetchViaHTTP } from 'next-test-utils' -import { createNext, FileRef } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' -import webdriver from 'next-webdriver' +import { createNextDescribe } from 'e2e-utils' async function resolveStreamResponse(response: any, onData?: any) { let result = '' @@ -18,183 +15,169 @@ async function resolveStreamResponse(response: any, onData?: any) { return result } -describe('app dir - external dependency', () => { - let next: NextInstance - - if ((global as any).isNextDeploy) { - it('should skip for deploy mode for now', () => {}) - return - } - - beforeAll(async () => { - next = await createNext({ - files: new FileRef(path.join(__dirname, './app-external')), - dependencies: { - '@next/font': 'canary', - react: 'latest', - 'react-dom': 'latest', - swr: '2.0.0-rc.0', +createNextDescribe( + 'app dir - external dependency', + { + files: path.join(__dirname, './app-external'), + dependencies: { + '@next/font': 'canary', + react: 'latest', + 'react-dom': 'latest', + swr: '2.0.0-rc.0', + }, + packageJson: { + scripts: { + setup: `cp -r ./node_modules_bak/* ./node_modules`, + build: 'yarn setup && next build', + dev: 'yarn setup && next dev', + start: 'next start', }, - packageJson: { - scripts: { - setup: `cp -r ./node_modules_bak/* ./node_modules`, - build: 'yarn setup && next build', - dev: 'yarn setup && next dev', - start: 'next start', - }, - }, - installCommand: 'yarn', - startCommand: (global as any).isNextDev ? 'yarn dev' : 'yarn start', - buildCommand: 'yarn build', - }) - }) - afterAll(() => next.destroy()) - - const { isNextDeploy } = global as any - const isReact17 = process.env.NEXT_TEST_REACT_VERSION === '^17' - if (isNextDeploy || isReact17) { - it('should skip tests for next-deploy and react 17', () => {}) - return - } - - it('should be able to opt-out 3rd party packages being bundled in server components', async () => { - await fetchViaHTTP(next.url, '/react-server/optout').then( - async (response) => { + }, + installCommand: 'yarn', + startCommand: (global as any).isNextDev ? 'yarn dev' : 'yarn start', + buildCommand: 'yarn build', + skipDeployment: true, + }, + ({ next }) => { + it('should be able to opt-out 3rd party packages being bundled in server components', async () => { + await next.fetch('/react-server/optout').then(async (response) => { const result = await resolveStreamResponse(response) expect(result).toContain('Server: index.default') expect(result).toContain('Server subpath: subpath.default') expect(result).toContain('Client: index.default') expect(result).toContain('Client subpath: subpath.default') + }) + }) + + it('should handle external async module libraries correctly', async () => { + const clientHtml = await next.render('/external-imports/client') + const serverHtml = await next.render('/external-imports/server') + const sharedHtml = await next.render('/shared-esm-dep') + + const browser = await next.browser('/external-imports/client') + const browserClientText = await browser.elementByCss('#content').text() + + function containClientContent(content) { + expect(content).toContain('module type:esm-export') + expect(content).toContain('export named:named') + expect(content).toContain('export value:123') + expect(content).toContain('export array:4,5,6') + expect(content).toContain('export object:{x:1}') + expect(content).toContain('swr-state') } - ) - }) - it('should handle external async module libraries correctly', async () => { - const clientHtml = await renderViaHTTP(next.url, '/external-imports/client') - const serverHtml = await renderViaHTTP(next.url, '/external-imports/server') - const sharedHtml = await renderViaHTTP(next.url, '/shared-esm-dep') - - const browser = await webdriver(next.url, '/external-imports/client') - const browserClientText = await browser.elementByCss('#content').text() - - function containClientContent(content) { - expect(content).toContain('module type:esm-export') - expect(content).toContain('export named:named') - expect(content).toContain('export value:123') - expect(content).toContain('export array:4,5,6') - expect(content).toContain('export object:{x:1}') - expect(content).toContain('swr-state') - } - - containClientContent(clientHtml) - containClientContent(browserClientText) - - // support esm module imports on server side, and indirect imports from shared components - expect(serverHtml).toContain('pure-esm-module') - expect(sharedHtml).toContain( - 'node_modules instance from client module pure-esm-module' - ) - }) + containClientContent(clientHtml) + containClientContent(browserClientText) - it('should transpile specific external packages with the `transpilePackages` option', async () => { - const clientHtml = await renderViaHTTP(next.url, '/external-imports/client') - expect(clientHtml).toContain('transpilePackages:5') - }) + // support esm module imports on server side, and indirect imports from shared components + expect(serverHtml).toContain('pure-esm-module') + expect(sharedHtml).toContain( + 'node_modules instance from client module pure-esm-module' + ) + }) - it('should resolve the subset react in server components based on the react-server condition', async () => { - await fetchViaHTTP(next.url, '/react-server').then(async (response) => { - const result = await resolveStreamResponse(response) - expect(result).toContain('Server: subset') - expect(result).toContain('Client: full') + it('should transpile specific external packages with the `transpilePackages` option', async () => { + const clientHtml = await next.render('/external-imports/client') + expect(clientHtml).toContain('transpilePackages:5') }) - }) - it('should resolve 3rd party package exports based on the react-server condition', async () => { - await fetchViaHTTP(next.url, '/react-server/3rd-party-package').then( - async (response) => { + it('should resolve the subset react in server components based on the react-server condition', async () => { + await next.fetch('/react-server').then(async (response) => { const result = await resolveStreamResponse(response) + expect(result).toContain('Server: subset') + expect(result).toContain('Client: full') + }) + }) - // Package should be resolved based on the react-server condition, - // as well as package's internal & external dependencies. - expect(result).toContain( - 'Server: index.react-server:react.subset:dep.server' - ) - expect(result).toContain('Client: index.default:react.full:dep.default') - - // Subpath exports should be resolved based on the condition too. - expect(result).toContain('Server subpath: subpath.react-server') - expect(result).toContain('Client subpath: subpath.default') - } - ) - }) + it('should resolve 3rd party package exports based on the react-server condition', async () => { + await next + .fetch('/react-server/3rd-party-package') + .then(async (response) => { + const result = await resolveStreamResponse(response) + + // Package should be resolved based on the react-server condition, + // as well as package's internal & external dependencies. + expect(result).toContain( + 'Server: index.react-server:react.subset:dep.server' + ) + expect(result).toContain( + 'Client: index.default:react.full:dep.default' + ) + + // Subpath exports should be resolved based on the condition too. + expect(result).toContain('Server subpath: subpath.react-server') + expect(result).toContain('Client subpath: subpath.default') + }) + }) - it('should correctly collect global css imports and mark them as side effects', async () => { - await fetchViaHTTP(next.url, '/css/a').then(async (response) => { - const result = await resolveStreamResponse(response) + it('should correctly collect global css imports and mark them as side effects', async () => { + await next.fetch('/css/a').then(async (response) => { + const result = await resolveStreamResponse(response) - // It should include the global CSS import - expect(result).toMatch(/\.css/) + // It should include the global CSS import + expect(result).toMatch(/\.css/) + }) }) - }) - it('should handle external css modules', async () => { - const browser = await webdriver(next.url, '/css/modules') + it('should handle external css modules', async () => { + const browser = await next.browser('/css/modules') - expect( - await browser.eval( - `window.getComputedStyle(document.querySelector('h1')).color` - ) - ).toBe('rgb(255, 0, 0)') - }) + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('h1')).color` + ) + ).toBe('rgb(255, 0, 0)') + }) - it('should use the same export type for packages in both ssr and client', async () => { - const browser = await webdriver(next.url, '/client-dep') - expect(await browser.eval(`window.document.body.innerText`)).toBe('hello') - }) + it('should use the same export type for packages in both ssr and client', async () => { + const browser = await next.browser('/client-dep') + expect(await browser.eval(`window.document.body.innerText`)).toBe('hello') + }) - it('should handle external css modules in pages', async () => { - const browser = await webdriver(next.url, '/test-pages') + it('should handle external css modules in pages', async () => { + const browser = await next.browser('/test-pages') - expect( - await browser.eval( - `window.getComputedStyle(document.querySelector('h1')).color` - ) - ).toBe('rgb(255, 0, 0)') - }) + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('h1')).color` + ) + ).toBe('rgb(255, 0, 0)') + }) - it('should handle external @next/font', async () => { - const browser = await webdriver(next.url, '/font') + it('should handle external @next/font', async () => { + const browser = await next.browser('/font') - expect( - await browser.eval( - `window.getComputedStyle(document.querySelector('p')).fontFamily` - ) - ).toMatch(/^__myFont_.{6}, __myFont_Fallback_.{6}$/) - }) + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('p')).fontFamily` + ) + ).toMatch(/^__myFont_.{6}, __myFont_Fallback_.{6}$/) + }) - describe('react in external esm packages', () => { - it('should use the same react in client app', async () => { - const html = await renderViaHTTP(next.url, '/esm/client') + describe('react in external esm packages', () => { + it('should use the same react in client app', async () => { + const html = await next.render('/esm/client') - const v1 = html.match(/App React Version: ([^<]+) { - const html = await renderViaHTTP(next.url, '/esm/server') + it('should use the same react in server app', async () => { + const html = await next.render('/esm/server') - const v1 = html.match(/App React Version: ([^<]+) { - const html = await renderViaHTTP(next.url, '/test-pages-esm') + it('should use the same react in pages', async () => { + const html = await next.render('/test-pages-esm') - const v1 = html.match(/App React Version: ([^<]+) { - if ((global as any).isNextDeploy) { - it('should skip next deploy for now', () => {}) - return - } - - let next: NextInstance - - afterAll(() => next.destroy()) - beforeAll(async () => { - next = await createNext({ - files: new FileRef(path.join(__dirname, 'app-middleware')), - }) - }) - - describe.each([ - { - title: 'Serverless Functions', - path: '/api/dump-headers-serverless', - toJson: (res: Response) => res.json(), - }, - { - title: 'Edge Functions', - path: '/api/dump-headers-edge', - toJson: (res: Response) => res.json(), - }, - { - title: 'next/headers', - path: '/headers', - toJson: async (res: Response) => { - const $ = cheerio.load(await res.text()) - return JSON.parse($('#headers').text()) +createNextDescribe( + 'app-dir with middleware', + { + files: path.join(__dirname, 'app-middleware'), + skipDeployment: true, + }, + ({ next }) => { + describe.each([ + { + title: 'Serverless Functions', + path: '/api/dump-headers-serverless', + toJson: (res: Response) => res.json(), }, - }, - ])('Mutate request headers for $title', ({ path, toJson }) => { - it(`Adds new headers`, async () => { - const res = await fetchViaHTTP(next.url, path, null, { - headers: { - 'x-from-client': 'hello-from-client', - }, - }) - expect(await toJson(res)).toMatchObject({ - 'x-from-client': 'hello-from-client', - 'x-from-middleware': 'hello-from-middleware', - }) - }) - - it(`Deletes headers`, async () => { - const res = await fetchViaHTTP( - next.url, - path, - { - 'remove-headers': 'x-from-client1,x-from-client2', + { + title: 'Edge Functions', + path: '/api/dump-headers-edge', + toJson: (res: Response) => res.json(), + }, + { + title: 'next/headers', + path: '/headers', + toJson: async (res: Response) => { + const $ = cheerio.load(await res.text()) + return JSON.parse($('#headers').text()) }, - { + }, + ])('Mutate request headers for $title', ({ path, toJson }) => { + it(`Adds new headers`, async () => { + const res = await next.fetch(path, null, { headers: { - 'x-from-client1': 'hello-from-client', - 'X-From-Client2': 'hello-from-client', + 'x-from-client': 'hello-from-client', }, - } - ) - - const json = await toJson(res) - expect(json).not.toHaveProperty('x-from-client1') - expect(json).not.toHaveProperty('X-From-Client2') - expect(json).toMatchObject({ - 'x-from-middleware': 'hello-from-middleware', + }) + expect(await toJson(res)).toMatchObject({ + 'x-from-client': 'hello-from-client', + 'x-from-middleware': 'hello-from-middleware', + }) }) - // Should not be included in response headers. - expect(res.headers.get('x-middleware-override-headers')).toBeNull() - expect( - res.headers.get('x-middleware-request-x-from-middleware') - ).toBeNull() - expect(res.headers.get('x-middleware-request-x-from-client1')).toBeNull() - expect(res.headers.get('x-middleware-request-x-from-client2')).toBeNull() - }) - - it(`Updates headers`, async () => { - const res = await fetchViaHTTP( - next.url, - path, - { - 'update-headers': - 'x-from-client1=new-value1,x-from-client2=new-value2', - }, - { - headers: { - 'x-from-client1': 'old-value1', - 'X-From-Client2': 'old-value2', - 'x-from-client3': 'old-value3', + it(`Deletes headers`, async () => { + const res = await next.fetch( + path, + { + 'remove-headers': 'x-from-client1,x-from-client2', }, - } - ) - expect(await toJson(res)).toMatchObject({ - 'x-from-client1': 'new-value1', - 'x-from-client2': 'new-value2', - 'x-from-client3': 'old-value3', - 'x-from-middleware': 'hello-from-middleware', + { + headers: { + 'x-from-client1': 'hello-from-client', + 'X-From-Client2': 'hello-from-client', + }, + } + ) + + const json = await toJson(res) + expect(json).not.toHaveProperty('x-from-client1') + expect(json).not.toHaveProperty('X-From-Client2') + expect(json).toMatchObject({ + 'x-from-middleware': 'hello-from-middleware', + }) + + // Should not be included in response headers. + expect(res.headers.get('x-middleware-override-headers')).toBeNull() + expect( + res.headers.get('x-middleware-request-x-from-middleware') + ).toBeNull() + expect( + res.headers.get('x-middleware-request-x-from-client1') + ).toBeNull() + expect( + res.headers.get('x-middleware-request-x-from-client2') + ).toBeNull() }) - // Should not be included in response headers. - expect(res.headers.get('x-middleware-override-headers')).toBeNull() - expect( - res.headers.get('x-middleware-request-x-from-middleware') - ).toBeNull() - expect(res.headers.get('x-middleware-request-x-from-client1')).toBeNull() - expect(res.headers.get('x-middleware-request-x-from-client2')).toBeNull() - expect(res.headers.get('x-middleware-request-x-from-client3')).toBeNull() + it(`Updates headers`, async () => { + const res = await next.fetch( + path, + { + 'update-headers': + 'x-from-client1=new-value1,x-from-client2=new-value2', + }, + { + headers: { + 'x-from-client1': 'old-value1', + 'X-From-Client2': 'old-value2', + 'x-from-client3': 'old-value3', + }, + } + ) + expect(await toJson(res)).toMatchObject({ + 'x-from-client1': 'new-value1', + 'x-from-client2': 'new-value2', + 'x-from-client3': 'old-value3', + 'x-from-middleware': 'hello-from-middleware', + }) + + // Should not be included in response headers. + expect(res.headers.get('x-middleware-override-headers')).toBeNull() + expect( + res.headers.get('x-middleware-request-x-from-middleware') + ).toBeNull() + expect( + res.headers.get('x-middleware-request-x-from-client1') + ).toBeNull() + expect( + res.headers.get('x-middleware-request-x-from-client2') + ).toBeNull() + expect( + res.headers.get('x-middleware-request-x-from-client3') + ).toBeNull() + }) }) - }) -}) - -describe('app dir middleware without pages dir', () => { - if ((global as any).isNextDeploy) { - it('should skip next deploy for now', () => {}) - return } - - let next: NextInstance - - afterAll(() => next.destroy()) - beforeAll(async () => { - next = await createNext({ - files: { - app: new FileRef(path.join(__dirname, 'app-middleware/app')), - 'next.config.js': new FileRef( - path.join(__dirname, 'app-middleware/next.config.js') - ), - 'middleware.js': ` - import { NextResponse } from 'next/server' - - export async function middleware(request) { - return new NextResponse('redirected') - } - - export const config = { - matcher: '/headers' - } - `, - }, +) + +createNextDescribe( + 'app dir middleware without pages dir', + { + files: { + app: new FileRef(path.join(__dirname, 'app-middleware/app')), + 'next.config.js': new FileRef( + path.join(__dirname, 'app-middleware/next.config.js') + ), + 'middleware.js': ` + import { NextResponse } from 'next/server' + + export async function middleware(request) { + return new NextResponse('redirected') + } + + export const config = { + matcher: '/headers' + } + `, + }, + skipDeployment: true, + }, + ({ next }) => { + // eslint-disable-next-line jest/no-identical-title + it('Updates headers', async () => { + const html = await next.render('/headers') + + expect(html).toContain('redirected') }) - }) - - it(`Updates headers`, async () => { - const html = await renderViaHTTP(next.url, '/headers') - - expect(html).toContain('redirected') - }) -}) + } +) diff --git a/test/e2e/app-dir/app-static.test.ts b/test/e2e/app-dir/app-static.test.ts index bca51dbeda6c9e8..b036c28ea7bb01f 100644 --- a/test/e2e/app-dir/app-static.test.ts +++ b/test/e2e/app-dir/app-static.test.ts @@ -2,712 +2,697 @@ import globOrig from 'glob' import cheerio from 'cheerio' import { promisify } from 'util' import path, { join } from 'path' -import { createNext, FileRef } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' -import { check, fetchViaHTTP, normalizeRegEx, waitFor } from 'next-test-utils' -import webdriver from 'next-webdriver' +import { createNextDescribe } from 'e2e-utils' +import { check, normalizeRegEx, waitFor } from 'next-test-utils' const glob = promisify(globOrig) -describe('app-dir static/dynamic handling', () => { - const isDev = (global as any).isNextDev +createNextDescribe( + 'app-dir static/dynamic handling', + { + files: path.join(__dirname, 'app-static'), + }, + ({ next, isNextDev: isDev, isNextStart }) => { + if (isNextStart) { + it('should output HTML/RSC files for static paths', async () => { + const files = ( + await glob('**/*', { + cwd: join(next.testDir, '.next/server/app'), + }) + ).filter((file) => file.match(/.*\.(js|html|rsc)$/)) + + expect(files).toEqual([ + '(new)/custom/page.js', + 'blog/[author]/[slug]/page.js', + 'blog/[author]/page.js', + 'blog/seb.html', + 'blog/seb.rsc', + 'blog/seb/second-post.html', + 'blog/seb/second-post.rsc', + 'blog/styfle.html', + 'blog/styfle.rsc', + 'blog/styfle/first-post.html', + 'blog/styfle/first-post.rsc', + 'blog/styfle/second-post.html', + 'blog/styfle/second-post.rsc', + 'blog/tim.html', + 'blog/tim.rsc', + 'blog/tim/first-post.html', + 'blog/tim/first-post.rsc', + 'dynamic-error.html', + 'dynamic-error.rsc', + 'dynamic-error/page.js', + 'dynamic-no-gen-params-ssr/[slug]/page.js', + 'dynamic-no-gen-params/[slug]/page.js', + 'force-static/[slug]/page.js', + 'force-static/first.html', + 'force-static/first.rsc', + 'force-static/page.js', + 'force-static/second.html', + 'force-static/second.rsc', + 'hooks/use-pathname/[slug]/page.js', + 'hooks/use-pathname/slug.html', + 'hooks/use-pathname/slug.rsc', + 'hooks/use-search-params.html', + 'hooks/use-search-params.rsc', + 'hooks/use-search-params/force-static.html', + 'hooks/use-search-params/force-static.rsc', + 'hooks/use-search-params/force-static/page.js', + 'hooks/use-search-params/page.js', + 'hooks/use-search-params/with-suspense.html', + 'hooks/use-search-params/with-suspense.rsc', + 'hooks/use-search-params/with-suspense/page.js', + 'ssg-preview.html', + 'ssg-preview.rsc', + 'ssg-preview/[[...route]]/page.js', + 'ssg-preview/test-2.html', + 'ssg-preview/test-2.rsc', + 'ssg-preview/test.html', + 'ssg-preview/test.rsc', + 'ssr-auto/cache-no-store/page.js', + 'ssr-auto/fetch-revalidate-zero/page.js', + 'ssr-forced/page.js', + 'variable-revalidate/no-store/page.js', + 'variable-revalidate/revalidate-3.html', + 'variable-revalidate/revalidate-3.rsc', + 'variable-revalidate/revalidate-3/page.js', + ]) + }) - let next: NextInstance + it('should have correct prerender-manifest entries', async () => { + const manifest = JSON.parse( + await next.readFile('.next/prerender-manifest.json') + ) - beforeAll(async () => { - next = await createNext({ - files: new FileRef(path.join(__dirname, 'app-static')), - dependencies: { - react: 'latest', - 'react-dom': 'latest', - }, - }) - }) - afterAll(() => next.destroy()) - - if ((global as any).isNextStart) { - it('should output HTML/RSC files for static paths', async () => { - const files = ( - await glob('**/*', { - cwd: join(next.testDir, '.next/server/app'), + Object.keys(manifest.dynamicRoutes).forEach((key) => { + const item = manifest.dynamicRoutes[key] + + if (item.dataRouteRegex) { + item.dataRouteRegex = normalizeRegEx(item.dataRouteRegex) + } + if (item.routeRegex) { + item.routeRegex = normalizeRegEx(item.routeRegex) + } }) - ).filter((file) => file.match(/.*\.(js|html|rsc)$/)) - - expect(files).toEqual([ - '(new)/custom/page.js', - 'blog/[author]/[slug]/page.js', - 'blog/[author]/page.js', - 'blog/seb.html', - 'blog/seb.rsc', - 'blog/seb/second-post.html', - 'blog/seb/second-post.rsc', - 'blog/styfle.html', - 'blog/styfle.rsc', - 'blog/styfle/first-post.html', - 'blog/styfle/first-post.rsc', - 'blog/styfle/second-post.html', - 'blog/styfle/second-post.rsc', - 'blog/tim.html', - 'blog/tim.rsc', - 'blog/tim/first-post.html', - 'blog/tim/first-post.rsc', - 'dynamic-error.html', - 'dynamic-error.rsc', - 'dynamic-error/page.js', - 'dynamic-no-gen-params-ssr/[slug]/page.js', - 'dynamic-no-gen-params/[slug]/page.js', - 'force-static/[slug]/page.js', - 'force-static/first.html', - 'force-static/first.rsc', - 'force-static/page.js', - 'force-static/second.html', - 'force-static/second.rsc', - 'hooks/use-pathname/[slug]/page.js', - 'hooks/use-pathname/slug.html', - 'hooks/use-pathname/slug.rsc', - 'hooks/use-search-params.html', - 'hooks/use-search-params.rsc', - 'hooks/use-search-params/force-static.html', - 'hooks/use-search-params/force-static.rsc', - 'hooks/use-search-params/force-static/page.js', - 'hooks/use-search-params/page.js', - 'hooks/use-search-params/with-suspense.html', - 'hooks/use-search-params/with-suspense.rsc', - 'hooks/use-search-params/with-suspense/page.js', - 'ssg-preview.html', - 'ssg-preview.rsc', - 'ssg-preview/[[...route]]/page.js', - 'ssg-preview/test-2.html', - 'ssg-preview/test-2.rsc', - 'ssg-preview/test.html', - 'ssg-preview/test.rsc', - 'ssr-auto/cache-no-store/page.js', - 'ssr-auto/fetch-revalidate-zero/page.js', - 'ssr-forced/page.js', - 'variable-revalidate/no-store/page.js', - 'variable-revalidate/revalidate-3.html', - 'variable-revalidate/revalidate-3.rsc', - 'variable-revalidate/revalidate-3/page.js', - ]) - }) - it('should have correct prerender-manifest entries', async () => { - const manifest = JSON.parse( - await next.readFile('.next/prerender-manifest.json') - ) + expect(manifest.version).toBe(3) + expect(manifest.routes).toEqual({ + '/blog/tim': { + initialRevalidateSeconds: 10, + srcRoute: '/blog/[author]', + dataRoute: '/blog/tim.rsc', + }, + '/blog/seb': { + initialRevalidateSeconds: 10, + srcRoute: '/blog/[author]', + dataRoute: '/blog/seb.rsc', + }, + '/blog/styfle': { + initialRevalidateSeconds: 10, + srcRoute: '/blog/[author]', + dataRoute: '/blog/styfle.rsc', + }, + '/blog/tim/first-post': { + initialRevalidateSeconds: false, + srcRoute: '/blog/[author]/[slug]', + dataRoute: '/blog/tim/first-post.rsc', + }, + '/dynamic-error': { + dataRoute: '/dynamic-error.rsc', + initialRevalidateSeconds: false, + srcRoute: '/dynamic-error', + }, + '/blog/seb/second-post': { + initialRevalidateSeconds: false, + srcRoute: '/blog/[author]/[slug]', + dataRoute: '/blog/seb/second-post.rsc', + }, + '/blog/styfle/first-post': { + initialRevalidateSeconds: false, + srcRoute: '/blog/[author]/[slug]', + dataRoute: '/blog/styfle/first-post.rsc', + }, + '/blog/styfle/second-post': { + initialRevalidateSeconds: false, + srcRoute: '/blog/[author]/[slug]', + dataRoute: '/blog/styfle/second-post.rsc', + }, + '/hooks/use-pathname/slug': { + dataRoute: '/hooks/use-pathname/slug.rsc', + initialRevalidateSeconds: false, + srcRoute: '/hooks/use-pathname/[slug]', + }, + '/hooks/use-search-params': { + dataRoute: '/hooks/use-search-params.rsc', + initialRevalidateSeconds: false, + srcRoute: '/hooks/use-search-params', + }, + '/hooks/use-search-params/force-static': { + dataRoute: '/hooks/use-search-params/force-static.rsc', + initialRevalidateSeconds: false, + srcRoute: '/hooks/use-search-params/force-static', + }, + '/hooks/use-search-params/with-suspense': { + dataRoute: '/hooks/use-search-params/with-suspense.rsc', + initialRevalidateSeconds: false, + srcRoute: '/hooks/use-search-params/with-suspense', + }, + '/force-static/first': { + dataRoute: '/force-static/first.rsc', + initialRevalidateSeconds: false, + srcRoute: '/force-static/[slug]', + }, + '/force-static/second': { + dataRoute: '/force-static/second.rsc', + initialRevalidateSeconds: false, + srcRoute: '/force-static/[slug]', + }, + '/ssg-preview': { + dataRoute: '/ssg-preview.rsc', + initialRevalidateSeconds: false, + srcRoute: '/ssg-preview/[[...route]]', + }, + '/ssg-preview/test': { + dataRoute: '/ssg-preview/test.rsc', + initialRevalidateSeconds: false, + srcRoute: '/ssg-preview/[[...route]]', + }, + '/ssg-preview/test-2': { + dataRoute: '/ssg-preview/test-2.rsc', + initialRevalidateSeconds: false, + srcRoute: '/ssg-preview/[[...route]]', + }, + '/variable-revalidate/revalidate-3': { + dataRoute: '/variable-revalidate/revalidate-3.rsc', + initialRevalidateSeconds: 3, + srcRoute: '/variable-revalidate/revalidate-3', + }, + }) + expect(manifest.dynamicRoutes).toEqual({ + '/blog/[author]/[slug]': { + routeRegex: normalizeRegEx('^/blog/([^/]+?)/([^/]+?)(?:/)?$'), + dataRoute: '/blog/[author]/[slug].rsc', + fallback: null, + dataRouteRegex: normalizeRegEx('^/blog/([^/]+?)/([^/]+?)\\.rsc$'), + }, + '/blog/[author]': { + dataRoute: '/blog/[author].rsc', + dataRouteRegex: normalizeRegEx('^\\/blog\\/([^\\/]+?)\\.rsc$'), + fallback: false, + routeRegex: normalizeRegEx('^\\/blog\\/([^\\/]+?)(?:\\/)?$'), + }, + '/hooks/use-pathname/[slug]': { + dataRoute: '/hooks/use-pathname/[slug].rsc', + dataRouteRegex: '^\\/hooks\\/use\\-pathname\\/([^\\/]+?)\\.rsc$', + fallback: null, + routeRegex: '^\\/hooks\\/use\\-pathname\\/([^\\/]+?)(?:\\/)?$', + }, + '/force-static/[slug]': { + dataRoute: '/force-static/[slug].rsc', + dataRouteRegex: '^\\/force\\-static\\/([^\\/]+?)\\.rsc$', + fallback: null, + routeRegex: '^\\/force\\-static\\/([^\\/]+?)(?:\\/)?$', + }, + '/ssg-preview/[[...route]]': { + dataRoute: '/ssg-preview/[[...route]].rsc', + dataRouteRegex: '^\\/ssg\\-preview(?:\\/(.+?))?\\.rsc$', + fallback: null, + routeRegex: '^\\/ssg\\-preview(?:\\/(.+?))?(?:\\/)?$', + }, + }) + }) + } - Object.keys(manifest.dynamicRoutes).forEach((key) => { - const item = manifest.dynamicRoutes[key] + it('Should not throw Dynamic Server Usage error when using generateStaticParams with previewData', async () => { + const browserOnIndexPage = await next.browser('/ssg-preview') - if (item.dataRouteRegex) { - item.dataRouteRegex = normalizeRegEx(item.dataRouteRegex) - } - if (item.routeRegex) { - item.routeRegex = normalizeRegEx(item.routeRegex) - } - }) + const content = await browserOnIndexPage + .elementByCss('#preview-data') + .text() - expect(manifest.version).toBe(3) - expect(manifest.routes).toEqual({ - '/blog/tim': { - initialRevalidateSeconds: 10, - srcRoute: '/blog/[author]', - dataRoute: '/blog/tim.rsc', - }, - '/blog/seb': { - initialRevalidateSeconds: 10, - srcRoute: '/blog/[author]', - dataRoute: '/blog/seb.rsc', - }, - '/blog/styfle': { - initialRevalidateSeconds: 10, - srcRoute: '/blog/[author]', - dataRoute: '/blog/styfle.rsc', - }, - '/blog/tim/first-post': { - initialRevalidateSeconds: false, - srcRoute: '/blog/[author]/[slug]', - dataRoute: '/blog/tim/first-post.rsc', - }, - '/dynamic-error': { - dataRoute: '/dynamic-error.rsc', - initialRevalidateSeconds: false, - srcRoute: '/dynamic-error', - }, - '/blog/seb/second-post': { - initialRevalidateSeconds: false, - srcRoute: '/blog/[author]/[slug]', - dataRoute: '/blog/seb/second-post.rsc', - }, - '/blog/styfle/first-post': { - initialRevalidateSeconds: false, - srcRoute: '/blog/[author]/[slug]', - dataRoute: '/blog/styfle/first-post.rsc', - }, - '/blog/styfle/second-post': { - initialRevalidateSeconds: false, - srcRoute: '/blog/[author]/[slug]', - dataRoute: '/blog/styfle/second-post.rsc', - }, - '/hooks/use-pathname/slug': { - dataRoute: '/hooks/use-pathname/slug.rsc', - initialRevalidateSeconds: false, - srcRoute: '/hooks/use-pathname/[slug]', - }, - '/hooks/use-search-params': { - dataRoute: '/hooks/use-search-params.rsc', - initialRevalidateSeconds: false, - srcRoute: '/hooks/use-search-params', - }, - '/hooks/use-search-params/force-static': { - dataRoute: '/hooks/use-search-params/force-static.rsc', - initialRevalidateSeconds: false, - srcRoute: '/hooks/use-search-params/force-static', - }, - '/hooks/use-search-params/with-suspense': { - dataRoute: '/hooks/use-search-params/with-suspense.rsc', - initialRevalidateSeconds: false, - srcRoute: '/hooks/use-search-params/with-suspense', - }, - '/force-static/first': { - dataRoute: '/force-static/first.rsc', - initialRevalidateSeconds: false, - srcRoute: '/force-static/[slug]', - }, - '/force-static/second': { - dataRoute: '/force-static/second.rsc', - initialRevalidateSeconds: false, - srcRoute: '/force-static/[slug]', - }, - '/ssg-preview': { - dataRoute: '/ssg-preview.rsc', - initialRevalidateSeconds: false, - srcRoute: '/ssg-preview/[[...route]]', - }, - '/ssg-preview/test': { - dataRoute: '/ssg-preview/test.rsc', - initialRevalidateSeconds: false, - srcRoute: '/ssg-preview/[[...route]]', - }, - '/ssg-preview/test-2': { - dataRoute: '/ssg-preview/test-2.rsc', - initialRevalidateSeconds: false, - srcRoute: '/ssg-preview/[[...route]]', - }, - '/variable-revalidate/revalidate-3': { - dataRoute: '/variable-revalidate/revalidate-3.rsc', - initialRevalidateSeconds: 3, - srcRoute: '/variable-revalidate/revalidate-3', - }, - }) - expect(manifest.dynamicRoutes).toEqual({ - '/blog/[author]/[slug]': { - routeRegex: normalizeRegEx('^/blog/([^/]+?)/([^/]+?)(?:/)?$'), - dataRoute: '/blog/[author]/[slug].rsc', - fallback: null, - dataRouteRegex: normalizeRegEx('^/blog/([^/]+?)/([^/]+?)\\.rsc$'), - }, - '/blog/[author]': { - dataRoute: '/blog/[author].rsc', - dataRouteRegex: normalizeRegEx('^\\/blog\\/([^\\/]+?)\\.rsc$'), - fallback: false, - routeRegex: normalizeRegEx('^\\/blog\\/([^\\/]+?)(?:\\/)?$'), - }, - '/hooks/use-pathname/[slug]': { - dataRoute: '/hooks/use-pathname/[slug].rsc', - dataRouteRegex: '^\\/hooks\\/use\\-pathname\\/([^\\/]+?)\\.rsc$', - fallback: null, - routeRegex: '^\\/hooks\\/use\\-pathname\\/([^\\/]+?)(?:\\/)?$', - }, - '/force-static/[slug]': { - dataRoute: '/force-static/[slug].rsc', - dataRouteRegex: '^\\/force\\-static\\/([^\\/]+?)\\.rsc$', - fallback: null, - routeRegex: '^\\/force\\-static\\/([^\\/]+?)(?:\\/)?$', - }, - '/ssg-preview/[[...route]]': { - dataRoute: '/ssg-preview/[[...route]].rsc', - dataRouteRegex: '^\\/ssg\\-preview(?:\\/(.+?))?\\.rsc$', - fallback: null, - routeRegex: '^\\/ssg\\-preview(?:\\/(.+?))?(?:\\/)?$', + expect(content).toContain('previewData') + }) + + it('should force SSR correctly for headers usage', async () => { + const res = await next.fetch('/force-static', undefined, { + headers: { + Cookie: 'myCookie=cookieValue', + another: 'header', }, }) - }) - } + expect(res.status).toBe(200) - it('Should not throw Dynamic Server Usage error when using generateStaticParams with previewData', async () => { - const browserOnIndexPage = await webdriver(next.url, '/ssg-preview') + const html = await res.text() + const $ = cheerio.load(html) - const content = await browserOnIndexPage - .elementByCss('#preview-data') - .text() + expect(JSON.parse($('#headers').text())).toIncludeAllMembers([ + 'cookie', + 'another', + ]) + expect(JSON.parse($('#cookies').text())).toEqual([ + { + name: 'myCookie', + value: 'cookieValue', + }, + ]) - expect(content).toContain('previewData') - }) + const firstTime = $('#now').text() - it('should force SSR correctly for headers usage', async () => { - const res = await fetchViaHTTP(next.url, '/force-static', undefined, { - headers: { - Cookie: 'myCookie=cookieValue', - another: 'header', - }, - }) - expect(res.status).toBe(200) - - const html = await res.text() - const $ = cheerio.load(html) - - expect(JSON.parse($('#headers').text())).toIncludeAllMembers([ - 'cookie', - 'another', - ]) - expect(JSON.parse($('#cookies').text())).toEqual([ - { - name: 'myCookie', - value: 'cookieValue', - }, - ]) - - const firstTime = $('#now').text() - - if (!(global as any).isNextDev) { - const res2 = await fetchViaHTTP(next.url, '/force-static') - expect(res2.status).toBe(200) - - const $2 = cheerio.load(await res2.text()) - expect(firstTime).not.toBe($2('#now').text()) - } - }) + if (!(global as any).isNextDev) { + const res2 = await next.fetch('/force-static') + expect(res2.status).toBe(200) - it('should honor dynamic = "force-static" correctly', async () => { - const res = await fetchViaHTTP(next.url, '/force-static/first') - expect(res.status).toBe(200) + const $2 = cheerio.load(await res2.text()) + expect(firstTime).not.toBe($2('#now').text()) + } + }) - const html = await res.text() - const $ = cheerio.load(html) + it('should honor dynamic = "force-static" correctly', async () => { + const res = await next.fetch('/force-static/first') + expect(res.status).toBe(200) - expect(JSON.parse($('#params').text())).toEqual({ slug: 'first' }) - expect(JSON.parse($('#headers').text())).toEqual([]) - expect(JSON.parse($('#cookies').text())).toEqual([]) + const html = await res.text() + const $ = cheerio.load(html) - const firstTime = $('#now').text() + expect(JSON.parse($('#params').text())).toEqual({ slug: 'first' }) + expect(JSON.parse($('#headers').text())).toEqual([]) + expect(JSON.parse($('#cookies').text())).toEqual([]) - if (!(global as any).isNextDev) { - const res2 = await fetchViaHTTP(next.url, '/force-static/first') - expect(res2.status).toBe(200) + const firstTime = $('#now').text() - const $2 = cheerio.load(await res2.text()) - expect(firstTime).toBe($2('#now').text()) - } - }) + if (!(global as any).isNextDev) { + const res2 = await next.fetch('/force-static/first') + expect(res2.status).toBe(200) - it('should honor dynamic = "force-static" correctly (lazy)', async () => { - const res = await fetchViaHTTP(next.url, '/force-static/random') - expect(res.status).toBe(200) + const $2 = cheerio.load(await res2.text()) + expect(firstTime).toBe($2('#now').text()) + } + }) - const html = await res.text() - const $ = cheerio.load(html) + it('should honor dynamic = "force-static" correctly (lazy)', async () => { + const res = await next.fetch('/force-static/random') + expect(res.status).toBe(200) - expect(JSON.parse($('#params').text())).toEqual({ slug: 'random' }) - expect(JSON.parse($('#headers').text())).toEqual([]) - expect(JSON.parse($('#cookies').text())).toEqual([]) + const html = await res.text() + const $ = cheerio.load(html) - const firstTime = $('#now').text() + expect(JSON.parse($('#params').text())).toEqual({ slug: 'random' }) + expect(JSON.parse($('#headers').text())).toEqual([]) + expect(JSON.parse($('#cookies').text())).toEqual([]) - if (!(global as any).isNextDev) { - const res2 = await fetchViaHTTP(next.url, '/force-static/random') - expect(res2.status).toBe(200) + const firstTime = $('#now').text() - const $2 = cheerio.load(await res2.text()) - expect(firstTime).toBe($2('#now').text()) - } - }) + if (!(global as any).isNextDev) { + const res2 = await next.fetch('/force-static/random') + expect(res2.status).toBe(200) - it('should handle dynamicParams: false correctly', async () => { - const validParams = ['tim', 'seb', 'styfle'] + const $2 = cheerio.load(await res2.text()) + expect(firstTime).toBe($2('#now').text()) + } + }) - for (const param of validParams) { - const res = await fetchViaHTTP(next.url, `/blog/${param}`, undefined, { - redirect: 'manual', - }) - expect(res.status).toBe(200) - const html = await res.text() - const $ = cheerio.load(html) + it('should handle dynamicParams: false correctly', async () => { + const validParams = ['tim', 'seb', 'styfle'] - expect(JSON.parse($('#params').text())).toEqual({ - author: param, - }) - expect($('#page').text()).toBe('/blog/[author]') - } - const invalidParams = ['timm', 'non-existent'] - - for (const param of invalidParams) { - const invalidRes = await fetchViaHTTP( - next.url, - `/blog/${param}`, - undefined, - { redirect: 'manual' } - ) - expect(invalidRes.status).toBe(404) - expect(await invalidRes.text()).toContain('page could not be found') - } - }) - - it('should work with forced dynamic path', async () => { - for (const slug of ['first', 'second']) { - const res = await fetchViaHTTP( - next.url, - `/dynamic-no-gen-params-ssr/${slug}`, - undefined, - { redirect: 'manual' } - ) - expect(res.status).toBe(200) - expect(await res.text()).toContain(`${slug}`) - } - }) - - it('should work with dynamic path no generateStaticParams', async () => { - for (const slug of ['first', 'second']) { - const res = await fetchViaHTTP( - next.url, - `/dynamic-no-gen-params/${slug}`, - undefined, - { redirect: 'manual' } - ) - expect(res.status).toBe(200) - expect(await res.text()).toContain(`${slug}`) - } - }) - - it('should handle dynamicParams: true correctly', async () => { - const paramsToCheck = [ - { - author: 'tim', - slug: 'first-post', - }, - { - author: 'seb', - slug: 'second-post', - }, - { - author: 'styfle', - slug: 'first-post', - }, - { - author: 'new-author', - slug: 'first-post', - }, - ] - - for (const params of paramsToCheck) { - const res = await fetchViaHTTP( - next.url, - `/blog/${params.author}/${params.slug}`, - undefined, - { + for (const param of validParams) { + const res = await next.fetch(`/blog/${param}`, undefined, { redirect: 'manual', - } - ) - expect(res.status).toBe(200) - const html = await res.text() - const $ = cheerio.load(html) + }) + expect(res.status).toBe(200) + const html = await res.text() + const $ = cheerio.load(html) - expect(JSON.parse($('#params').text())).toEqual(params) - expect($('#page').text()).toBe('/blog/[author]/[slug]') - } - }) - - it('should navigate to static path correctly', async () => { - const browser = await webdriver(next.url, '/blog/tim') - await browser.eval('window.beforeNav = 1') - - expect(await browser.eval('document.documentElement.innerHTML')).toContain( - '/blog/[author]' - ) - await browser.elementByCss('#author-2').click() - - await check(async () => { - const params = JSON.parse(await browser.elementByCss('#params').text()) - return params.author === 'seb' ? 'found' : params - }, 'found') - - expect(await browser.eval('window.beforeNav')).toBe(1) - await browser.elementByCss('#author-1-post-1').click() - - await check(async () => { - const params = JSON.parse(await browser.elementByCss('#params').text()) - return params.author === 'tim' && params.slug === 'first-post' - ? 'found' - : params - }, 'found') - - expect(await browser.eval('window.beforeNav')).toBe(1) - await browser.back() - - await check(async () => { - const params = JSON.parse(await browser.elementByCss('#params').text()) - return params.author === 'seb' ? 'found' : params - }, 'found') - - expect(await browser.eval('window.beforeNav')).toBe(1) - }) - - it('should ssr dynamically when detected automatically with fetch cache option', async () => { - const pathname = '/ssr-auto/cache-no-store' - const initialRes = await fetchViaHTTP(next.url, pathname, undefined, { - redirect: 'manual', + expect(JSON.parse($('#params').text())).toEqual({ + author: param, + }) + expect($('#page').text()).toBe('/blog/[author]') + } + const invalidParams = ['timm', 'non-existent'] + + for (const param of invalidParams) { + const invalidRes = await next.fetch(`/blog/${param}`, undefined, { + redirect: 'manual', + }) + expect(invalidRes.status).toBe(404) + expect(await invalidRes.text()).toContain('page could not be found') + } }) - expect(initialRes.status).toBe(200) - const initialHtml = await initialRes.text() - const initial$ = cheerio.load(initialHtml) + it('should work with forced dynamic path', async () => { + for (const slug of ['first', 'second']) { + const res = await next.fetch( + `/dynamic-no-gen-params-ssr/${slug}`, + undefined, + { redirect: 'manual' } + ) + expect(res.status).toBe(200) + expect(await res.text()).toContain(`${slug}`) + } + }) - expect(initial$('#page').text()).toBe(pathname) - const initialDate = initial$('#date').text() + it('should work with dynamic path no generateStaticParams', async () => { + for (const slug of ['first', 'second']) { + const res = await next.fetch( + `/dynamic-no-gen-params/${slug}`, + undefined, + { redirect: 'manual' } + ) + expect(res.status).toBe(200) + expect(await res.text()).toContain(`${slug}`) + } + }) - expect(initialHtml).toContain('Example Domain') + it('should handle dynamicParams: true correctly', async () => { + const paramsToCheck = [ + { + author: 'tim', + slug: 'first-post', + }, + { + author: 'seb', + slug: 'second-post', + }, + { + author: 'styfle', + slug: 'first-post', + }, + { + author: 'new-author', + slug: 'first-post', + }, + ] + + for (const params of paramsToCheck) { + const res = await next.fetch( + `/blog/${params.author}/${params.slug}`, + undefined, + { + redirect: 'manual', + } + ) + expect(res.status).toBe(200) + const html = await res.text() + const $ = cheerio.load(html) - const secondRes = await fetchViaHTTP(next.url, pathname, undefined, { - redirect: 'manual', + expect(JSON.parse($('#params').text())).toEqual(params) + expect($('#page').text()).toBe('/blog/[author]/[slug]') + } }) - expect(secondRes.status).toBe(200) - const secondHtml = await secondRes.text() - const second$ = cheerio.load(secondHtml) + it('should navigate to static path correctly', async () => { + const browser = await next.browser('/blog/tim') + await browser.eval('window.beforeNav = 1') - expect(second$('#page').text()).toBe(pathname) - const secondDate = second$('#date').text() + expect( + await browser.eval('document.documentElement.innerHTML') + ).toContain('/blog/[author]') + await browser.elementByCss('#author-2').click() - expect(secondHtml).toContain('Example Domain') - expect(secondDate).not.toBe(initialDate) - }) + await check(async () => { + const params = JSON.parse(await browser.elementByCss('#params').text()) + return params.author === 'seb' ? 'found' : params + }, 'found') - it('should render not found pages correctly and fallback to the default one', async () => { - const res = await fetchViaHTTP(next.url, `/blog/shu/hi`, undefined, { - redirect: 'manual', - }) - expect(res.status).toBe(404) - const html = await res.text() - expect(html).toInclude('"noindex"') - expect(html).toInclude('This page could not be found.') - }) - - // TODO-APP: support fetch revalidate case for dynamic rendering - it.skip('should ssr dynamically when detected automatically with fetch revalidate option', async () => { - const pathname = '/ssr-auto/fetch-revalidate-zero' - const initialRes = await fetchViaHTTP(next.url, pathname, undefined, { - redirect: 'manual', - }) - expect(initialRes.status).toBe(200) + expect(await browser.eval('window.beforeNav')).toBe(1) + await browser.elementByCss('#author-1-post-1').click() - const initialHtml = await initialRes.text() - const initial$ = cheerio.load(initialHtml) + await check(async () => { + const params = JSON.parse(await browser.elementByCss('#params').text()) + return params.author === 'tim' && params.slug === 'first-post' + ? 'found' + : params + }, 'found') - expect(initial$('#page').text()).toBe(pathname) - const initialDate = initial$('#date').text() + expect(await browser.eval('window.beforeNav')).toBe(1) + await browser.back() - expect(initialHtml).toContain('Example Domain') + await check(async () => { + const params = JSON.parse(await browser.elementByCss('#params').text()) + return params.author === 'seb' ? 'found' : params + }, 'found') - const secondRes = await fetchViaHTTP(next.url, pathname, undefined, { - redirect: 'manual', + expect(await browser.eval('window.beforeNav')).toBe(1) }) - expect(secondRes.status).toBe(200) - const secondHtml = await secondRes.text() - const second$ = cheerio.load(secondHtml) + it('should ssr dynamically when detected automatically with fetch cache option', async () => { + const pathname = '/ssr-auto/cache-no-store' + const initialRes = await next.fetch(pathname, undefined, { + redirect: 'manual', + }) + expect(initialRes.status).toBe(200) + + const initialHtml = await initialRes.text() + const initial$ = cheerio.load(initialHtml) - expect(second$('#page').text()).toBe(pathname) - const secondDate = second$('#date').text() + expect(initial$('#page').text()).toBe(pathname) + const initialDate = initial$('#date').text() - expect(secondHtml).toContain('Example Domain') - expect(secondDate).not.toBe(initialDate) - }) + expect(initialHtml).toContain('Example Domain') - it('should ssr dynamically when forced via config', async () => { - const initialRes = await fetchViaHTTP(next.url, '/ssr-forced', undefined, { - redirect: 'manual', - }) - expect(initialRes.status).toBe(200) + const secondRes = await next.fetch(pathname, undefined, { + redirect: 'manual', + }) + expect(secondRes.status).toBe(200) + + const secondHtml = await secondRes.text() + const second$ = cheerio.load(secondHtml) - const initialHtml = await initialRes.text() - const initial$ = cheerio.load(initialHtml) + expect(second$('#page').text()).toBe(pathname) + const secondDate = second$('#date').text() - expect(initial$('#page').text()).toBe('/ssr-forced') - const initialDate = initial$('#date').text() + expect(secondHtml).toContain('Example Domain') + expect(secondDate).not.toBe(initialDate) + }) - const secondRes = await fetchViaHTTP(next.url, '/ssr-forced', undefined, { - redirect: 'manual', + it('should render not found pages correctly and fallback to the default one', async () => { + const res = await next.fetch(`/blog/shu/hi`, undefined, { + redirect: 'manual', + }) + expect(res.status).toBe(404) + const html = await res.text() + expect(html).toInclude('"noindex"') + expect(html).toInclude('This page could not be found.') }) - expect(secondRes.status).toBe(200) - const secondHtml = await secondRes.text() - const second$ = cheerio.load(secondHtml) + // TODO-APP: support fetch revalidate case for dynamic rendering + it.skip('should ssr dynamically when detected automatically with fetch revalidate option', async () => { + const pathname = '/ssr-auto/fetch-revalidate-zero' + const initialRes = await next.fetch(pathname, undefined, { + redirect: 'manual', + }) + expect(initialRes.status).toBe(200) - expect(second$('#page').text()).toBe('/ssr-forced') - const secondDate = second$('#date').text() + const initialHtml = await initialRes.text() + const initial$ = cheerio.load(initialHtml) - expect(secondDate).not.toBe(initialDate) - }) + expect(initial$('#page').text()).toBe(pathname) + const initialDate = initial$('#date').text() - describe('useSearchParams', () => { - describe('client', () => { - it('should bailout to client rendering - without suspense boundary', async () => { - const browser = await webdriver( - next.url, - '/hooks/use-search-params?first=value&second=other&third' - ) + expect(initialHtml).toContain('Example Domain') - expect(await browser.elementByCss('#params-first').text()).toBe('value') - expect(await browser.elementByCss('#params-second').text()).toBe( - 'other' - ) - expect(await browser.elementByCss('#params-third').text()).toBe('') - expect(await browser.elementByCss('#params-not-real').text()).toBe( - 'N/A' - ) + const secondRes = await next.fetch(pathname, undefined, { + redirect: 'manual', }) + expect(secondRes.status).toBe(200) - it('should bailout to client rendering - with suspense boundary', async () => { - const browser = await webdriver( - next.url, - '/hooks/use-search-params/with-suspense?first=value&second=other&third' - ) + const secondHtml = await secondRes.text() + const second$ = cheerio.load(secondHtml) - expect(await browser.elementByCss('#params-first').text()).toBe('value') - expect(await browser.elementByCss('#params-second').text()).toBe( - 'other' - ) - expect(await browser.elementByCss('#params-third').text()).toBe('') - expect(await browser.elementByCss('#params-not-real').text()).toBe( - 'N/A' - ) - }) + expect(second$('#page').text()).toBe(pathname) + const secondDate = second$('#date').text() - it.skip('should have empty search params on force-static', async () => { - const browser = await webdriver( - next.url, - '/hooks/use-search-params/force-static?first=value&second=other&third' - ) + expect(secondHtml).toContain('Example Domain') + expect(secondDate).not.toBe(initialDate) + }) - expect(await browser.elementByCss('#params-first').text()).toBe('N/A') - expect(await browser.elementByCss('#params-second').text()).toBe('N/A') - expect(await browser.elementByCss('#params-third').text()).toBe('N/A') - expect(await browser.elementByCss('#params-not-real').text()).toBe( - 'N/A' - ) + it('should ssr dynamically when forced via config', async () => { + const initialRes = await next.fetch('/ssr-forced', undefined, { + redirect: 'manual', + }) + expect(initialRes.status).toBe(200) - await browser.elementById('to-use-search-params').click() - await browser.waitForElementByCss('#hooks-use-search-params') + const initialHtml = await initialRes.text() + const initial$ = cheerio.load(initialHtml) - // Should not be empty after navigating to another page with useSearchParams - expect(await browser.elementByCss('#params-first').text()).toBe('1') - expect(await browser.elementByCss('#params-second').text()).toBe('2') - expect(await browser.elementByCss('#params-third').text()).toBe('3') - expect(await browser.elementByCss('#params-not-real').text()).toBe( - 'N/A' - ) + expect(initial$('#page').text()).toBe('/ssr-forced') + const initialDate = initial$('#date').text() + + const secondRes = await next.fetch('/ssr-forced', undefined, { + redirect: 'manual', }) + expect(secondRes.status).toBe(200) - // TODO-APP: re-enable after investigating rewrite params - if (!(global as any).isNextDeploy) { - it('should have values from canonical url on rewrite', async () => { - const browser = await webdriver( - next.url, - '/rewritten-use-search-params?first=a&second=b&third=c' + const secondHtml = await secondRes.text() + const second$ = cheerio.load(secondHtml) + + expect(second$('#page').text()).toBe('/ssr-forced') + const secondDate = second$('#date').text() + + expect(secondDate).not.toBe(initialDate) + }) + + describe('useSearchParams', () => { + describe('client', () => { + it('should bailout to client rendering - without suspense boundary', async () => { + const browser = await next.browser( + '/hooks/use-search-params?first=value&second=other&third' ) - expect(await browser.elementByCss('#params-first').text()).toBe('a') - expect(await browser.elementByCss('#params-second').text()).toBe('b') - expect(await browser.elementByCss('#params-third').text()).toBe('c') + expect(await browser.elementByCss('#params-first').text()).toBe( + 'value' + ) + expect(await browser.elementByCss('#params-second').text()).toBe( + 'other' + ) + expect(await browser.elementByCss('#params-third').text()).toBe('') expect(await browser.elementByCss('#params-not-real').text()).toBe( 'N/A' ) }) - } - }) - // Don't run these tests in dev mode since they won't be statically generated - if (!isDev) { - describe('server response', () => { - it('should bailout to client rendering - without suspense boundary', async () => { - const res = await fetchViaHTTP(next.url, '/hooks/use-search-params') - const html = await res.text() - expect(html).toInclude('') - }) it('should bailout to client rendering - with suspense boundary', async () => { - const res = await fetchViaHTTP( - next.url, - '/hooks/use-search-params/with-suspense' + const browser = await next.browser( + '/hooks/use-search-params/with-suspense?first=value&second=other&third' + ) + + expect(await browser.elementByCss('#params-first').text()).toBe( + 'value' + ) + expect(await browser.elementByCss('#params-second').text()).toBe( + 'other' + ) + expect(await browser.elementByCss('#params-third').text()).toBe('') + expect(await browser.elementByCss('#params-not-real').text()).toBe( + 'N/A' ) - const html = await res.text() - expect(html).toInclude('

search params suspense

') }) it.skip('should have empty search params on force-static', async () => { - const res = await fetchViaHTTP( - next.url, + const browser = await next.browser( '/hooks/use-search-params/force-static?first=value&second=other&third' ) - const html = await res.text() - // Shouild not bail out to client rendering - expect(html).not.toInclude('

search params suspense

') + expect(await browser.elementByCss('#params-first').text()).toBe('N/A') + expect(await browser.elementByCss('#params-second').text()).toBe( + 'N/A' + ) + expect(await browser.elementByCss('#params-third').text()).toBe('N/A') + expect(await browser.elementByCss('#params-not-real').text()).toBe( + 'N/A' + ) + + await browser.elementById('to-use-search-params').click() + await browser.waitForElementByCss('#hooks-use-search-params') - // Use empty search params instead - const $ = cheerio.load(html) - expect($('#params-first').text()).toBe('N/A') - expect($('#params-second').text()).toBe('N/A') - expect($('#params-third').text()).toBe('N/A') - expect($('#params-not-real').text()).toBe('N/A') + // Should not be empty after navigating to another page with useSearchParams + expect(await browser.elementByCss('#params-first').text()).toBe('1') + expect(await browser.elementByCss('#params-second').text()).toBe('2') + expect(await browser.elementByCss('#params-third').text()).toBe('3') + expect(await browser.elementByCss('#params-not-real').text()).toBe( + 'N/A' + ) }) + + // TODO-APP: re-enable after investigating rewrite params + if (!(global as any).isNextDeploy) { + it('should have values from canonical url on rewrite', async () => { + const browser = await next.browser( + '/rewritten-use-search-params?first=a&second=b&third=c' + ) + + expect(await browser.elementByCss('#params-first').text()).toBe('a') + expect(await browser.elementByCss('#params-second').text()).toBe( + 'b' + ) + expect(await browser.elementByCss('#params-third').text()).toBe('c') + expect(await browser.elementByCss('#params-not-real').text()).toBe( + 'N/A' + ) + }) + } }) - } - }) + // Don't run these tests in dev mode since they won't be statically generated + if (!isDev) { + describe('server response', () => { + it('should bailout to client rendering - without suspense boundary', async () => { + const res = await next.fetch('/hooks/use-search-params') + const html = await res.text() + expect(html).toInclude('') + }) + + it('should bailout to client rendering - with suspense boundary', async () => { + const res = await next.fetch( + '/hooks/use-search-params/with-suspense' + ) + const html = await res.text() + expect(html).toInclude('

search params suspense

') + }) + + it.skip('should have empty search params on force-static', async () => { + const res = await next.fetch( + '/hooks/use-search-params/force-static?first=value&second=other&third' + ) + const html = await res.text() + + // Shouild not bail out to client rendering + expect(html).not.toInclude('

search params suspense

') + + // Use empty search params instead + const $ = cheerio.load(html) + expect($('#params-first').text()).toBe('N/A') + expect($('#params-second').text()).toBe('N/A') + expect($('#params-third').text()).toBe('N/A') + expect($('#params-not-real').text()).toBe('N/A') + }) + }) + } + }) - // TODO: needs updating as usePathname should not bail - describe.skip('usePathname', () => { - if (isDev) { - it('should bail out to client rendering during SSG', async () => { - const res = await fetchViaHTTP(next.url, '/hooks/use-pathname/slug') - const html = await res.text() - expect(html).toInclude('') + // TODO: needs updating as usePathname should not bail + describe.skip('usePathname', () => { + if (isDev) { + it('should bail out to client rendering during SSG', async () => { + const res = await next.fetch('/hooks/use-pathname/slug') + const html = await res.text() + expect(html).toInclude('') + }) + } + + it('should have the correct values', async () => { + const browser = await next.browser('/hooks/use-pathname/slug') + + expect(await browser.elementByCss('#pathname').text()).toBe( + '/hooks/use-pathname/slug' + ) }) - } - it('should have the correct values', async () => { - const browser = await webdriver(next.url, '/hooks/use-pathname/slug') + it('should have values from canonical url on rewrite', async () => { + const browser = await next.browser('/rewritten-use-pathname') - expect(await browser.elementByCss('#pathname').text()).toBe( - '/hooks/use-pathname/slug' - ) + expect(await browser.elementByCss('#pathname').text()).toBe( + '/rewritten-use-pathname' + ) + }) }) - it('should have values from canonical url on rewrite', async () => { - const browser = await webdriver(next.url, '/rewritten-use-pathname') + if (!(global as any).isNextDeploy) { + it('should show a message to leave feedback for `appDir`', async () => { + expect(next.cliOutput).toContain( + `Thank you for testing \`appDir\` please leave your feedback at https://nextjs.link/app-feedback` + ) + }) + } - expect(await browser.elementByCss('#pathname').text()).toBe( - '/rewritten-use-pathname' - ) - }) - }) + it('should keep querystring on static page', async () => { + const browser = await next.browser('/blog/tim?message=hello-world') + const checkUrl = async () => + expect(await browser.url()).toBe( + next.url + '/blog/tim?message=hello-world' + ) - if (!(global as any).isNextDeploy) { - it('should show a message to leave feedback for `appDir`', async () => { - expect(next.cliOutput).toContain( - `Thank you for testing \`appDir\` please leave your feedback at https://nextjs.link/app-feedback` - ) + checkUrl() + await waitFor(1000) + checkUrl() }) } - - it('should keep querystring on static page', async () => { - const browser = await webdriver(next.url, '/blog/tim?message=hello-world') - const checkUrl = async () => - expect(await browser.url()).toBe( - next.url + '/blog/tim?message=hello-world' - ) - - checkUrl() - await waitFor(1000) - checkUrl() - }) -}) +) diff --git a/test/e2e/app-dir/asset-prefix.test.ts b/test/e2e/app-dir/asset-prefix.test.ts index 2e1f188b580220c..04c742c34fab657 100644 --- a/test/e2e/app-dir/asset-prefix.test.ts +++ b/test/e2e/app-dir/asset-prefix.test.ts @@ -1,62 +1,42 @@ -import { createNext, FileRef } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' -import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils' +import { createNextDescribe } from 'e2e-utils' import path from 'path' -import cheerio from 'cheerio' -import webdriver from 'next-webdriver' -describe('app-dir assetPrefix handling', () => { - if ((global as any).isNextDeploy) { - it('should skip next deploy for now', () => {}) - return - } - - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: new FileRef(path.join(__dirname, 'asset-prefix')), - dependencies: { - react: 'latest', - 'react-dom': 'latest', - }, - skipStart: true, +createNextDescribe( + 'app-dir assetPrefix handling', + { + files: path.join(__dirname, 'asset-prefix'), + skipDeployment: true, + }, + ({ next }) => { + it('should redirect route when requesting it directly', async () => { + const res = await next.fetch( + '/a/', + {}, + { + redirect: 'manual', + } + ) + expect(res.status).toBe(308) + expect(res.headers.get('location')).toBe(next.url + '/a') }) - await next.start() - }) - afterAll(() => next.destroy()) - - it('should redirect route when requesting it directly', async () => { - const res = await fetchViaHTTP( - next.url, - '/a/', - {}, - { - redirect: 'manual', - } - ) - expect(res.status).toBe(308) - expect(res.headers.get('location')).toBe(next.url + '/a') - }) - - it('should render link', async () => { - const html = await renderViaHTTP(next.url, '/') - const $ = cheerio.load(html) - expect($('#to-a-trailing-slash').attr('href')).toBe('/a') - }) + it('should render link', async () => { + const $ = await next.render$('/') + expect($('#to-a-trailing-slash').attr('href')).toBe('/a') + }) - it('should redirect route when requesting it directly by browser', async () => { - const browser = await webdriver(next.url, '/a') - expect(await browser.waitForElementByCss('#a-page').text()).toBe('A page') - }) + it('should redirect route when requesting it directly by browser', async () => { + const browser = await next.browser('/a') + expect(await browser.waitForElementByCss('#a-page').text()).toBe('A page') + }) - it('should redirect route when clicking link', async () => { - const browser = await webdriver(next.url, '/') - await browser - .elementByCss('#to-a-trailing-slash') - .click() - .waitForElementByCss('#a-page') - expect(await browser.waitForElementByCss('#a-page').text()).toBe('A page') - }) -}) + it('should redirect route when clicking link', async () => { + const browser = await next.browser('/') + await browser + .elementByCss('#to-a-trailing-slash') + .click() + .waitForElementByCss('#a-page') + expect(await browser.waitForElementByCss('#a-page').text()).toBe('A page') + }) + } +) diff --git a/test/e2e/app-dir/async-component-preload.test.ts b/test/e2e/app-dir/async-component-preload.test.ts index 813892ac949e8e9..5531acc67d32f0d 100644 --- a/test/e2e/app-dir/async-component-preload.test.ts +++ b/test/e2e/app-dir/async-component-preload.test.ts @@ -1,29 +1,18 @@ -import { createNext, FileRef } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' -import webdriver from 'next-webdriver' +import { createNextDescribe } from 'e2e-utils' import path from 'path' -describe('async-component-preload', () => { - if ((global as any).isNextDeploy) { - it('should skip next deploy for now', () => {}) - return - } - - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: new FileRef(path.join(__dirname, 'async-component-preload')), - dependencies: { - react: 'latest', - 'react-dom': 'latest', - }, +createNextDescribe( + 'async-component-preload', + { + files: path.join(__dirname, 'async-component-preload'), + skipDeployment: true, + }, + ({ next }) => { + it('should handle redirect in an async page', async () => { + const browser = await next.browser('/') + expect(await browser.waitForElementByCss('#success').text()).toBe( + 'Success' + ) }) - }) - afterAll(() => next.destroy()) - - it('should handle redirect in an async page', async () => { - const browser = await webdriver(next.url, '/') - expect(await browser.waitForElementByCss('#success').text()).toBe('Success') - }) -}) + } +) diff --git a/test/e2e/app-dir/back-button-download-bug.test.ts b/test/e2e/app-dir/back-button-download-bug.test.ts index b610bfc4808c727..231dd04653fe214 100644 --- a/test/e2e/app-dir/back-button-download-bug.test.ts +++ b/test/e2e/app-dir/back-button-download-bug.test.ts @@ -1,42 +1,30 @@ -import { createNext, FileRef } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' +import { createNextDescribe } from 'e2e-utils' import path from 'path' -import webdriver from 'next-webdriver' // TODO-APP: fix test as it's failing randomly describe.skip('app-dir back button download bug', () => { - if ((global as any).isNextDeploy) { - it('should skip next deploy for now', () => {}) - return - } + createNextDescribe( + 'app-dir back button download bug', + { + files: path.join(__dirname, 'back-button-download-bug'), + skipDeployment: true, + }, + ({ next }) => { + it('should redirect route when clicking link', async () => { + const browser = await next.browser('/') + const text = await browser + .elementByCss('#to-post-1') + .click() + .waitForElementByCss('#post-page') + .text() + expect(text).toBe('This is the post page') - let next: NextInstance + await browser.back() - beforeAll(async () => { - next = await createNext({ - files: new FileRef(path.join(__dirname, 'back-button-download-bug')), - dependencies: { - react: 'latest', - 'react-dom': 'latest', - }, - skipStart: true, - }) - - await next.start() - }) - afterAll(() => next.destroy()) - - it('should redirect route when clicking link', async () => { - const browser = await webdriver(next.url, '/') - const text = await browser - .elementByCss('#to-post-1') - .click() - .waitForElementByCss('#post-page') - .text() - expect(text).toBe('This is the post page') - - await browser.back() - - expect(await browser.waitForElementByCss('#home-page').text()).toBe('Home!') - }) + expect(await browser.waitForElementByCss('#home-page').text()).toBe( + 'Home!' + ) + }) + } + ) }) diff --git a/test/e2e/app-dir/create-root-layout.test.ts b/test/e2e/app-dir/create-root-layout.test.ts index 9583bc385948d20..53fa0e8fc3353fb 100644 --- a/test/e2e/app-dir/create-root-layout.test.ts +++ b/test/e2e/app-dir/create-root-layout.test.ts @@ -1,7 +1,6 @@ import path from 'path' import { createNext, FileRef } from 'e2e-utils' import { NextInstance } from 'test/lib/next-modes/base' -import webdriver from 'next-webdriver' import { check } from 'next-test-utils' describe('app-dir create root layout', () => { @@ -12,10 +11,6 @@ describe('app-dir create root layout', () => { return } - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } let next: NextInstance if (isDev) { @@ -39,7 +34,7 @@ describe('app-dir create root layout', () => { it('create root layout', async () => { const outputIndex = next.cliOutput.length - const browser = await webdriver(next.url, '/route') + const browser = await next.browser('/route') expect(await browser.elementById('page-text').text()).toBe( 'Hello world!' @@ -101,7 +96,7 @@ describe('app-dir create root layout', () => { it('create root layout', async () => { const outputIndex = next.cliOutput.length - const browser = await webdriver(next.url, '/') + const browser = await next.browser('/') expect(await browser.elementById('page-text').text()).toBe( 'Hello world' @@ -168,7 +163,7 @@ describe('app-dir create root layout', () => { it('create root layout', async () => { const outputIndex = next.cliOutput.length - const browser = await webdriver(next.url, '/route/second/inner') + const browser = await next.browser('/route/second/inner') expect(await browser.elementById('page-text').text()).toBe( 'Hello world' @@ -236,7 +231,7 @@ describe('app-dir create root layout', () => { it('create root layout', async () => { const outputIndex = next.cliOutput.length - const browser = await webdriver(next.url, '/') + const browser = await next.browser('/') expect(await browser.elementById('page-text').text()).toBe( 'Hello world!' diff --git a/test/e2e/app-dir/dynamic-href.test.ts b/test/e2e/app-dir/dynamic-href.test.ts index 5f5d73ecef249ed..5f4ee257bfe166a 100644 --- a/test/e2e/app-dir/dynamic-href.test.ts +++ b/test/e2e/app-dir/dynamic-href.test.ts @@ -1,75 +1,63 @@ -import { createNext, FileRef } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' +import { createNextDescribe } from 'e2e-utils' import { getRedboxDescription, hasRedbox } from 'next-test-utils' import path from 'path' -import webdriver from 'next-webdriver' -describe('dynamic-href', () => { - const isDev = (global as any).isNextDev - if ((global as any).isNextDeploy) { - it('should skip next deploy for now', () => {}) - return - } - - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: new FileRef(path.join(__dirname, 'dynamic-href')), - dependencies: { - react: 'latest', - 'react-dom': 'latest', - }, - }) - }) - afterAll(() => next.destroy()) - - if (isDev) { - it('should error when using dynamic href.pathname in app dir', async () => { - const browser = await webdriver(next.url, '/object') - - // Error should show up - expect(await hasRedbox(browser, true)).toBeTrue() - expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( - `"Error: Dynamic href \`/object/[slug]\` found in while using the \`/app\` router, this is not supported. Read more: https://nextjs.org/docs/messages/app-dir-dynamic-href"` - ) +createNextDescribe( + 'dynamic-href', + { + files: path.join(__dirname, 'dynamic-href'), + skipDeployment: true, + }, + ({ isNextDev: isDev, next }) => { + if (isDev) { + it('should error when using dynamic href.pathname in app dir', async () => { + const browser = await next.browser('/object') + + // Error should show up + expect(await hasRedbox(browser, true)).toBeTrue() + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: Dynamic href \`/object/[slug]\` found in while using the \`/app\` router, this is not supported. Read more: https://nextjs.org/docs/messages/app-dir-dynamic-href"` + ) - // Fix error - const pageContent = await next.readFile('app/object/page.js') - await next.patchFile( - 'app/object/page.js', - pageContent.replace( - "pathname: '/object/[slug]'", - "pathname: '/object/slug'" + // Fix error + const pageContent = await next.readFile('app/object/page.js') + await next.patchFile( + 'app/object/page.js', + pageContent.replace( + "pathname: '/object/[slug]'", + "pathname: '/object/slug'" + ) + ) + expect(await browser.waitForElementByCss('#link').text()).toBe( + 'to slug' ) - ) - expect(await browser.waitForElementByCss('#link').text()).toBe('to slug') - // Navigate to new page - await browser.elementByCss('#link').click() - expect(await browser.waitForElementByCss('#pathname').text()).toBe( - '/object/slug' - ) - expect(await browser.elementByCss('#slug').text()).toBe('1') - }) + // Navigate to new page + await browser.elementByCss('#link').click() + expect(await browser.waitForElementByCss('#pathname').text()).toBe( + '/object/slug' + ) + expect(await browser.elementByCss('#slug').text()).toBe('1') + }) - it('should error when using dynamic href in app dir', async () => { - const browser = await webdriver(next.url, '/string') + it('should error when using dynamic href in app dir', async () => { + const browser = await next.browser('/string') - // Error should show up - expect(await hasRedbox(browser, true)).toBeTrue() - expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( - `"Error: Dynamic href \`/object/[slug]\` found in while using the \`/app\` router, this is not supported. Read more: https://nextjs.org/docs/messages/app-dir-dynamic-href"` - ) - }) - } else { - it('should not error on /object in prod', async () => { - const browser = await webdriver(next.url, '/object') - expect(await browser.elementByCss('#link').text()).toBe('to slug') - }) - it('should not error on /string in prod', async () => { - const browser = await webdriver(next.url, '/string') - expect(await browser.elementByCss('#link').text()).toBe('to slug') - }) + // Error should show up + expect(await hasRedbox(browser, true)).toBeTrue() + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: Dynamic href \`/object/[slug]\` found in while using the \`/app\` router, this is not supported. Read more: https://nextjs.org/docs/messages/app-dir-dynamic-href"` + ) + }) + } else { + it('should not error on /object in prod', async () => { + const browser = await next.browser('/object') + expect(await browser.elementByCss('#link').text()).toBe('to slug') + }) + it('should not error on /string in prod', async () => { + const browser = await next.browser('/string') + expect(await browser.elementByCss('#link').text()).toBe('to slug') + }) + } } -}) +) diff --git a/test/e2e/app-dir/global-error.test.ts b/test/e2e/app-dir/global-error.test.ts index 2f6b37f29ae03b5..f6bb42219d3d8c1 100644 --- a/test/e2e/app-dir/global-error.test.ts +++ b/test/e2e/app-dir/global-error.test.ts @@ -1,35 +1,29 @@ import path from 'path' import { getRedboxHeader, hasRedbox } from 'next-test-utils' -import { createNext, FileRef } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' -import webdriver from 'next-webdriver' +import { createNextDescribe } from 'e2e-utils' -describe('app dir - global error', () => { - let next: NextInstance - const isDev = (global as any).isNextDev +createNextDescribe( + 'app dir - global error', + { + files: path.join(__dirname, './global-error'), + }, + ({ next, isNextDev }) => { + it('should trigger error component when an error happens during rendering', async () => { + const browser = await next.browser('/throw') + await browser + .waitForElementByCss('#error-trigger-button') + .elementByCss('#error-trigger-button') + .click() - beforeAll(async () => { - next = await createNext({ - files: new FileRef(path.join(__dirname, './global-error')), + if (isNextDev) { + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxHeader(browser)).toMatch(/Error: Client error/) + } else { + await browser + expect(await browser.elementByCss('#error').text()).toBe( + 'Error message: Client error' + ) + } }) - }) - afterAll(() => next.destroy()) - - it('should trigger error component when an error happens during rendering', async () => { - const browser = await webdriver(next.url, '/throw') - await browser - .waitForElementByCss('#error-trigger-button') - .elementByCss('#error-trigger-button') - .click() - - if (isDev) { - expect(await hasRedbox(browser)).toBe(true) - expect(await getRedboxHeader(browser)).toMatch(/Error: Client error/) - } else { - await browser - expect(await browser.elementByCss('#error').text()).toBe( - 'Error message: Client error' - ) - } - }) -}) + } +) diff --git a/test/e2e/app-dir/import.test.ts b/test/e2e/app-dir/import.test.ts index 76af9e1405f8a5e..ed434e39317e38b 100644 --- a/test/e2e/app-dir/import.test.ts +++ b/test/e2e/app-dir/import.test.ts @@ -1,38 +1,24 @@ import path from 'path' -import cheerio from 'cheerio' -import { createNext, FileRef } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' -import { renderViaHTTP } from 'next-test-utils' +import { createNextDescribe } from 'e2e-utils' -describe('app dir imports', () => { - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } - let next: NextInstance - - function runTests() { - beforeAll(async () => { - next = await createNext({ - files: new FileRef(path.join(__dirname, 'import')), - dependencies: { - react: 'latest', - 'react-dom': 'latest', - typescript: 'latest', - '@types/react': 'latest', - '@types/node': 'latest', - }, - }) - }) - afterAll(() => next.destroy()) +createNextDescribe( + 'app dir imports', + { + files: path.join(__dirname, 'import'), + dependencies: { + react: 'latest', + 'react-dom': 'latest', + typescript: 'latest', + '@types/react': 'latest', + '@types/node': 'latest', + }, + }, + ({ next }) => { ;['js', 'jsx', 'ts', 'tsx'].forEach((ext) => { it(`we can import all components from .${ext}`, async () => { - const html = await renderViaHTTP(next.url, `/${ext}`) - const $ = cheerio.load(html) + const $ = await next.render$(`/${ext}`) expect($('#js').text()).toBe('CompJs') }) }) } - - runTests() -}) +) diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index 912486fcd26db7b..8a5cf0ab95cc6ec 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -1,61 +1,50 @@ -import { createNext, FileRef } from 'e2e-utils' +import { createNextDescribe } from 'e2e-utils' import crypto from 'crypto' -import { NextInstance } from 'test/lib/next-modes/base' -import { - check, - fetchViaHTTP, - getRedboxHeader, - hasRedbox, - renderViaHTTP, - waitFor, -} from 'next-test-utils' +import { check, getRedboxHeader, hasRedbox, waitFor } from 'next-test-utils' import path from 'path' import cheerio from 'cheerio' -import webdriver from 'next-webdriver' - -describe('app dir', () => { - const isDev = (global as any).isNextDev - let next: NextInstance - - function runTests() { - beforeAll(async () => { - next = await createNext({ - files: new FileRef(path.join(__dirname, 'app')), - dependencies: { - swr: '2.0.0-rc.0', - react: 'latest', - 'react-dom': 'latest', - sass: 'latest', - }, - }) - }) - afterAll(async () => { - await next.destroy() - }) - if (!(global as any).isNextDeploy) { +createNextDescribe( + 'app dir', + { + files: path.join(__dirname, 'app'), + dependencies: { + swr: '2.0.0-rc.0', + react: 'latest', + 'react-dom': 'latest', + sass: 'latest', + }, + }, + ({ next, isNextDev: isDev, isNextStart, isNextDeploy }) => { + if (!isNextDeploy) { it('should not share edge workers', async () => { const controller1 = new AbortController() const controller2 = new AbortController() - fetchViaHTTP(next.url, '/slow-page-no-loading', undefined, { - signal: controller1.signal, - }).catch(() => {}) - fetchViaHTTP(next.url, '/slow-page-no-loading', undefined, { - signal: controller2.signal, - }).catch(() => {}) + next + .fetch('/slow-page-no-loading', undefined, { + signal: controller1.signal, + }) + .catch(() => {}) + next + .fetch('/slow-page-no-loading', undefined, { + signal: controller2.signal, + }) + .catch(() => {}) await waitFor(1000) controller1.abort() const controller3 = new AbortController() - fetchViaHTTP(next.url, '/slow-page-no-loading', undefined, { - signal: controller3.signal, - }).catch(() => {}) + next + .fetch('/slow-page-no-loading', undefined, { + signal: controller3.signal, + }) + .catch(() => {}) await waitFor(1000) controller2.abort() controller3.abort() - const res = await fetchViaHTTP(next.url, '/slow-page-no-loading') + const res = await next.fetch('/slow-page-no-loading') expect(res.status).toBe(200) expect(await res.text()).toContain('hello from slow page') expect(next.cliOutput).not.toContain( @@ -64,7 +53,7 @@ describe('app dir', () => { }) } - if ((global as any).isNextStart) { + if (isNextStart) { it('should generate build traces correctly', async () => { const trace = JSON.parse( await next.readFile( @@ -78,8 +67,7 @@ describe('app dir', () => { } it('should use application/octet-stream for flight', async () => { - const res = await fetchViaHTTP( - next.url, + const res = await next.fetch( '/dashboard/deployments/123', {}, { @@ -92,8 +80,7 @@ describe('app dir', () => { }) it('should use application/octet-stream for flight with edge runtime', async () => { - const res = await fetchViaHTTP( - next.url, + const res = await next.fetch( '/dashboard', {}, { @@ -106,13 +93,12 @@ describe('app dir', () => { }) it('should pass props from getServerSideProps in root layout', async () => { - const html = await renderViaHTTP(next.url, '/dashboard') - const $ = cheerio.load(html) + const $ = await next.render$('/dashboard') expect($('title').text()).toBe('hello world') }) it('should serve from pages', async () => { - const html = await renderViaHTTP(next.url, '/') + const html = await next.render('/') expect(html).toContain('hello from pages/index') // esm imports should work fine in pages/ @@ -120,27 +106,27 @@ describe('app dir', () => { }) it('should serve dynamic route from pages', async () => { - const html = await renderViaHTTP(next.url, '/blog/first') + const html = await next.render('/blog/first') expect(html).toContain('hello from pages/blog/[slug]') }) it('should serve from public', async () => { - const html = await renderViaHTTP(next.url, '/hello.txt') + const html = await next.render('/hello.txt') expect(html).toContain('hello world') }) it('should serve from app', async () => { - const html = await renderViaHTTP(next.url, '/dashboard') + const html = await next.render('/dashboard') expect(html).toContain('hello from app/dashboard') }) - if (!(global as any).isNextDeploy) { + if (!isNextDeploy) { it('should serve /index as separate page', async () => { const stderr = [] next.on('stderr', (err) => { stderr.push(err) }) - const html = await renderViaHTTP(next.url, '/dashboard/index') + const html = await next.render('/dashboard/index') expect(html).toContain('hello from app/dashboard/index') expect(stderr.some((err) => err.includes('Invalid hook call'))).toBe( false @@ -148,8 +134,7 @@ describe('app dir', () => { }) it('should handle next/dynamic correctly', async () => { - const html = await renderViaHTTP(next.url, '/dashboard/dynamic') - const $ = cheerio.load(html) + const $ = await next.render$('/dashboard/dynamic') // filter out the script const selector = 'body div' const serverContent = $(selector).text() @@ -168,9 +153,9 @@ describe('app dir', () => { ) // client component under server component with ssr: false will not be rendered either in flight or SSR - expect(html).not.toContain('client component under sever no ssr') + expect($.html()).not.toContain('client component under sever no ssr') - const browser = await webdriver(next.url, '/dashboard/dynamic') + const browser = await next.browser('/dashboard/dynamic') const clientContent = await browser.elementByCss(selector).text() expect(clientContent).toContain('next-dynamic dynamic no ssr on server') expect(clientContent).toContain('client component under sever no ssr') @@ -182,7 +167,7 @@ describe('app dir', () => { }) it('should serve polyfills for browsers that do not support modules', async () => { - const html = await renderViaHTTP(next.url, '/dashboard/index') + const html = await next.render('/dashboard/index') expect(html).toMatch( /